diff --git a/cloudsmith_cli/core/keyring.py b/cloudsmith_cli/core/keyring.py index 5e721098..54844c72 100644 --- a/cloudsmith_cli/core/keyring.py +++ b/cloudsmith_cli/core/keyring.py @@ -2,9 +2,6 @@ import os from datetime import datetime, timedelta, timezone -import keyring -from keyring.errors import KeyringError - ACCESS_TOKEN_KEY = "cloudsmith_cli-access_token-{api_host}" @@ -47,12 +44,17 @@ def _prepare_keyring_backend(): them through the library's own documented mechanism. Apply them here instead, once the backend has been resolved. """ + import keyring + _sync_keyring_backend_env() _sync_keyring_property_env() keyring.get_keyring().set_properties_from_env() def _get_value(key): + import keyring + from keyring.errors import KeyringError + _prepare_keyring_backend() username = _get_username() try: @@ -62,6 +64,8 @@ def _get_value(key): def _set_value(key, value): + import keyring + _prepare_keyring_backend() username = _get_username() keyring.set_password(key, username, value) @@ -142,6 +146,9 @@ def store_sso_tokens(api_host, access_token, refresh_token): def _delete_value(key): + import keyring + from keyring.errors import KeyringError + _prepare_keyring_backend() username = _get_username() try: @@ -178,6 +185,8 @@ def delete_sso_tokens(api_host): def store_oidc_token(api_host, org, service_slug, token_data): """Store OIDC token in keyring if enabled.""" + from keyring.errors import KeyringError + if not should_use_keyring(): return False diff --git a/packaging/pyinstaller/entry.py b/packaging/pyinstaller/entry.py index 16c6e5ea..0c04a90d 100644 --- a/packaging/pyinstaller/entry.py +++ b/packaging/pyinstaller/entry.py @@ -4,8 +4,6 @@ import pkgutil import sys -import keyring.backend - import cloudsmith_cli from cloudsmith_cli.cli.commands.main import main @@ -39,6 +37,8 @@ def _check_extra_keyring_backends(failed: list) -> None: absent from discovery rather than raising, so this checks the discovered class list explicitly instead of relying on an import error. """ + import keyring.backend + discovered = [type(b).__module__ for b in keyring.backend.get_all_keyring()] for module_prefix in ("keyrings.cryptfile", "keyrings.alt"): if not any(name.startswith(module_prefix) for name in discovered): diff --git a/pyproject.toml b/pyproject.toml index b0d30268..76289af2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,8 +142,34 @@ show_missing = true directory = "reports/coverage" [tool.ruff.lint] -extend-select = ["FA", "TC"] +extend-select = ["FA", "TC", "TID253"] ignore = ["TRY002", "BLE001"] +[tool.ruff.lint.flake8-tidy-imports] +# These modules dominate CLI startup time. Import them inside functions, +# or add the file to the allowlist below when a module is itself loaded +# lazily (see cli/commands/registry.py and cli/tests/test_startup_imports.py). +banned-module-level-imports = [ + "cloudsmith_api", + "httpx", + "keyring", + "mcp", + "requests", + "rich", + "semver", + "urllib3", +] + [tool.ruff.lint.per-file-ignores] '__init__.py' = ["F401"] +# Lazy leaves: modules that are only imported when their command or code +# path runs, so module-level imports of heavy modules are free at startup. +'cloudsmith_cli/cli/commands/*' = ["TID253"] +'cloudsmith_cli/cli/saml.py' = ["TID253"] +'cloudsmith_cli/conftest.py' = ["TID253"] +'cloudsmith_cli/core/api/*' = ["TID253"] +'cloudsmith_cli/core/credentials/oidc/exchange.py' = ["TID253"] +'cloudsmith_cli/core/download.py' = ["TID253"] +'cloudsmith_cli/core/mcp/*' = ["TID253"] +'cloudsmith_cli/core/rest.py' = ["TID253"] +'cloudsmith_cli/core/session.py' = ["TID253"]