From 001de3bf8ad5558d774390b138ad587990e354b3 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 be3b91aa..faf3b014 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,8 +146,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 a24d3db34e6a088345321973db7a9be0de61c148 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 cb46ff6e..8b186472 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}" @@ -81,6 +78,8 @@ 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() _sync_keyring_file_path_env() @@ -90,6 +89,9 @@ def _prepare_keyring_backend(): def _get_value(key): + import keyring + from keyring.errors import KeyringError + _prepare_keyring_backend() username = _get_username() try: @@ -99,6 +101,8 @@ def _get_value(key): def _set_value(key, value): + import keyring + _prepare_keyring_backend() username = _get_username() keyring.set_password(key, username, value) @@ -179,6 +183,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: @@ -215,6 +222,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 faf3b014..48ababb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,7 +174,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"]