Skip to content

Modernize tooling: uv, ruff, ty, Python 3.13+ and full typing - #31

Merged
Korijn merged 3 commits into
masterfrom
claude/modernize-uv-ruff-py313-4y3adt
Aug 4, 2026
Merged

Modernize tooling: uv, ruff, ty, Python 3.13+ and full typing#31
Korijn merged 3 commits into
masterfrom
claude/modernize-uv-ruff-py313-4y3adt

Conversation

@Korijn

@Korijn Korijn commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Brings the project setup in line with fork-tongue/observ, drops support for everything below Python 3.13, and fully annotates the package.

Packaging

  • requires-python = ">=3.13", and tomli is gone in favour of stdlib tomllib.
  • Unpinned the remaining dependencies: keyring~=24.3.0>=25.6, shellingham~=1.5.4>=1.5.4.
  • flit → hatchling, and [project.optional-dependencies] dev → PEP 735 [dependency-groups] split into dev/ruff/ty so each CI job installs only what it needs.
  • __version__ now comes from importlib.metadata, so the version is declared once in pyproject.toml instead of being kept in sync by hand.
  • SPDX license = "MIT" + license-files.

Typing

Every function, parameter and module-level variable in keycmd is annotated, and the package ships a py.typed marker so consumers get the types.

Typing the config surface meant describing its shape, so [keys] and [aliases] are now TypedDicts (KeyConf, AliasConf, Conf) with NotRequired for the optional b64/format fields — the schema the README documents, expressed in code. load_conf casts at the boundary where user-authored TOML enters the program, which is where the promise is actually made; get_env still reports violations as user errors at runtime. find_file got @overloads, since its return type genuinely depends on first_only.

Enforcement, so this does not decay:

  • ruff's ANN rules require annotations (the test suite is exempt).
  • ty's off-by-default rules are on: missing-type-argument, possibly-missing-attribute, possibly-missing-import, possibly-unresolved-reference, division-by-zero. Suppressions must name a rule and must be needed (blanket-ignore-comment, unused-ignore-comment).
  • CI and pre-commit run ty check --error-on-warning, so warn-level diagnostics fail rather than scroll by.

Two bugs fell out of making the types honest

  • shell.exec passed env=None straight to os.execvpe, which requires a mapping and would have raised TypeError. Reachable via run_shell()/run_cmd() with no env on POSIX; the tests never hit it because they force the subprocess branch. It now falls back to os.environ, matching what the subprocess branch already did.
  • creds.get_env bound both the loop variable and the looked-up key data to the name key, giving one name two types. Split into key/data and src/alias_src.

Linting

Ruff's selection now matches observ's — adds isort (I), T10, T20 and ANN, keeps B. The old ignore list (E501, E731, B019, RUF012) is gone; nothing in the codebase needed it.

Added .pre-commit-config.yaml running format, lint, typecheck and tests.

CI

Restructured from four ad-hoc jobs into lint / typecheck / test / build / publish, all uv-driven with caching:

  • actions/checkout@v4@v7, upload-artifact@v4@v7, download-artifact@v4@v8; setup-python replaced by astral-sh/setup-uv. Third-party actions pinned by commit SHA.
  • Build is now uv build + uvx twine check instead of the hand-rolled sdist-install-and-git reset --hard dance.
  • The concurrency group no longer cancels tag runs mid-publish.
  • Test matrix is Python 3.13 + 3.14. It stays on Windows because the suite exercises a real OS keyring, which is what the matrix was already doing and why.

Verification

  • ruff check, ruff format --check and ty check --error-on-warning are all clean.
  • sdist and wheel build and pass twine check; py.typed is present in both. The sdist installs into a clean 3.13 venv with a working keycmd --version.
  • Tests: 16/17. test_shell.py::test_run_cmd fails only when run on Linux, because it asserts cmd.exe's exit code 1 for an unknown command while bash returns 127. Pre-existing and platform-specific, so it is green on the Windows runners.
  • Checked the new enforcement actually bites rather than trusting a green run: an unannotated probe function draws 2 ANN errors, and a probe consumer confirms the types catch real misuse (assigning find_file(..., first_only=False) to a str, a keys entry missing credential, a non-str env value), while valid load_conf()get_env() stays clean.

