From f6393eaaac21d9ebc0ed13c00ed04eb9b50ab800 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Sat, 22 Aug 2026 00:33:19 +0100 Subject: [PATCH 1/2] chore(no-ticket): ban module-level imports of startup-heavy modules Select TID253 with a ban list of the modules that dominated CLI startup. Files that load lazily are allowlisted per file. Defer the keyring import in the frozen entrypoint to the selftest that uses it. Co-Authored-By: Claude Fable 5 --- packaging/pyinstaller/entry.py | 4 ++-- pyproject.toml | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) 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..49bf3019 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,8 +142,35 @@ 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/keyring.py' = ["TID253"] +'cloudsmith_cli/core/mcp/*' = ["TID253"] +'cloudsmith_cli/core/rest.py' = ["TID253"] +'cloudsmith_cli/core/session.py' = ["TID253"] From f7601a45051374a0b8268b7b4154ebd31158636f Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Sat, 22 Aug 2026 00:40:30 +0100 Subject: [PATCH 2/2] perf(no-ticket): defer the keyring library import core/keyring.py sits on the eager import path through the credential chain. Import the keyring library inside the functions that use it and remove the file from the TID253 allowlist. Co-Authored-By: Claude Fable 5 --- cloudsmith_cli/core/keyring.py | 15 ++++++++++++--- pyproject.toml | 1 - 2 files changed, 12 insertions(+), 4 deletions(-) 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/pyproject.toml b/pyproject.toml index 49bf3019..76289af2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -170,7 +170,6 @@ banned-module-level-imports = [ 'cloudsmith_cli/core/api/*' = ["TID253"] 'cloudsmith_cli/core/credentials/oidc/exchange.py' = ["TID253"] 'cloudsmith_cli/core/download.py' = ["TID253"] -'cloudsmith_cli/core/keyring.py' = ["TID253"] 'cloudsmith_cli/core/mcp/*' = ["TID253"] 'cloudsmith_cli/core/rest.py' = ["TID253"] 'cloudsmith_cli/core/session.py' = ["TID253"]