Skip to content

Commit

Permalink
Convert the lazy importer to a "pyi" reader
Browse files Browse the repository at this point in the history
- Create an `__init__.pyi` which declares all of the imports and the
  `__all__` tuple for the top-level package. Tests confirm that `mypy`
  will respect this file.
- Expand the implementation of lazy imports to handle more details and
  to parse the data from `__init__.pyi`.
- The lazy importer implementation now provides `__all__` copied (at
  runtime) out of the `__init__.pyi`, and builds implementations for
  the `__init__.py`.
  • Loading branch information
sirosen committed Nov 7, 2024
1 parent ffc1087 commit 1f6a624
Show file tree
Hide file tree
Showing 7 changed files with 443 additions and 324 deletions.
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
exclude = .git,.tox,__pycache__,.eggs,dist,.venv*,docs,build
max-line-length = 88
extend-ignore = W503,W504,E203

# in pyi stubs, spacing rules are different (black handles this)
per-file-ignores = *.pyi:E302,E305
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ where = ["src"]
namespaces = false

[tool.setuptools.package-data]
globus_sdk = ["py.typed"]
globus_sdk = [
"py.typed",
"__init__.pyi",
]
"globus_sdk.login_flows.local_server_login_flow_manager.html_files" = ["*.html"]

[tool.setuptools.dynamic.version]
Expand Down Expand Up @@ -189,6 +192,9 @@ disable = [
"import-error",
# "disallowed" usage of our own classes and objects gets underfoot
"protected-access",
# incorrect mis-reporting of lazily loaded attributes makes this lint
# unusable
"no-name-in-module",
# objections to log messages doing eager (vs lazy) string formatting
# the perf benefit of deferred logging doesn't always outweigh the readability cost
"logging-fstring-interpolation", "logging-format-interpolation",
Expand Down
2 changes: 1 addition & 1 deletion scripts/ensure_exports_are_documented.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
_ALL_NAME_PATTERN = re.compile(r'\s+"(\w+)",?')

PACKAGE_LOCS_TO_SCAN = (
"globus_sdk/",
"globus_sdk/__init__.pyi",
"globus_sdk/login_flows/",
"globus_sdk/gare/",
"globus_sdk/globus_app/",
Expand Down
294 changes: 18 additions & 276 deletions src/globus_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import logging
import sys
import typing as t

from .version import __version__
from ._lazy_import import (
default_dir_implementation,
default_getattr_implementation,
load_all_tuple,
)
from .version import __version__ # noqa: F401


def _force_eager_imports() -> None:
Expand All @@ -12,281 +16,19 @@ def _force_eager_imports() -> None:
getattr(current_module, attr)


if t.TYPE_CHECKING:
from .authorizers import (
AccessTokenAuthorizer,
BasicAuthorizer,
ClientCredentialsAuthorizer,
NullAuthorizer,
RefreshTokenAuthorizer,
)
from .client import BaseClient
from .exc import (
ErrorSubdocument,
GlobusAPIError,
GlobusConnectionError,
GlobusConnectionTimeoutError,
GlobusError,
GlobusSDKUsageError,
GlobusTimeoutError,
NetworkError,
RemovedInV4Warning,
ValidationError,
)
from .globus_app import ClientApp, GlobusApp, GlobusAppConfig, UserApp
from .local_endpoint import (
GlobusConnectPersonalOwnerInfo,
LocalGlobusConnectPersonal,
LocalGlobusConnectServer,
)
from .response import ArrayResponse, GlobusHTTPResponse, IterableResponse
from .scopes import Scope, ScopeCycleError, ScopeParseError
from .services.auth import (
AuthAPIError,
AuthClient,
AuthLoginClient,
ConfidentialAppAuthClient,
DependentScopeSpec,
GetConsentsResponse,
GetIdentitiesResponse,
IdentityMap,
NativeAppAuthClient,
OAuthAuthorizationCodeResponse,
OAuthClientCredentialsResponse,
OAuthDependentTokenResponse,
OAuthRefreshTokenResponse,
OAuthTokenResponse,
)
from .services.compute import (
ComputeAPIError,
ComputeClient,
ComputeClientV2,
ComputeClientV3,
ComputeFunctionDocument,
ComputeFunctionMetadata,
)
from .services.flows import (
FlowsAPIError,
FlowsClient,
IterableFlowsResponse,
SpecificFlowClient,
)
from .services.gcs import (
ActiveScaleStoragePolicies,
AzureBlobStoragePolicies,
BlackPearlStoragePolicies,
BoxStoragePolicies,
CephStoragePolicies,
CollectionDocument,
CollectionPolicies,
ConnectorTable,
EndpointDocument,
GCSAPIError,
GCSClient,
GCSRoleDocument,
GlobusConnectServerConnector,
GoogleCloudStorageCollectionPolicies,
GoogleCloudStoragePolicies,
GoogleDriveStoragePolicies,
GuestCollectionDocument,
HPSSStoragePolicies,
IrodsStoragePolicies,
IterableGCSResponse,
MappedCollectionDocument,
OneDriveStoragePolicies,
POSIXCollectionPolicies,
POSIXStagingCollectionPolicies,
POSIXStagingStoragePolicies,
POSIXStoragePolicies,
S3StoragePolicies,
StorageGatewayDocument,
StorageGatewayPolicies,
UnpackingGCSResponse,
UserCredentialDocument,
)
from .services.groups import (
BatchMembershipActions,
GroupMemberVisibility,
GroupPolicies,
GroupRequiredSignupFields,
GroupRole,
GroupsAPIError,
GroupsClient,
GroupsManager,
GroupVisibility,
)
from .services.search import (
SearchAPIError,
SearchClient,
SearchQuery,
SearchQueryV1,
SearchScrollQuery,
)
from .services.timer import TimerAPIError, TimerClient
from .services.timers import (
OnceTimerSchedule,
RecurringTimerSchedule,
TimerJob,
TimersAPIError,
TimersClient,
TransferTimer,
)
from .services.transfer import (
ActivationRequirementsResponse,
DeleteData,
IterableTransferResponse,
TransferAPIError,
TransferClient,
TransferData,
)
from .utils import MISSING, MissingType

else:

def __dir__() -> t.List[str]:
# dir(globus_sdk) should include everything exported in __all__
# as well as some explicitly selected attributes from the default dir() output
# on a module
#
# see also:
# https://discuss.python.org/t/how-to-properly-extend-standard-dir-search-with-module-level-dir/4202
return list(__all__) + [
# __all__ itself can be inspected
"__all__",
# useful to figure out where a package is installed
"__file__",
"__path__",
]

def __getattr__(name: str) -> t.Any:
from ._lazy_import import load_attr
#
# all lazy SDK attributes are defined in __init__.pyi
#
# to add an attribute, write the relevant import in `__init__.pyi` and update
# the `__all__` tuple there
#
__all__ = load_all_tuple(__name__, "__init__.pyi")
__getattr__ = default_getattr_implementation(__name__, "__init__.pyi")
__dir__ = default_dir_implementation(__name__)

if name in __all__:
value = load_attr(__name__, name)
setattr(sys.modules[__name__], name, value)
return value

raise AttributeError(f"module {__name__} has no attribute {name}")


__all__ = (
"__version__",
"_force_eager_imports",
"AccessTokenAuthorizer",
"ActivationRequirementsResponse",
"ActiveScaleStoragePolicies",
"ArrayResponse",
"AuthAPIError",
"AuthClient",
"AuthLoginClient",
"AzureBlobStoragePolicies",
"BaseClient",
"BasicAuthorizer",
"BatchMembershipActions",
"BlackPearlStoragePolicies",
"BoxStoragePolicies",
"CephStoragePolicies",
"ClientApp",
"ClientCredentialsAuthorizer",
"CollectionDocument",
"CollectionPolicies",
"ComputeAPIError",
"ComputeClient",
"ComputeClientV2",
"ComputeClientV3",
"ComputeFunctionDocument",
"ComputeFunctionMetadata",
"ConfidentialAppAuthClient",
"ConnectorTable",
"DeleteData",
"DependentScopeSpec",
"EndpointDocument",
"ErrorSubdocument",
"FlowsAPIError",
"FlowsClient",
"GCSAPIError",
"GCSClient",
"GCSRoleDocument",
"GetConsentsResponse",
"GetIdentitiesResponse",
"GlobusAPIError",
"GlobusApp",
"GlobusAppConfig",
"GlobusConnectPersonalOwnerInfo",
"GlobusConnectServerConnector",
"GlobusConnectionError",
"GlobusConnectionTimeoutError",
"GlobusError",
"GlobusHTTPResponse",
"GlobusSDKUsageError",
"GlobusTimeoutError",
"GoogleCloudStorageCollectionPolicies",
"GoogleCloudStoragePolicies",
"GoogleDriveStoragePolicies",
"GroupMemberVisibility",
"GroupPolicies",
"GroupRequiredSignupFields",
"GroupRole",
"GroupVisibility",
"GroupsAPIError",
"GroupsClient",
"GroupsManager",
"GuestCollectionDocument",
"HPSSStoragePolicies",
"IdentityMap",
"IrodsStoragePolicies",
"IterableFlowsResponse",
"IterableGCSResponse",
"IterableResponse",
"IterableTransferResponse",
"LocalGlobusConnectPersonal",
"LocalGlobusConnectServer",
"MISSING",
"MappedCollectionDocument",
"MissingType",
"NativeAppAuthClient",
"NetworkError",
"NullAuthorizer",
"OAuthAuthorizationCodeResponse",
"OAuthClientCredentialsResponse",
"OAuthDependentTokenResponse",
"OAuthRefreshTokenResponse",
"OAuthTokenResponse",
"OnceTimerSchedule",
"OneDriveStoragePolicies",
"POSIXCollectionPolicies",
"POSIXStagingCollectionPolicies",
"POSIXStagingStoragePolicies",
"POSIXStoragePolicies",
"RecurringTimerSchedule",
"RefreshTokenAuthorizer",
"RemovedInV4Warning",
"S3StoragePolicies",
"Scope",
"ScopeCycleError",
"ScopeParseError",
"SearchAPIError",
"SearchClient",
"SearchQuery",
"SearchQueryV1",
"SearchScrollQuery",
"SpecificFlowClient",
"StorageGatewayDocument",
"StorageGatewayPolicies",
"TimerAPIError",
"TimerClient",
"TimerJob",
"TimersAPIError",
"TimersClient",
"TransferAPIError",
"TransferClient",
"TransferData",
"TransferTimer",
"UnpackingGCSResponse",
"UserApp",
"UserCredentialDocument",
"ValidationError",
)
del load_all_tuple
del default_getattr_implementation
del default_dir_implementation


# configure logging for a library, per python best practices:
Expand Down
Loading

0 comments on commit 1f6a624

Please sign in to comment.