Notes

  • PyPI publishing is unchanged — still secrets.PYPI_PASSWORD. observ uses trusted publishing (OIDC), which is better, but switching requires configuring a publisher on PyPI first, and doing it here would break the next release. Happy to switch once that exists.
  • uv.lock is gitignored, matching observ — appropriate for a library.
  • ty checks keycmd only and the tests are exempt from ANN, so "fully typed" means the package, not the test suite.

Generated by Claude Code

claude added 3 commits August 4, 2026 17:37
Adopt the same project setup as the observ repo:

- Require Python 3.13+ and drop the `tomli` dependency in favour of
  stdlib `tomllib`. Unpin `keyring` (~=24.3 -> >=25.6) and `shellingham`.
- Switch the build backend from flit to hatchling and move dev
  dependencies from an optional-dependencies extra to PEP 735
  dependency groups (dev/ruff/ty), with uv as the package manager.
- Derive `__version__` from package metadata so the version is declared
  in one place only.
- Extend the ruff lint selection (isort, flake8-debugger, flake8-print)
  and drop the ignores that no longer apply.
- Add ty type checking and a pre-commit config running format, lint,
  typecheck and tests. Annotate `error()` as `NoReturn`, which is what
  it always was and what ty needs to see to narrow correctly.
- Reorganize CI into separate lint, typecheck, test, build and publish
  jobs driven by uv, upgrade all actions to their latest versions and
  pin third-party actions by commit SHA. Tests now run on Python 3.13
  and 3.14; the matrix stays on Windows because the suite needs an OS
  keyring that unlocks unattended.
- Document the development workflow and the 3.13+ requirement in the
  README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nTpsbs48Pcky9GVthXnWv
Annotate every function, parameter and module-level variable in
`keycmd`, and ship a `py.typed` marker so downstream consumers get the
types too.

Typing the config surface required describing its shape, so the
[keys] and [aliases] tables are now `TypedDict`s. `load_conf` casts to
that shape at the boundary where user authored TOML enters the program,
which is where the promise is actually made.

Enforcement, so this does not decay:

- ruff's `ANN` rules require annotations (the test suite is exempt).
- ty's off-by-default rules are enabled: `missing-type-argument`,
  `possibly-missing-attribute`, `possibly-missing-import`,
  `possibly-unresolved-reference` and `division-by-zero`. Suppressions
  must name a rule and must be needed (`blanket-ignore-comment`,
  `unused-ignore-comment`).
- CI and pre-commit run `ty check --error-on-warning`, so warn-level
  diagnostics fail rather than scroll by.

Two fixes fell out of making the types honest:

- `shell.exec` passed `env=None` straight to `os.execvpe`, which
  requires a mapping and would have raised `TypeError`. It now falls
  back to `os.environ`, matching what the subprocess branch already did.
- `creds.get_env` bound both the loop variable and the looked up key
  data to the name `key`, so the two had different types under one name.
  They are now `key`/`data` and `src`/`alias_src`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nTpsbs48Pcky9GVthXnWv
`uv sync --only-group ty` installs ty but not the project, so ty could
not resolve the `keyring` and `shellingham` imports and failed with
`unresolved-import`.

The pattern was copied from observ, where it is correct because that
project has no runtime dependencies. keycmd has two, so the typecheck
job needs the project installed as well. It still skips the rest of the
dev group.

The lint job keeps using `--only-group ruff`: ruff is purely syntactic
and needs nothing installed to resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nTpsbs48Pcky9GVthXnWv
@Korijn
Korijn merged commit 88146fe into master Aug 4, 2026
6 checks passed
@Korijn
Korijn deleted the claude/modernize-uv-ruff-py313-4y3adt branch August 4, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants