From d6a7c189126a7f6f1ca8a5dc723c515461ab6b28 Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 18:51:21 +0200 Subject: [PATCH 01/10] build!: require Python 3.10 and replace pyrefly with ty Move dev tools to a uv dependency group (pytest 9, pytest-cov 7, ruff 0.17, ty, pre-commit 4.6) and drop the pathlib backport dependency. Coverage is no longer part of the default pytest run; fail_under records the current 45%. Hooks run the locked ruff and ty through uv run on every OS and leave tests to just check. The Nix package no longer needs the pathlib and pytest-cov workarounds. --- .pre-commit-config.yaml | 51 ++---- flake.nix | 2 +- pkgs/kilm.nix | 16 +- pyproject.toml | 40 +++-- uv.lock | 334 +++++++++++----------------------------- 5 files changed, 125 insertions(+), 318 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe278c4..357f031 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,40 +1,23 @@ +# Hooks run the tool versions locked in uv.lock through `uv run`, so they match +# `just check` and CI on every OS. `just setup` installs them. Tests stay out of +# the hook to keep commits fast; `just check` runs them. +minimum_pre_commit_version: "4.4.0" repos: - # Ruff formatter - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.6 + - repo: local hooks: - id: ruff-format name: ruff format - description: Format Python code with ruff - - # Ruff linter - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.6 - hooks: - - id: ruff + entry: uv run --locked ruff format + language: unsupported + types_or: [python, pyi] + - id: ruff-check name: ruff check --fix - description: Lint Python code with ruff and auto-fix issues - args: [--fix] - - # Pyrefly type checker - - repo: local - hooks: - - id: pyrefly - name: pyrefly check - description: Type check Python code with pyrefly - entry: .venv/bin/pyrefly - args: [check] - language: system - types: [python] - - # Pytest tests - - repo: local - hooks: - - id: pytest - name: pytest - description: Run tests with pytest - entry: .venv/bin/pytest - language: system - types: [python] + entry: uv run --locked ruff check --fix + language: unsupported + types_or: [python, pyi] + - id: ty + name: ty check + entry: uv run --locked ty check + language: unsupported + types_or: [python, pyi] pass_filenames: false - always_run: true diff --git a/flake.nix b/flake.nix index 731ae3b..0ec9f2b 100644 --- a/flake.nix +++ b/flake.nix @@ -50,7 +50,7 @@ devShells.default = pkgs.mkShell { # Python, runtime deps, and pytest come from the package. Lint and - # type-check through `uv run` so ruff and pyrefly match uv.lock. + # type-check through `uv run` so ruff and ty match uv.lock. inputsFrom = [ kilm ]; packages = [ pkgs.uv ]; # Import the in-tree package instead of requiring an install step. diff --git a/pkgs/kilm.nix b/pkgs/kilm.nix index 6ba66df..3a00480 100644 --- a/pkgs/kilm.nix +++ b/pkgs/kilm.nix @@ -16,11 +16,7 @@ let requirementName = requirement: lib.toLower (builtins.head (builtins.match "([A-Za-z0-9._-]+).*" requirement)); - # `pathlib` is declared in pyproject but is stdlib on Python 3; the PyPI - # backport is a no-op and absent from nixpkgs. - stdlibBackports = [ "pathlib" ]; - - runtimeDependencies = lib.subtractLists stdlibBackports (map requirementName project.dependencies); + runtimeDependencies = map requirementName project.dependencies; in python3Packages.buildPythonPackage { pname = project.name; @@ -31,8 +27,6 @@ python3Packages.buildPythonPackage { build-system = [ python3Packages.hatchling ]; - pythonRemoveDeps = stdlibBackports; - dependencies = map (name: python3Packages.${name}) runtimeDependencies; # `kilm sync` and `kilm add-hook` shell out to git. Append it to the wrapper's @@ -45,14 +39,10 @@ python3Packages.buildPythonPackage { (lib.makeBinPath [ git ]) ]; - # pytest-cov is required because pyproject's pytest addopts pass --cov. nativeCheckInputs = [ git - ] - ++ (with python3Packages; [ - pytest-cov - pytestCheckHook - ]); + python3Packages.pytestCheckHook + ]; # Tests shell out to `git` and write into HOME; give them a writable one. preCheck = '' diff --git a/pyproject.toml b/pyproject.toml index 4004fb8..94ace3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,18 +17,16 @@ maintainers = [ classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "Intended Audience :: Science/Research", + "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", - "Framework :: Pytest", - "Development Status :: 4 - Beta", "Natural Language :: English", "Environment :: Console", ] @@ -40,27 +38,26 @@ keywords = [ "pcb-design", "cli", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "click>=8.0", "typer>=0.17.4", "rich>=14.0.0", "packaging>=25.0", "pyyaml>=6.0", - "pathlib>=1.0.1", "pathspec>=0.12.1", "jinja2>=3.1.6", "questionary>=1.10.0", "requests>=2.32.4", ] -[project.optional-dependencies] +[dependency-groups] dev = [ - "pytest>=7.0.0", - "pytest-cov>=4.0.0", - "pyrefly>=0.31.0", - "ruff>=0.1.0", - "pre-commit>=3.5.0", + "pre-commit>=4.4.0", + "pytest>=9.1.1", + "pytest-cov>=7.1.0", + "ruff>=0.17.0", + "ty>=0.0.86", ] [project.urls] @@ -72,26 +69,25 @@ Repository = "https://github.com/barisgit/KiLM" [project.scripts] kilm = "kicad_lib_manager.main:app" +[tool.uv] +# Oldest uv verified with this lockfile; CI pins an exact version. +required-version = ">=0.9.8" [tool.hatch.build.targets.wheel] packages = ["kicad_lib_manager"] [tool.pytest.ini_options] -testpaths = ["tests"] +testpaths = ["tests"] python_files = ["test_*.py", "*_test.py"] python_classes = ["Test*"] python_functions = ["test_*"] +# Coverage is opt-in (`just cov`) so the default run stays fast and writes no reports. addopts = [ "--strict-markers", "--strict-config", - "--cov=kicad_lib_manager", - "--cov-report=term-missing", - "--cov-report=html", - "--cov-report=xml", ] [tool.ruff] -target-version = "py39" line-length = 88 [tool.ruff.lint] @@ -118,9 +114,6 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "tests/**/*" = ["ARG", "S101"] -[tool.pyrefly] -python-version = "3.9" - [tool.coverage.run] source = ["kicad_lib_manager"] omit = [ @@ -129,12 +122,15 @@ omit = [ ] [tool.coverage.report] +# Measured 45.5% when coverage became opt-in; raise it as tests are added. +fail_under = 45 +show_missing = true exclude_lines = [ "pragma: no cover", "def __repr__", "if self.debug:", "if settings.DEBUG", - "raise AssertionError", + "raise AssertionError", "raise NotImplementedError", "if 0:", "if __name__ == .__main__.:", diff --git a/uv.lock b/uv.lock index 22d7455..ca5f033 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.9" +requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", - "python_full_version < '3.10'", + "python_full_version < '3.11'", ] [[package]] @@ -86,45 +85,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, - { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, - { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, - { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, - { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, - { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - [[package]] name = "click" version = "8.2.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ @@ -222,16 +191,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, - { url = "https://files.pythonhosted.org/packages/91/70/f73ad83b1d2fd2d5825ac58c8f551193433a7deaf9b0d00a8b69ef61cd9a/coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352", size = 217009, upload-time = "2025-08-29T15:34:57.381Z" }, - { url = "https://files.pythonhosted.org/packages/01/e8/099b55cd48922abbd4b01ddd9ffa352408614413ebfc965501e981aced6b/coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612", size = 217400, upload-time = "2025-08-29T15:34:58.985Z" }, - { url = "https://files.pythonhosted.org/packages/ee/d1/c6bac7c9e1003110a318636fef3b5c039df57ab44abcc41d43262a163c28/coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b", size = 243835, upload-time = "2025-08-29T15:35:00.541Z" }, - { url = "https://files.pythonhosted.org/packages/01/f9/82c6c061838afbd2172e773156c0aa84a901d59211b4975a4e93accf5c89/coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144", size = 245658, upload-time = "2025-08-29T15:35:02.135Z" }, - { url = "https://files.pythonhosted.org/packages/81/6a/35674445b1d38161148558a3ff51b0aa7f0b54b1def3abe3fbd34efe05bc/coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b", size = 247433, upload-time = "2025-08-29T15:35:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/98c99e7cafb288730a93535092eb433b5503d529869791681c4f2e2012a8/coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862", size = 245315, upload-time = "2025-08-29T15:35:05.629Z" }, - { url = "https://files.pythonhosted.org/packages/09/05/123e0dba812408c719c319dea05782433246f7aa7b67e60402d90e847545/coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2", size = 243385, upload-time = "2025-08-29T15:35:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/67/52/d57a42502aef05c6325f28e2e81216c2d9b489040132c18725b7a04d1448/coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78", size = 244343, upload-time = "2025-08-29T15:35:09.55Z" }, - { url = "https://files.pythonhosted.org/packages/6b/22/7f6fad7dbb37cf99b542c5e157d463bd96b797078b1ec506691bc836f476/coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c", size = 219530, upload-time = "2025-08-29T15:35:11.167Z" }, - { url = "https://files.pythonhosted.org/packages/62/30/e2fda29bfe335026027e11e6a5e57a764c9df13127b5cf42af4c3e99b937/coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf", size = 220432, upload-time = "2025-08-29T15:35:12.902Z" }, { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, ] @@ -254,31 +213,19 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] -[[package]] -name = "filelock" -version = "3.19.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, -] - [[package]] name = "filelock" version = "4.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*'", + "python_full_version < '3.11'", ] sdist = { url = "https://files.pythonhosted.org/packages/4c/58/6fd434bec86eff7c38a3168454cb132b762b2bea9b3ac094101a2f7bc32a/filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d", size = 561277, upload-time = "2026-10-09T19:57:20.34Z" } wheels = [ @@ -341,28 +288,24 @@ name = "kilm" version = "0.5.6" source = { editable = "." } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "jinja2" }, { name = "packaging" }, - { name = "pathlib" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "questionary" }, - { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "requests" }, { name = "rich" }, { name = "typer" }, ] -[package.optional-dependencies] +[package.dev-dependencies] dev = [ { name = "pre-commit" }, - { name = "pyrefly" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "ty" }, ] [package.metadata] @@ -370,46 +313,29 @@ requires-dist = [ { name = "click", specifier = ">=8.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "packaging", specifier = ">=25.0" }, - { name = "pathlib", specifier = ">=1.0.1" }, { name = "pathspec", specifier = ">=0.12.1" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.5.0" }, - { name = "pyrefly", marker = "extra == 'dev'", specifier = ">=0.31.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "questionary", specifier = ">=1.10.0" }, { name = "requests", specifier = ">=2.32.4" }, { name = "rich", specifier = ">=14.0.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, { name = "typer", specifier = ">=0.17.4" }, ] -provides-extras = ["dev"] -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "mdurl", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit", specifier = ">=4.4.0" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = ">=0.17.0" }, + { name = "ty", specifier = ">=0.0.86" }, ] [[package]] name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.10'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ @@ -472,16 +398,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, - { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, - { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, - { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] [[package]] @@ -511,15 +427,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] -[[package]] -name = "pathlib" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/aa/9b065a76b9af472437a0059f77e8f962fe350438b927cb80184c32f075eb/pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f", size = 49298, upload-time = "2014-09-03T15:41:57.18Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/f9/690a8600b93c332de3ab4a344a4ac34f00c8f104917061f779db6a918ed6/pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147", size = 14363, upload-time = "2022-05-04T13:37:20.585Z" }, -] - [[package]] name = "pathspec" version = "0.12.1" @@ -549,7 +456,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.3.0" +version = "4.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -558,9 +465,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, ] [[package]] @@ -584,59 +491,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] -[[package]] -name = "pyrefly" -version = "0.31.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/c4/4b45120c1af9698909fb5e771170566959bad43530f2f0ac434674789e14/pyrefly-0.31.1.tar.gz", hash = "sha256:79609735d2683c9fda12b14e74a0643477984216297fc8786086cc29f964901d", size = 1342496, upload-time = "2025-09-05T23:06:50.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/26/1c8699516f2d67456df729df2996a5957e6f27693db00219252e7be514dd/pyrefly-0.31.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b251a502a3d7c435361fa66449b0d3eaafd9e5b7bd16802e626ae28fa8972e54", size = 6622774, upload-time = "2025-09-05T23:06:33.592Z" }, - { url = "https://files.pythonhosted.org/packages/b4/fe/1d63007e29494894bee8e56a8e3631f5a1ecdbbf99fce233ebbc5c13e6dc/pyrefly-0.31.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3f9ca929a9a259c3c2d2d10b7639e68a6ec11f227cf0f5525a53ceb720b065ea", size = 6162225, upload-time = "2025-09-05T23:06:35.841Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/8f67f8a68b1d35c87b170d8c11619d01d7769c7f206d72954ed80c44c675/pyrefly-0.31.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68cf8e4b4abbdb0f50919caf1310e82fc7f2b09ba5152bfcedd8fda189b0cf42", size = 6410448, upload-time = "2025-09-05T23:06:38.228Z" }, - { url = "https://files.pythonhosted.org/packages/ee/e6/755de7c249a6a497542f21e61e94b3afbfc096fa480678e87e7c02410b4a/pyrefly-0.31.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f7c67586f506910248cc410b5232418635604c8cc2f4d61d6432418c6a5647", size = 7204044, upload-time = "2025-09-05T23:06:39.859Z" }, - { url = "https://files.pythonhosted.org/packages/39/be/4ca0a3e8f78357e6846fb367ad00790c74c7bc80928171ce0e34d33aabc8/pyrefly-0.31.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c0d809af388144cb148eb11c6ebc33f8e5c6adafc22a7f0da99d9b555123e42", size = 6890396, upload-time = "2025-09-05T23:06:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/01/1a/9c3381910d51d64a26726c6111ac5a1ee90e5de9e92b4827be352610c5b8/pyrefly-0.31.1-py3-none-win32.whl", hash = "sha256:8bde23e37fb68367b691b7491880e0ce95e150d879fe43e3a86e78f3cf911a23", size = 6383610, upload-time = "2025-09-05T23:06:44.159Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ab/f99cf8f7acebc0ddf0718ca4f33786e78353019903e5888de133c67863f5/pyrefly-0.31.1-py3-none-win_amd64.whl", hash = "sha256:bec49618d2574ccd200de515480c4b60740926f3a7a4b1804fdae49b72665719", size = 6800276, upload-time = "2025-09-05T23:06:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/02/53/b02648724846a3bd676410261f2d980a260a16e337fe978028429fc712d1/pyrefly-0.31.1-py3-none-win_arm64.whl", hash = "sha256:370d2b7fab9cab0de937f44638cf6666322e27b6d2c84c4b82cba0e868c6d437", size = 6421184, upload-time = "2025-09-05T23:06:48.218Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pluggy", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, -] - [[package]] name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pluggy", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ @@ -645,17 +511,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "6.3.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/4c/f883ab8f0daad69f47efdf95f55a66b51a8b939c430dadce0611508d9e99/pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2", size = 70398, upload-time = "2025-09-06T15:40:14.361Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/b4/bb7263e12aade3842b938bc5c6958cae79c5ee18992f9b9349019579da0f/pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749", size = 25115, upload-time = "2025-09-06T15:40:12.44Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -663,8 +528,7 @@ name = "python-discovery" version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "filelock", version = "4.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b0/73/54993df8fc57e906dc9b7e01d1d9b0b2d3eb0ce3190639e33dccb12853f7/python_discovery-1.6.2.tar.gz", hash = "sha256:cd1738ca1d37c86ef9d0b654dd46fcee1e41c97c6add12575e8501ced39afdb4", size = 86921, upload-time = "2026-10-10T01:24:27.912Z" } @@ -714,15 +578,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] @@ -737,37 +592,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, ] -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "certifi", marker = "python_full_version < '3.10'" }, - { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, - { name = "idna", marker = "python_full_version < '3.10'" }, - { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - [[package]] name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.10'" }, - { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, - { name = "urllib3", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ @@ -779,8 +612,7 @@ name = "rich" version = "14.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } @@ -790,28 +622,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" }, - { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" }, - { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" }, - { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" }, - { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" }, - { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" }, - { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" }, - { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" }, - { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" }, - { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" }, - { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" }, + { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" }, + { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" }, + { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" }, ] [[package]] @@ -862,13 +693,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "ty" +version = "0.0.86" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/fa/9b35728cc3c04d64f434c656836fa3c5f6e384a7813e48e63245510d5c69/ty-0.0.86.tar.gz", hash = "sha256:6edcaf52e207d653873d5f495c1a8470075b232030714c981f07f0a1075bc343", size = 7911723, upload-time = "2026-10-09T22:20:57.83Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/27/dd422283667ce10d60af57edc3ccab078084282ee304a5c22b5ed79390c6/ty-0.0.86-py3-none-linux_armv6l.whl", hash = "sha256:305c238df5dedf97f1b65e52d2c446a41d11551291e37054858c00c5627bf15b", size = 14159390, upload-time = "2026-10-09T22:20:17.583Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/838ea547c177983976103d3a54b346afc22d0ff97943ab58c7b7aff1f753/ty-0.0.86-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27a8a7cf26a1cd17d93586eb20aadbcc535fa6ad4b535813ee8fc52560c31efd", size = 13684426, upload-time = "2026-10-09T22:20:19.939Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/dfde84ffab702545f866350675b16f8df6555d3c41123c25e7a509fe857e/ty-0.0.86-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40e5fc602af41b0ea89dd1d19fe8da7e75322e6e0db95ea23340a858b409ac2b", size = 12845457, upload-time = "2026-10-09T22:20:21.979Z" }, + { url = "https://files.pythonhosted.org/packages/47/b2/7e0e7ffc8b3961f5f74893ba9b98433b922d3f1ae6621d1e3e3e20663e1d/ty-0.0.86-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4b525ce4feb60aa2dd6becae4c470daea889149ba9dc7699ffbcf41448f69a9", size = 13656310, upload-time = "2026-10-09T22:20:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/977594e024d154f0e064b48a6f7e39a0e1e55a524349dba22146f01e02db/ty-0.0.86-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a000ddcc0b99bd1ab8c8277630711061a36ae83395780fdb935bad540ebe7103", size = 13887036, upload-time = "2026-10-09T22:20:26.844Z" }, + { url = "https://files.pythonhosted.org/packages/3a/75/67e8e7bf89a6be7535768380cb20569b460b89cb09d5c82ce27411782786/ty-0.0.86-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3848f3cab206e9798e5a9f7c4ce8a80297b0a88ba9aa8a091360a298e97424", size = 14805103, upload-time = "2026-10-09T22:20:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a1/14/a7f3b88cc76156feaf5d48601bd5da96a116f846ad0abc526df03a735bcc/ty-0.0.86-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:087bad6ae8fa7248a16c01f5646c121dccaacbaf320ff544eed4e8c5c1fd7555", size = 15203931, upload-time = "2026-10-09T22:20:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/a7/23/42f6ad5d4bb7091d70a0e96d7d436d6bc9a490210a928727dde5d883ed26/ty-0.0.86-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7f2721746c9ee64626c69da9245faedc0f375024bf41458e45eb7b28fe5f63", size = 15094434, upload-time = "2026-10-09T22:20:34.269Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b8/211781d3c8de5fa88437412b7a8c0d7a632596033cff51d5004b08e94e1c/ty-0.0.86-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c18501c54874149d05e5199211146876afeaeee2af234042e56dccc9d5812a", size = 14436812, upload-time = "2026-10-09T22:20:36.795Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/518cc7ef2586384345be675d470e0087591810304b5d994d0a2194c7d68e/ty-0.0.86-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0a48967c8adba6b12035243f225a0585b0bc5f0bf940a4829876bc455ebf3d2f", size = 14671116, upload-time = "2026-10-09T22:20:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/7a/18/a820553ad3206d0cdfad5866dd64939c7e3be73e891d88c73f7fd3acb5d6/ty-0.0.86-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:561a4eae98f6ff05395e8d7be8151fdb328eeaead1ac63dc721ca891623f7924", size = 13586361, upload-time = "2026-10-09T22:20:41.444Z" }, + { url = "https://files.pythonhosted.org/packages/22/58/f1700176e6ae4d0a3d25e447734c60ca464d6e76ed2e8a32920ebd41e523/ty-0.0.86-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccad053ce880a1f8c0850d9dd1f277f3ee559a98bded3f93472268376ceccc2d", size = 13900289, upload-time = "2026-10-09T22:20:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/2f0e3c9a055f8606911a80311763a089784b66af22dd80fd3290e3b167c6/ty-0.0.86-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7c16d22857bad1da56b147bf53ef0ed2252d7677dc5e7ee51c757b2e52ab6351", size = 14235785, upload-time = "2026-10-09T22:20:46.401Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1d/72fa2347b641143b0440296144a65f424fe047302cb13af059988f1de7a4/ty-0.0.86-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d863f8f190e96878ed1900a8a6d5093f37761c0c47409e2bdb3d9370e37471e1", size = 14559840, upload-time = "2026-10-09T22:20:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/47/4a/a798be4aee4ea0bb427c22f157945d0ac41310f4d6e058b3758dfb859451/ty-0.0.86-py3-none-win32.whl", hash = "sha256:1ac3d8fe9efae04fbf1407583ed4118e0364a45b49a020d62b202846695a2ba1", size = 13133227, upload-time = "2026-10-09T22:20:51.034Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a0/c9d585fa1c5bf4ed2727f576f1542bc13e268f036451a79d057c249fc26f/ty-0.0.86-py3-none-win_amd64.whl", hash = "sha256:6fe4116fb1a7ad3c4f7804d9241dc81187717fbd9f5fed5b50e2752a20e17a67", size = 13665391, upload-time = "2026-10-09T22:20:53.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7e/d9811358bce1e54ac1b93bd44f4c45eba5e2e92672c5410248776c54319e/ty-0.0.86-py3-none-win_arm64.whl", hash = "sha256:0df36f494c5d07f68ebec3edb74fc719c4c959db7061c2018f1dff2dfe62150b", size = 13237981, upload-time = "2026-10-09T22:20:55.586Z" }, +] + [[package]] name = "typer" version = "0.17.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "rich" }, { name = "shellingham" }, { name = "typing-extensions" }, @@ -887,26 +742,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - [[package]] name = "urllib3" version = "2.8.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, @@ -918,8 +757,7 @@ version = "21.14.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "filelock", version = "4.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "platformdirs" }, From e6cf9d11df5f36297e910d77dfff4c36e7f450a0 Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 18:51:29 +0200 Subject: [PATCH 02/10] refactor: adopt Python 3.10 syntax and fix ty diagnostics ruff --fix rewrites Optional/Union annotations now that requires-python is 3.10. ty found three real typing gaps: untyped variable dicts in template make, nameless templates stored under a None key in find_all_templates, and files planned as heterogeneous dicts in create_project_from_template. --- kicad_lib_manager/commands/add_3d/command.py | 10 +-- .../commands/add_hook/command.py | 4 +- kicad_lib_manager/commands/config/command.py | 4 +- .../commands/import_zip/command.py | 14 +-- .../commands/import_zip/formats.py | 10 +-- .../commands/import_zip/importer.py | 9 +- .../commands/import_zip/kicad_cli.py | 11 ++- .../commands/import_zip/symbols.py | 7 +- .../commands/import_zip/target.py | 13 ++- kicad_lib_manager/commands/init/command.py | 8 +- .../commands/list_libraries/command.py | 4 +- kicad_lib_manager/commands/pin/command.py | 8 +- kicad_lib_manager/commands/setup/command.py | 9 +- .../commands/template/command.py | 35 ++++---- kicad_lib_manager/commands/unpin/command.py | 5 +- kicad_lib_manager/commands/update/perform.py | 4 +- .../interfaces/config_service.py | 6 +- kicad_lib_manager/interfaces/kicad_service.py | 6 +- .../interfaces/library_service.py | 12 +-- kicad_lib_manager/main.py | 4 +- kicad_lib_manager/services/config_service.py | 40 ++++----- kicad_lib_manager/services/kicad_service.py | 5 +- kicad_lib_manager/services/library_service.py | 13 ++- kicad_lib_manager/services/update_service.py | 18 ++-- kicad_lib_manager/utils/banner.py | 4 +- kicad_lib_manager/utils/env_vars.py | 7 +- kicad_lib_manager/utils/file_ops.py | 5 +- kicad_lib_manager/utils/metadata.py | 10 +-- kicad_lib_manager/utils/template.py | 86 ++++++++++--------- tests/import_zip/helpers.py | 3 +- 30 files changed, 182 insertions(+), 192 deletions(-) diff --git a/kicad_lib_manager/commands/add_3d/command.py b/kicad_lib_manager/commands/add_3d/command.py index 2f35c30..bb8242e 100644 --- a/kicad_lib_manager/commands/add_3d/command.py +++ b/kicad_lib_manager/commands/add_3d/command.py @@ -3,7 +3,7 @@ """ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer from rich.console import Console @@ -22,13 +22,13 @@ def add_3d( name: Annotated[ - Optional[str], + str | None, typer.Option( help="Name for this 3D models collection (automatic if not provided)" ), ] = None, directory: Annotated[ - Optional[Path], + Path | None, typer.Option( help="Directory containing 3D models (default: current directory)", exists=True, @@ -38,10 +38,10 @@ def add_3d( ), ] = None, description: Annotated[ - Optional[str], typer.Option(help="Description for this 3D models collection") + str | None, typer.Option(help="Description for this 3D models collection") ] = None, env_var: Annotated[ - Optional[str], + str | None, typer.Option( "--env-var", help="Custom environment variable name for this 3D model library", diff --git a/kicad_lib_manager/commands/add_hook/command.py b/kicad_lib_manager/commands/add_hook/command.py index 6a001c8..f7c263e 100644 --- a/kicad_lib_manager/commands/add_hook/command.py +++ b/kicad_lib_manager/commands/add_hook/command.py @@ -4,7 +4,7 @@ """ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer from rich.console import Console @@ -21,7 +21,7 @@ def add_hook( directory: Annotated[ - Optional[Path], + Path | None, typer.Option( help="Target git repository directory (defaults to current directory)" ), diff --git a/kicad_lib_manager/commands/config/command.py b/kicad_lib_manager/commands/config/command.py index e10191b..bd136c0 100644 --- a/kicad_lib_manager/commands/config/command.py +++ b/kicad_lib_manager/commands/config/command.py @@ -5,7 +5,7 @@ import sys from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer from rich.console import Console @@ -319,7 +319,7 @@ def list_config( def set_default( library_name: Annotated[ - Optional[str], typer.Argument(help="Name of library to set as default") + str | None, typer.Argument(help="Name of library to set as default") ] = None, library_type: Annotated[ str, diff --git a/kicad_lib_manager/commands/import_zip/command.py b/kicad_lib_manager/commands/import_zip/command.py index e1e6ed7..510cb38 100644 --- a/kicad_lib_manager/commands/import_zip/command.py +++ b/kicad_lib_manager/commands/import_zip/command.py @@ -7,7 +7,7 @@ """ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer from rich.console import Console @@ -29,7 +29,7 @@ def import_zip( ), ], library: Annotated[ - Optional[str], + str | None, typer.Option( "--library", "-l", @@ -37,7 +37,7 @@ def import_zip( ), ] = None, symbol_lib: Annotated[ - Optional[str], + str | None, typer.Option( "--symbol-lib", "-s", @@ -45,7 +45,7 @@ def import_zip( ), ] = None, footprint_lib: Annotated[ - Optional[str], + str | None, typer.Option( "--footprint-lib", "-f", @@ -53,7 +53,7 @@ def import_zip( ), ] = None, kicad_cli_path: Annotated[ - Optional[Path], + Path | None, typer.Option( "--kicad-cli", help="Path to kicad-cli or kicad.appimage for format upgrade" ), @@ -95,7 +95,7 @@ def import_zip( raise typer.Exit(1) -def _resolve_kicad_cli(path: Optional[Path]) -> Optional[Path]: +def _resolve_kicad_cli(path: Path | None) -> Path | None: """kicad-cli to upgrade with: path when given (it must exist), else a detected one.""" if path is not None and not path.exists(): console.print(f"[red]kicad-cli not found at: {path}[/red]") @@ -108,7 +108,7 @@ def _resolve_kicad_cli(path: Optional[Path]) -> Optional[Path]: return kicad_cli -def _import_one(zip_path: Path, target: ImportTarget) -> Optional[ZipResult]: +def _import_one(zip_path: Path, target: ImportTarget) -> ZipResult | None: """Import one ZIP and return what it added, or None after reporting a failure. A failed ZIP does not stop the run; the caller imports the rest. diff --git a/kicad_lib_manager/commands/import_zip/formats.py b/kicad_lib_manager/commands/import_zip/formats.py index 81e66a5..00b21ec 100644 --- a/kicad_lib_manager/commands/import_zip/formats.py +++ b/kicad_lib_manager/commands/import_zip/formats.py @@ -2,7 +2,7 @@ import re from pathlib import Path -from typing import Literal, Optional +from typing import Literal from .errors import ImportFailure @@ -15,19 +15,19 @@ _HEADER_CHARS = 1024 -def format_version(text: str) -> Optional[int]: +def format_version(text: str) -> int | None: """Return the format version in a KiCad file header, None when absent.""" m = _FORMAT_VERSION_RE.search(text[:_HEADER_CHARS]) return int(m.group(1)) if m else None -def file_format_version(path: Path) -> Optional[int]: +def file_format_version(path: Path) -> int | None: """format_version of a file, reading only its header.""" with path.open(encoding="utf-8", errors="replace") as fh: return format_version(fh.read(_HEADER_CHARS)) -def footprint_lib_version(fp_dir: Path) -> Optional[int]: +def footprint_lib_version(fp_dir: Path) -> int | None: """Newest format among fp_dir's footprints; None when it has none. Legacy (module ...) footprints carry no version and count as 0. @@ -39,7 +39,7 @@ def footprint_lib_version(fp_dir: Path) -> Optional[int]: def require_compatible( name: str, text: str, - limit: Optional[int], + limit: int | None, library: Path, kind: Literal["sym", "fp"], ) -> None: diff --git a/kicad_lib_manager/commands/import_zip/importer.py b/kicad_lib_manager/commands/import_zip/importer.py index 73221fc..a1b6da5 100644 --- a/kicad_lib_manager/commands/import_zip/importer.py +++ b/kicad_lib_manager/commands/import_zip/importer.py @@ -5,7 +5,6 @@ from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import Path -from typing import Optional from rich.console import Console @@ -57,12 +56,12 @@ class ImportTarget: model_prefix: str upgrader: Upgrader dry_run: bool - sym_format: Optional[int] = None - fp_format: Optional[int] = None + sym_format: int | None = None + fp_format: int | None = None models: dict[str, ModelRef] = field(default_factory=dict) footprints: dict[str, str] = field(default_factory=dict) symbols: set[str] = field(default_factory=set) - backup: Optional[Path] = None + backup: Path | None = None @dataclass @@ -94,7 +93,7 @@ class _Plan: model_copies: list[tuple[Path, ModelRef]] = field(default_factory=list) footprints: dict[str, tuple[Path, str]] = field(default_factory=dict) fp_refs: dict[str, str] = field(default_factory=dict) - symbols: Optional[SymbolMerge] = None + symbols: SymbolMerge | None = None lines: list[str] = field(default_factory=list) def result(self) -> ZipResult: diff --git a/kicad_lib_manager/commands/import_zip/kicad_cli.py b/kicad_lib_manager/commands/import_zip/kicad_cli.py index b2fed5f..5f584bc 100644 --- a/kicad_lib_manager/commands/import_zip/kicad_cli.py +++ b/kicad_lib_manager/commands/import_zip/kicad_cli.py @@ -8,7 +8,6 @@ import tempfile from dataclasses import dataclass from pathlib import Path -from typing import Optional from rich.console import Console @@ -41,7 +40,7 @@ def version_key(path: Path) -> list[int]: return sorted(found, key=version_key, reverse=True) -def detect_kicad_cli() -> Optional[Path]: +def detect_kicad_cli() -> Path | None: """Return kicad-cli from PATH or a known install location, else None.""" on_path = shutil.which("kicad-cli") if on_path is not None: @@ -69,11 +68,11 @@ class Upgrader: skipped for the rest of the run. kicad_cli None disables upgrades. """ - kicad_cli: Optional[Path] + kicad_cli: Path | None footprints_enabled: bool = True symbols_enabled: bool = True - def footprint(self, name: str, text: str, limit: Optional[int]) -> str: + def footprint(self, name: str, text: str, limit: int | None) -> str: """Return text upgraded if it is a legacy (module ...) footprint.""" cli = self.kicad_cli if cli is None or not self.footprints_enabled: @@ -95,7 +94,7 @@ def footprint(self, name: str, text: str, limit: Optional[int]) -> str: return text return new_text - def symbols(self, name: str, text: str, limit: Optional[int]) -> str: + def symbols(self, name: str, text: str, limit: int | None) -> str: """Return the symbol library text upgraded.""" cli = self.kicad_cli if cli is None or not self.symbols_enabled: @@ -132,7 +131,7 @@ def _run(kicad_cli: Path, label: str, *args: str) -> bool: return True -def _within_limit(text: str, limit: Optional[int], suffix: str) -> bool: +def _within_limit(text: str, limit: int | None, suffix: str) -> bool: """Whether upgraded text's format is at most limit; reports when not.""" version = format_version(text) if limit is None or version is None or version <= limit: diff --git a/kicad_lib_manager/commands/import_zip/symbols.py b/kicad_lib_manager/commands/import_zip/symbols.py index 0057cf6..690b22e 100644 --- a/kicad_lib_manager/commands/import_zip/symbols.py +++ b/kicad_lib_manager/commands/import_zip/symbols.py @@ -4,7 +4,6 @@ from collections.abc import Mapping from collections.abc import Set as AbstractSet from dataclasses import dataclass -from typing import Optional from .errors import ImportFailure @@ -20,7 +19,7 @@ def extract_symbol_blocks(text: str) -> list[str]: blocks: list[str] = [] depth = 0 in_str = False - block_start: Optional[int] = None + block_start: int | None = None i = 0 n = len(text) while i < n: @@ -55,7 +54,7 @@ def symbol_name(block: str) -> str: def fix_footprint_ref( - block: str, default_lib: str, fp_refs: Optional[Mapping[str, str]] = None + block: str, default_lib: str, fp_refs: Mapping[str, str] | None = None ) -> str: """Point the symbol's Footprint field at a footprint library. @@ -89,7 +88,7 @@ def merge_symbols( source: str, library: str, default_fp_lib: str, - fp_refs: Optional[Mapping[str, str]] = None, + fp_refs: Mapping[str, str] | None = None, known: AbstractSet[str] = frozenset(), ) -> SymbolMerge: """Append the symbols of source that library lacks. diff --git a/kicad_lib_manager/commands/import_zip/target.py b/kicad_lib_manager/commands/import_zip/target.py index 759a9b5..4c5a78b 100644 --- a/kicad_lib_manager/commands/import_zip/target.py +++ b/kicad_lib_manager/commands/import_zip/target.py @@ -1,7 +1,6 @@ """Resolving the libraries an import writes to from the KiLM configuration.""" from pathlib import Path -from typing import Optional import typer from rich.console import Console @@ -17,9 +16,9 @@ def build_target( config: Config, - library: Optional[str], - symbol_lib: Optional[str], - footprint_lib: Optional[str], + library: str | None, + symbol_lib: str | None, + footprint_lib: str | None, upgrader: Upgrader, dry_run: bool, ) -> ImportTarget: @@ -89,7 +88,7 @@ def build_target( ) -def _library_path(config: Config, library: Optional[str]) -> Path: +def _library_path(config: Config, library: str | None) -> Path: """Path of the named github library, else of the current one.""" github_libs = config.get_libraries(library_type="github") if not github_libs: @@ -118,9 +117,7 @@ def _library_path(config: Config, library: Optional[str]) -> Path: return lib_path -def _pick_lib( - candidates: list[Path], name: Optional[str], kind: str, option: str -) -> Path: +def _pick_lib(candidates: list[Path], name: str | None, kind: str, option: str) -> Path: """Return the library called name, or the only candidate when name is None.""" available = ", ".join(c.stem for c in candidates) if name is not None: diff --git a/kicad_lib_manager/commands/init/command.py b/kicad_lib_manager/commands/init/command.py index e52cdcd..1e93fc5 100644 --- a/kicad_lib_manager/commands/init/command.py +++ b/kicad_lib_manager/commands/init/command.py @@ -4,7 +4,7 @@ """ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer from rich.console import Console @@ -19,7 +19,7 @@ def init( name: Annotated[ - Optional[str], + str | None, typer.Option( "--name", help="Name for this library collection (automatic if not provided)", @@ -33,14 +33,14 @@ def init( ), ] = True, description: Annotated[ - Optional[str], + str | None, typer.Option( "--description", help="Description for this library collection", ), ] = None, env_var: Annotated[ - Optional[str], + str | None, typer.Option( "--env-var", help="Custom environment variable name for this library", diff --git a/kicad_lib_manager/commands/list_libraries/command.py b/kicad_lib_manager/commands/list_libraries/command.py index 334c181..4421337 100644 --- a/kicad_lib_manager/commands/list_libraries/command.py +++ b/kicad_lib_manager/commands/list_libraries/command.py @@ -3,7 +3,7 @@ """ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer from rich.console import Console @@ -17,7 +17,7 @@ def list_cmd( kicad_lib_dir: Annotated[ - Optional[str], + str | None, typer.Option( "--kicad-lib-dir", help="KiCad library directory (uses KICAD_USER_LIB env var if not specified)", diff --git a/kicad_lib_manager/commands/pin/command.py b/kicad_lib_manager/commands/pin/command.py index 538a495..8ffea1c 100644 --- a/kicad_lib_manager/commands/pin/command.py +++ b/kicad_lib_manager/commands/pin/command.py @@ -3,7 +3,7 @@ """ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer from rich.console import Console @@ -23,7 +23,7 @@ def pin( kicad_lib_dir: Annotated[ - Optional[str], + str | None, typer.Option( "--kicad-lib-dir", envvar="KICAD_USER_LIB", @@ -31,7 +31,7 @@ def pin( ), ] = None, symbols: Annotated[ - Optional[list[str]], + list[str] | None, typer.Option( "--symbols", "-s", @@ -39,7 +39,7 @@ def pin( ), ] = None, footprints: Annotated[ - Optional[list[str]], + list[str] | None, typer.Option( "--footprints", "-f", diff --git a/kicad_lib_manager/commands/setup/command.py b/kicad_lib_manager/commands/setup/command.py index fa91348..707de09 100644 --- a/kicad_lib_manager/commands/setup/command.py +++ b/kicad_lib_manager/commands/setup/command.py @@ -4,7 +4,6 @@ import re from pathlib import Path -from typing import Optional import typer from rich.console import Console @@ -78,24 +77,24 @@ def fix_invalid_uris( def setup( - kicad_lib_dir: Optional[str] = typer.Option( + kicad_lib_dir: str | None = typer.Option( None, "--kicad-lib-dir", envvar="KICAD_USER_LIB", help="KiCad library directory (uses KICAD_USER_LIB env var if not specified)", ), - kicad_3d_dir: Optional[str] = typer.Option( + kicad_3d_dir: str | None = typer.Option( None, "--kicad-3d-dir", envvar="KICAD_3D_LIB", help="KiCad 3D models directory (uses KICAD_3D_LIB env var if not specified)", ), - threed_lib_dirs: Optional[str] = typer.Option( + threed_lib_dirs: str | None = typer.Option( None, "--threed-lib-dirs", help="Names of 3D model libraries to use (comma-separated, uses all if not specified)", ), - symbol_lib_dirs: Optional[str] = typer.Option( + symbol_lib_dirs: str | None = typer.Option( None, "--symbol-lib-dirs", help="Names of symbol libraries to use (comma-separated, uses current if not specified)", diff --git a/kicad_lib_manager/commands/template/command.py b/kicad_lib_manager/commands/template/command.py index 45015fe..660aac2 100644 --- a/kicad_lib_manager/commands/template/command.py +++ b/kicad_lib_manager/commands/template/command.py @@ -9,7 +9,7 @@ import sys import traceback from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import jinja2 import pathspec @@ -27,6 +27,7 @@ TEMPLATE_CONTENT_DIR, TEMPLATE_METADATA, TEMPLATES_DIR, + TemplateVariable, create_project_from_template, create_template_metadata, create_template_structure, @@ -51,19 +52,19 @@ def main_callback() -> None: def create( name: Annotated[ - Optional[str], typer.Argument(help="Name of the project to create") + str | None, typer.Argument(help="Name of the project to create") ] = None, directory: Annotated[ - Optional[str], typer.Argument(help="Directory to create project in") + str | None, typer.Argument(help="Directory to create project in") ] = None, template: Annotated[ - Optional[str], typer.Option(help="Name of the template to use") + str | None, typer.Option(help="Name of the template to use") ] = None, library: Annotated[ - Optional[str], typer.Option(help="Name of the library containing the template") + str | None, typer.Option(help="Name of the library containing the template") ] = None, set_var: Annotated[ - Optional[list[str]], + list[str] | None, typer.Option("--set-var", help="Set template variable in key=value format"), ] = None, dry_run: Annotated[ @@ -488,33 +489,33 @@ def create( def make( name: Annotated[ - Optional[str], typer.Argument(help="Name of the template to create") + str | None, typer.Argument(help="Name of the template to create") ] = None, source_directory: Annotated[ - Optional[Path], typer.Argument(help="Source project directory") + Path | None, typer.Argument(help="Source project directory") ] = None, description: Annotated[ - Optional[str], typer.Option(help="Template description") + str | None, typer.Option(help="Template description") ] = None, use_case: Annotated[ - Optional[str], typer.Option("--use-case", help="Template use case description") + str | None, typer.Option("--use-case", help="Template use case description") ] = None, output_directory: Annotated[ - Optional[Path], + Path | None, typer.Option( "--output-directory", help="Directory where the template will be created" ), ] = None, exclude: Annotated[ - Optional[list[str]], + list[str] | None, typer.Option(help="Additional patterns to exclude (gitignore format)"), ] = None, variable: Annotated[ - Optional[list[str]], + list[str] | None, typer.Option(help="Define a template variable in name=value format"), ] = None, extends: Annotated[ - Optional[str], typer.Option(help="Parent template that this template extends") + str | None, typer.Option(help="Parent template that this template extends") ] = None, non_interactive: Annotated[ bool, @@ -692,7 +693,7 @@ def make( console.print(f"Extends: {extends}") # Parse variables from command line - variable_dict = {} + variable_dict: dict[str, TemplateVariable] = {} for var in variable or []: if "=" in var: key, value = var.split("=", 1) @@ -707,7 +708,7 @@ def make( console.print(f" {key}: {value['default']} - {value['description']}") # If interactive mode is enabled, scan for potential variables - detected_variables = {} + detected_variables: dict[str, TemplateVariable] = {} if interactive: potential_vars = find_potential_variables(source_directory) if potential_vars: @@ -907,7 +908,7 @@ def make( def list_templates( library: Annotated[ - Optional[str], typer.Option(help="Filter templates by library name") + str | None, typer.Option(help="Filter templates by library name") ] = None, verbose: Annotated[ bool, diff --git a/kicad_lib_manager/commands/unpin/command.py b/kicad_lib_manager/commands/unpin/command.py index 040347e..5c83038 100644 --- a/kicad_lib_manager/commands/unpin/command.py +++ b/kicad_lib_manager/commands/unpin/command.py @@ -3,7 +3,6 @@ """ import json -from typing import Optional import typer from rich.console import Console @@ -16,13 +15,13 @@ def unpin( - symbols: Optional[list[str]] = typer.Option( + symbols: list[str] | None = typer.Option( None, "--symbols", "-s", help="Symbol libraries to unpin (can be specified multiple times)", ), - footprints: Optional[list[str]] = typer.Option( + footprints: list[str] | None = typer.Option( None, "--footprints", "-f", diff --git a/kicad_lib_manager/commands/update/perform.py b/kicad_lib_manager/commands/update/perform.py index 8c19f02..6a47054 100644 --- a/kicad_lib_manager/commands/update/perform.py +++ b/kicad_lib_manager/commands/update/perform.py @@ -1,7 +1,7 @@ """Update perform command implementation.""" import importlib.metadata -from typing import Annotated, Optional +from typing import Annotated import typer from rich.console import Console @@ -14,7 +14,7 @@ def perform_update_command( target_version: Annotated[ - Optional[str], + str | None, typer.Option( "--target-version", "-t", diff --git a/kicad_lib_manager/interfaces/config_service.py b/kicad_lib_manager/interfaces/config_service.py index 6258620..94ed315 100644 --- a/kicad_lib_manager/interfaces/config_service.py +++ b/kicad_lib_manager/interfaces/config_service.py @@ -4,7 +4,7 @@ from abc import abstractmethod from pathlib import Path -from typing import Optional, Protocol +from typing import Protocol class ConfigServiceProtocol(Protocol): @@ -35,11 +35,11 @@ def get_libraries(self) -> list[dict]: """Get all configured libraries.""" @abstractmethod - def get_library_by_name(self, name: str) -> Optional[dict]: + def get_library_by_name(self, name: str) -> dict | None: """Get a specific library by name.""" @abstractmethod - def get_current_library(self) -> Optional[str]: + def get_current_library(self) -> str | None: """Get the current active library path.""" @abstractmethod diff --git a/kicad_lib_manager/interfaces/kicad_service.py b/kicad_lib_manager/interfaces/kicad_service.py index 556944e..179dcd3 100644 --- a/kicad_lib_manager/interfaces/kicad_service.py +++ b/kicad_lib_manager/interfaces/kicad_service.py @@ -4,7 +4,7 @@ from abc import abstractmethod from pathlib import Path -from typing import Optional, Protocol +from typing import Protocol class KiCadServiceProtocol(Protocol): @@ -43,8 +43,8 @@ def get_configured_libraries( def add_libraries_to_kicad( self, config_dir: Path, - symbol_libs: Optional[list[dict]] = None, - footprint_libs: Optional[list[dict]] = None, + symbol_libs: list[dict] | None = None, + footprint_libs: list[dict] | None = None, backup: bool = True, max_backups: int = 5, ) -> bool: diff --git a/kicad_lib_manager/interfaces/library_service.py b/kicad_lib_manager/interfaces/library_service.py index ff04f07..06b4cc1 100644 --- a/kicad_lib_manager/interfaces/library_service.py +++ b/kicad_lib_manager/interfaces/library_service.py @@ -4,7 +4,7 @@ from abc import abstractmethod from pathlib import Path -from typing import Optional, Protocol, Union +from typing import Protocol class LibraryServiceProtocol(Protocol): @@ -24,19 +24,19 @@ def list_libraries(self, directory: Path) -> tuple[list[str], list[str]]: def initialize_library( self, directory: Path, - name: Optional[str] = None, - description: Optional[str] = None, - env_var: Optional[str] = None, + name: str | None = None, + description: str | None = None, + env_var: str | None = None, force: bool = False, no_env_var: bool = False, - ) -> dict[str, Union[str, bool, dict[str, bool]]]: + ) -> dict[str, str | bool | dict[str, bool]]: """Initialize a library in the given directory.""" ... @abstractmethod def get_library_metadata( self, directory: Path - ) -> Optional[dict[str, Union[str, bool, dict[str, bool]]]]: + ) -> dict[str, str | bool | dict[str, bool]] | None: """Get metadata for a library directory.""" ... diff --git a/kicad_lib_manager/main.py b/kicad_lib_manager/main.py index 465877d..574faf4 100644 --- a/kicad_lib_manager/main.py +++ b/kicad_lib_manager/main.py @@ -4,7 +4,7 @@ """ import importlib.metadata -from typing import Annotated, Optional +from typing import Annotated import typer from rich.align import Align @@ -109,7 +109,7 @@ def version_callback(value: bool) -> None: @app.callback() def main( _version: Annotated[ - Optional[bool], + bool | None, typer.Option( "--version", "-V", diff --git a/kicad_lib_manager/services/config_service.py b/kicad_lib_manager/services/config_service.py index 0b829e0..02586b3 100644 --- a/kicad_lib_manager/services/config_service.py +++ b/kicad_lib_manager/services/config_service.py @@ -4,7 +4,7 @@ import time from pathlib import Path -from typing import Optional, TypedDict, Union, cast +from typing import TypedDict, cast import click import yaml @@ -25,7 +25,7 @@ class LibraryDict(TypedDict): type: str -ConfigValue = Union[str, int, list[LibraryDict]] +ConfigValue = str | int | list[LibraryDict] DEFAULT_CONFIG: dict[str, ConfigValue] = { "max_backups": DEFAULT_MAX_BACKUPS, @@ -67,9 +67,7 @@ def _get_config_file(self) -> Path: config_dir.mkdir(parents=True, exist_ok=True) return config_dir / CONFIG_FILE_NAME - def get( - self, key: str, default: Optional[ConfigValue] = None - ) -> Optional[ConfigValue]: + def get(self, key: str, default: ConfigValue | None = None) -> ConfigValue | None: """Get a configuration value""" return self._config.get(key, default) @@ -108,7 +106,7 @@ def add_library(self, name: str, path: str, library_type: str = "github") -> Non self._config["libraries"] = libraries self.save() - def remove_library(self, name: str, library_type: Optional[str] = None) -> bool: + def remove_library(self, name: str, library_type: str | None = None) -> bool: """ Remove a library from the configuration @@ -138,7 +136,7 @@ def remove_library(self, name: str, library_type: Optional[str] = None) -> bool: return removed - def get_libraries(self, library_type: Optional[str] = None) -> list[LibraryDict]: + def get_libraries(self, library_type: str | None = None) -> list[LibraryDict]: """ Get libraries from configuration @@ -155,9 +153,7 @@ def get_libraries(self, library_type: Optional[str] = None) -> list[LibraryDict] else: return libraries - def get_library_path( - self, name: str, library_type: str = "github" - ) -> Optional[str]: + def get_library_path(self, name: str, library_type: str = "github") -> str | None: """ Get the path for a specific library @@ -176,7 +172,7 @@ def get_library_path( return None - def get_current_library_paths(self) -> tuple[Optional[str], Optional[str]]: + def get_current_library_paths(self) -> tuple[str | None, str | None]: """ Get paths for the current active libraries @@ -188,7 +184,7 @@ def get_current_library_paths(self) -> tuple[Optional[str], Optional[str]]: cloud_libraries = self.get_libraries("cloud") # Get the current explicitly set library path - current_lib_path = cast("Optional[str]", self._config.get("current_library")) + current_lib_path = cast("str | None", self._config.get("current_library")) # For GitHub libraries: first check if current library is a GitHub library, # otherwise use the first available GitHub library @@ -232,7 +228,7 @@ def set_current_library(self, path: str) -> None: self._config["current_library"] = str(path) self.save() - def get_current_library(self) -> Optional[str]: + def get_current_library(self) -> str | None: """ Get the current active library path @@ -242,7 +238,7 @@ def get_current_library(self) -> Optional[str]: value = self._config.get("current_library") return value if isinstance(value, str) else None - def get_symbol_library_path(self) -> Optional[str]: + def get_symbol_library_path(self) -> str | None: """ Get the path for the current symbol library @@ -252,7 +248,7 @@ def get_symbol_library_path(self) -> Optional[str]: github_path, _ = self.get_current_library_paths() return github_path - def get_3d_library_path(self) -> Optional[str]: + def get_3d_library_path(self) -> str | None: """ Get the path for the current 3D models library @@ -365,7 +361,7 @@ def mark_update_check_performed(self) -> None: last_check_file = cache_dir / "last_update_check" last_check_file.write_text(str(time.time())) - def get_update_preferences(self) -> dict[str, Union[bool, str]]: + def get_update_preferences(self) -> dict[str, bool | str]: """ Get update-related preferences. @@ -378,7 +374,7 @@ def get_update_preferences(self) -> dict[str, Union[bool, str]]: "auto_update": bool(self.get("auto_update", False)), } - def set_update_preference(self, key: str, value: Union[bool, str]) -> None: + def set_update_preference(self, key: str, value: bool | str) -> None: """ Set an update preference with strict type validation and coercion. @@ -411,7 +407,7 @@ def set_update_preference(self, key: str, value: Union[bool, str]) -> None: self.set(key, coerced_value) self.save() - def _coerce_to_bool(self, value: Union[bool, str]) -> bool: + def _coerce_to_bool(self, value: bool | str) -> bool: """ Coerce a value to boolean with strict validation. @@ -452,8 +448,8 @@ def _make_library_dict(name: str, path: str, type_: str) -> LibraryDict: def _validate_library_entry( - lib: Union[dict[str, str], LibraryDict], -) -> Optional[LibraryDict]: + lib: dict[str, str] | LibraryDict, +) -> LibraryDict | None: """Validate and normalize a library entry.""" if isinstance(lib, dict) and all(key in lib for key in ["name", "path", "type"]): return _make_library_dict( @@ -496,7 +492,7 @@ def get_libraries(self) -> list[LibraryDict]: """Get all configured libraries.""" return self._config.get_libraries() - def get_library_by_name(self, name: str) -> Optional[LibraryDict]: + def get_library_by_name(self, name: str) -> LibraryDict | None: """Get a specific library by name.""" libraries = self.get_libraries() for lib in libraries: @@ -504,7 +500,7 @@ def get_library_by_name(self, name: str) -> Optional[LibraryDict]: return lib return None - def get_current_library(self) -> Optional[str]: + def get_current_library(self) -> str | None: """Get the current active library path.""" return self._config.get_current_library() diff --git a/kicad_lib_manager/services/kicad_service.py b/kicad_lib_manager/services/kicad_service.py index 4295491..0d0d1c4 100644 --- a/kicad_lib_manager/services/kicad_service.py +++ b/kicad_lib_manager/services/kicad_service.py @@ -4,7 +4,6 @@ import json from pathlib import Path -from typing import Optional from ..utils.file_ops import list_configured_libraries from .library_service import LibraryService @@ -58,8 +57,8 @@ def get_configured_libraries( def add_libraries_to_kicad( self, config_dir: Path, - symbol_libs: Optional[list[dict]] = None, - footprint_libs: Optional[list[dict]] = None, + symbol_libs: list[dict] | None = None, + footprint_libs: list[dict] | None = None, backup: bool = True, max_backups: int = 5, ) -> bool: diff --git a/kicad_lib_manager/services/library_service.py b/kicad_lib_manager/services/library_service.py index b74ec32..aac6da7 100644 --- a/kicad_lib_manager/services/library_service.py +++ b/kicad_lib_manager/services/library_service.py @@ -7,7 +7,6 @@ import platform import re from pathlib import Path -from typing import Optional import yaml @@ -35,9 +34,9 @@ def list_libraries(self, directory: Path) -> tuple[list[str], list[str]]: def initialize_library( self, directory: Path, - name: Optional[str] = None, - description: Optional[str] = None, - env_var: Optional[str] = None, + name: str | None = None, + description: str | None = None, + env_var: str | None = None, force: bool = False, no_env_var: bool = False, ) -> dict: @@ -92,7 +91,7 @@ def initialize_library( return metadata - def get_library_metadata(self, directory: Path) -> Optional[dict]: + def get_library_metadata(self, directory: Path) -> dict | None: """Get metadata for a library directory.""" return read_github_metadata(directory) @@ -148,8 +147,8 @@ def _create_library_structure(self, directory: Path) -> tuple[list[str], list[st def add_libraries( kicad_lib_dir: str, kicad_config: Path, - kicad_3d_dir: Optional[str] = None, - additional_3d_dirs: Optional[dict[str, str]] = None, + kicad_3d_dir: str | None = None, + additional_3d_dirs: dict[str, str] | None = None, dry_run: bool = False, ) -> tuple[set[str], bool]: """ diff --git a/kicad_lib_manager/services/update_service.py b/kicad_lib_manager/services/update_service.py index e21bd98..6f08ff7 100644 --- a/kicad_lib_manager/services/update_service.py +++ b/kicad_lib_manager/services/update_service.py @@ -9,7 +9,7 @@ import sys import time from pathlib import Path, PurePath -from typing import Any, Optional +from typing import Any import requests from packaging.version import InvalidVersion, Version @@ -20,8 +20,8 @@ class InstallationDetector: def __init__( self, - executable_path: Optional[PurePath] = None, - platform_name: Optional[str] = None, + executable_path: PurePath | None = None, + platform_name: str | None = None, ): self.executable_path = executable_path or Path(sys.executable) self.platform_name = platform_name or os.name @@ -144,7 +144,7 @@ def __init__(self, package_name: str, version: str = "unknown"): self.cache_file = Path.home() / ".cache" / "kilm" / "version_check.json" self.user_agent = f"KiLM/{version} (+https://github.com/barisgit/KiLM)" - def check_latest_version(self) -> Optional[str]: + def check_latest_version(self) -> str | None: """ Check latest version from PyPI with caching and rate limiting. Returns None if check fails or is rate limited. @@ -183,7 +183,7 @@ def check_latest_version(self) -> Optional[str]: return None - def _load_cache(self) -> Optional[dict]: + def _load_cache(self) -> dict | None: """Load cached version data.""" if self.cache_file.exists(): try: @@ -251,7 +251,7 @@ def __init__(self, current_version: str): self.current_version = current_version self.installation_method = detect_installation_method() - def check_latest_version(self) -> Optional[str]: + def check_latest_version(self) -> str | None: """Check for latest version available on PyPI.""" return self.version_checker.check_latest_version() @@ -431,7 +431,7 @@ def can_auto_update(self) -> bool: def perform_update( self, - target_version: Optional[str] = None, + target_version: str | None = None, force: bool = False, dry_run: bool = False, ) -> tuple[bool, str]: @@ -458,9 +458,7 @@ def perform_update( # Perform the update return self.manager.perform_update() - def _show_dry_run_info( - self, target_version: Optional[str] = None - ) -> tuple[bool, str]: + def _show_dry_run_info(self, target_version: str | None = None) -> tuple[bool, str]: """Show what would happen during an update.""" from rich.console import Console from rich.panel import Panel diff --git a/kicad_lib_manager/utils/banner.py b/kicad_lib_manager/utils/banner.py index 82f1f41..2007bae 100644 --- a/kicad_lib_manager/utils/banner.py +++ b/kicad_lib_manager/utils/banner.py @@ -49,7 +49,9 @@ def show_banner( ] # Print each line with different colors for front and back characters - for line, front_color, back_color in zip(banner_lines, front_colors, back_colors): + for line, front_color, back_color in zip( + banner_lines, front_colors, back_colors, strict=True + ): # Split the line into front characters (KILM) and back characters (box drawing) # The KILM text is represented by the block characters ██╗, ██║, etc. # The back characters are the decorative box drawing elements diff --git a/kicad_lib_manager/utils/env_vars.py b/kicad_lib_manager/utils/env_vars.py index a68620e..9832de9 100644 --- a/kicad_lib_manager/utils/env_vars.py +++ b/kicad_lib_manager/utils/env_vars.py @@ -8,7 +8,6 @@ import shutil import subprocess from pathlib import Path -from typing import Optional # Import Config here, but only use it when needed to avoid circular imports try: @@ -31,7 +30,7 @@ CLOUD_METADATA_FILE = ".kilm_metadata" -def find_environment_variables(var_name: str) -> Optional[str]: +def find_environment_variables(var_name: str) -> str | None: """ Find environment variables from various sources in the following order: 1. Config file @@ -230,8 +229,8 @@ def update_kicad_env_vars( def update_pinned_libraries( kicad_config: Path, - symbol_libs: Optional[list[str]] = None, - footprint_libs: Optional[list[str]] = None, + symbol_libs: list[str] | None = None, + footprint_libs: list[str] | None = None, dry_run: bool = False, max_backups: int = 5, ) -> bool: diff --git a/kicad_lib_manager/utils/file_ops.py b/kicad_lib_manager/utils/file_ops.py index f5a4157..8e312cc 100644 --- a/kicad_lib_manager/utils/file_ops.py +++ b/kicad_lib_manager/utils/file_ops.py @@ -7,12 +7,9 @@ import shutil import uuid from pathlib import Path -from typing import Optional -def read_file_with_encoding( - file_path: Path, encodings: Optional[list[str]] = None -) -> str: +def read_file_with_encoding(file_path: Path, encodings: list[str] | None = None) -> str: """ Read a file trying multiple encodings until successful diff --git a/kicad_lib_manager/utils/metadata.py b/kicad_lib_manager/utils/metadata.py index 4c9b401..07001c7 100644 --- a/kicad_lib_manager/utils/metadata.py +++ b/kicad_lib_manager/utils/metadata.py @@ -5,7 +5,7 @@ import json import re from pathlib import Path -from typing import Any, Optional, Union +from typing import Any import yaml @@ -14,7 +14,7 @@ def read_github_metadata( directory: Path, -) -> Optional[dict[str, Any]]: +) -> dict[str, Any] | None: """ Read metadata from a GitHub library directory. @@ -63,7 +63,7 @@ def write_github_metadata(directory: Path, metadata: dict[str, Any]) -> bool: return False -def read_cloud_metadata(directory: Path) -> Optional[dict[str, Union[str, int, None]]]: +def read_cloud_metadata(directory: Path) -> dict[str, str | int | None] | None: """ Read metadata from a cloud 3D model directory. @@ -91,7 +91,7 @@ def read_cloud_metadata(directory: Path) -> Optional[dict[str, Union[str, int, N def write_cloud_metadata( - directory: Path, metadata: dict[str, Union[str, int, None]] + directory: Path, metadata: dict[str, str | int | None] ) -> bool: """ Write metadata to a cloud 3D model directory. @@ -182,7 +182,7 @@ def get_default_github_metadata( } -def get_default_cloud_metadata(directory: Path) -> dict[str, Union[str, int, None]]: +def get_default_cloud_metadata(directory: Path) -> dict[str, str | int | None]: """ Generate default metadata for a cloud 3D model directory. diff --git a/kicad_lib_manager/utils/template.py b/kicad_lib_manager/utils/template.py index 4fcb261..2267a44 100644 --- a/kicad_lib_manager/utils/template.py +++ b/kicad_lib_manager/utils/template.py @@ -11,7 +11,7 @@ import traceback from collections.abc import Mapping from pathlib import Path -from typing import Any, Optional, TypedDict, Union, cast +from typing import Any, NamedTuple, TypedDict, cast import click import jinja2 @@ -96,6 +96,14 @@ class TemplateVariable(TypedDict): default: str +class _PlannedFile(NamedTuple): + """A template file and the project path it renders to.""" + + source: Path + target: Path + is_binary: bool + + class TemplateDependencies(TypedDict): """Template dependencies definition.""" @@ -117,7 +125,7 @@ class TemplateMetadata(TypedDict, total=False): library_path: str -def get_gitignore_spec(directory: Path) -> Optional[pathspec.PathSpec]: +def get_gitignore_spec(directory: Path) -> pathspec.PathSpec | None: """ Get a PathSpec object representing the gitignore patterns in the given directory. @@ -209,7 +217,7 @@ def list_templates_in_directory(directory: Path) -> list[TemplateMetadata]: def find_potential_variables( - directory: Path, patterns: Optional[list[str]] = None + directory: Path, patterns: list[str] | None = None ) -> dict[str, list[str]]: """ Scan files in a directory for potential template variables. @@ -276,11 +284,11 @@ def find_potential_variables( def create_template_metadata( name: str, - description: Optional[str] = None, - use_case: Optional[str] = None, - variables: Optional[dict[str, TemplateVariable]] = None, - extends: Optional[str] = None, - dependencies: Optional[list[str]] = None, + description: str | None = None, + use_case: str | None = None, + variables: dict[str, TemplateVariable] | None = None, + extends: str | None = None, + dependencies: list[str] | None = None, ) -> TemplateMetadata: """ Create template metadata dictionary. @@ -416,8 +424,8 @@ def create_template_structure( source_directory: Path, template_directory: Path, metadata: TemplateMetadata, - gitignore_spec: Optional[pathspec.PathSpec] = None, - additional_excludes: Optional[list[str]] = None, + gitignore_spec: pathspec.PathSpec | None = None, + additional_excludes: list[str] | None = None, ) -> None: """ Create template structure from source directory. @@ -477,9 +485,9 @@ def create_template_structure( MAX_PATH_LENGTH = 512 # Track main KiCad files - main_project_file: Optional[str] = None - main_schematic_file: Optional[str] = None - main_pcb_file: Optional[str] = None + main_project_file: str | None = None + main_schematic_file: str | None = None + main_pcb_file: str | None = None # First scan to find main KiCad files click.echo("Scanning for main KiCad files...") @@ -850,7 +858,7 @@ def sheet_replacer(match): # Add new functions for rendering templates and creating projects from templates def render_template_string( - template_str: str, variables: Mapping[str, Union[str, int, bool]] + template_str: str, variables: Mapping[str, str | int | bool] ) -> str: """ Render a template string using Jinja2 or custom Windows-compatible templating. @@ -878,7 +886,7 @@ def render_template_string( def render_filename_custom( - filename: str, variables: Mapping[str, Union[str, int, bool]] + filename: str, variables: Mapping[str, str | int | bool] ) -> str: """ Render a filename using a custom Windows-compatible templating system. @@ -973,9 +981,7 @@ def replacer(match): return filename -def render_filename( - filename: str, variables: Mapping[str, Union[str, int, bool]] -) -> str: +def render_filename(filename: str, variables: Mapping[str, str | int | bool]) -> str: """ Render a filename using either Jinja2 or custom Windows-compatible templating. @@ -1015,7 +1021,7 @@ def find_all_templates(config: Any) -> dict[str, TemplateMetadata]: Returns: Dictionary mapping template names to template metadata """ - all_templates = {} + all_templates: dict[str, TemplateMetadata] = {} all_libraries = config.get_libraries() # type: ignore[attr-defined] for library in all_libraries: @@ -1026,7 +1032,10 @@ def find_all_templates(config: Any) -> dict[str, TemplateMetadata]: templates = list_templates_in_directory(Path(library_path)) for template in templates: name = template.get("name") - if name and name not in all_templates: + if not name: + # A template without a name cannot be selected, so it is not listed. + continue + if name not in all_templates: # Add the template all_templates[name] = template @@ -1034,14 +1043,11 @@ def find_all_templates(config: Any) -> dict[str, TemplateMetadata]: all_templates[name]["source_library"] = library.get("name", "unknown") all_templates[name]["library_path"] = library_path else: - # Store template - if name in all_templates: - # Use click.echo for warnings/errors - click.echo( - f"Warning: Duplicate template name '{name}' found.", err=True - ) - click.echo(f" Existing: {all_templates[name]['path']}", err=True) - click.echo(f" New: {template['path']}", err=True) + click.echo( + f"Warning: Duplicate template name '{name}' found.", err=True + ) + click.echo(f" Existing: {all_templates[name]['path']}", err=True) + click.echo(f" New: {template['path']}", err=True) all_templates[name] = template return all_templates @@ -1050,7 +1056,7 @@ def find_all_templates(config: Any) -> dict[str, TemplateMetadata]: def render_template_file( source_file: Path, target_file: Path, - variables: Mapping[str, Union[str, int, bool]], + variables: Mapping[str, str | int | bool], is_binary: bool = False, ) -> bool: """ @@ -1132,8 +1138,8 @@ def render_template_file( def create_project_from_template( template_dir: Path, project_dir: Path, - variables: Mapping[str, Union[str, int, bool]], - metadata: Optional[dict[str, Any]] = None, + variables: Mapping[str, str | int | bool], + metadata: dict[str, Any] | None = None, dry_run: bool = False, skip_hooks: bool = False, ) -> bool: @@ -1199,7 +1205,7 @@ def create_project_from_template( click.echo("Warning: Template inheritance not fully implemented yet", err=True) # Collect files to create - files_to_create = [] + files_to_create: list[_PlannedFile] = [] # Walk through the template directory for root, _dirs, files in os.walk(template_content_dir): @@ -1250,7 +1256,9 @@ def create_project_from_template( # Add to the list of files to create files_to_create.append( - {"source": source_file, "target": target_file, "is_binary": is_binary} + _PlannedFile( + source=source_file, target=target_file, is_binary=is_binary + ) ) # In dry run mode, just show what would be created @@ -1258,20 +1266,20 @@ def create_project_from_template( click.echo() # Add spacing click.echo("The following files would be created:") for file_info in files_to_create: - click.echo(f" {file_info['target']}") + click.echo(f" {file_info.target}") return True # Actually create the files for file_info in files_to_create: success = render_template_file( - source_file=file_info["source"], - target_file=file_info["target"], + source_file=file_info.source, + target_file=file_info.target, variables=variables, - is_binary=file_info["is_binary"], + is_binary=file_info.is_binary, ) if not success: - click.echo(f"Warning: Failed to create {file_info['target']}", err=True) + click.echo(f"Warning: Failed to create {file_info.target}", err=True) # Run post-creation hook if present and not skipped if not skip_hooks: @@ -1289,7 +1297,7 @@ def create_project_from_template( def run_post_create_hook( hook_script: Path, project_dir: Path, - variables: Mapping[str, Union[str, int, bool]], + variables: Mapping[str, str | int | bool], template_metadata: dict[str, Any], ) -> None: """ diff --git a/tests/import_zip/helpers.py b/tests/import_zip/helpers.py index 5de4a8d..2452d49 100644 --- a/tests/import_zip/helpers.py +++ b/tests/import_zip/helpers.py @@ -2,7 +2,6 @@ import zipfile from pathlib import Path -from typing import Union from unittest.mock import patch from click.testing import Result @@ -30,7 +29,7 @@ """ -def zip_with(tmp_path: Path, name: str, files: dict[str, Union[str, bytes]]) -> Path: +def zip_with(tmp_path: Path, name: str, files: dict[str, str | bytes]) -> Path: """Write tmp_path/.zip holding files (member name -> content).""" zip_path = tmp_path / f"{name}.zip" with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: From a32926ffcc488e166d39c071824e454c62ad0c63 Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 18:51:29 +0200 Subject: [PATCH 03/10] chore: add the just command contract just setup/check/test/cov/fix/docs/docs-dev wrap the native tools; every uv call asserts uv.lock is current. Pin Node 22 and pnpm for the docs, and ignore .worktrees/. --- .gitignore | 7 +++--- docs/.node-version | 1 + justfile | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 docs/.node-version create mode 100644 justfile diff --git a/.gitignore b/.gitignore index 829adf0..d59e013 100644 --- a/.gitignore +++ b/.gitignore @@ -27,9 +27,6 @@ coverage.xml .hypothesis/ .pytest_cache/ -# Sphinx documentation -docs/_build/ - # IDE specific files .idea/ .vscode/ @@ -48,7 +45,9 @@ TODO.md .claude/settings.local.json .claude/doc/ temp/ -templates/ + +# Parallel checkouts (git worktree add .worktrees/) +.worktrees/ # Nix build output result result-* diff --git a/docs/.node-version b/docs/.node-version new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/docs/.node-version @@ -0,0 +1 @@ +22 diff --git a/justfile b/justfile new file mode 100644 index 0000000..6555229 --- /dev/null +++ b/justfile @@ -0,0 +1,53 @@ +# KiLM developer commands. Bare `just` lists them; AGENTS.md explains the workflow. + +# Recipes run in just's default `sh` (Git for Windows provides it) so they work on every OS. +set positional-arguments + +# Every uv command asserts that uv.lock matches pyproject.toml instead of rewriting it. +export UV_LOCKED := "1" + +[private] +default: + @just --list --unsorted + +# Install the locked Python and docs dependencies and the git hooks; safe to rerun. +setup: _require-tools + uv sync + pnpm --dir docs install --frozen-lockfile + uv run pre-commit install + @echo "Ready. Next: just check (everything CI runs), just test, just docs-dev" + +# Run every required check without modifying files; CI runs this. +check: lint test + +# Check formatting, lint, and types without modifying files. +lint: + uv run ruff format --check + uv run ruff check + uv run ty check + +# Run the tests without coverage; arguments go to pytest (e.g. just test -k sync). +test *args: + uv run pytest "$@" + +# Run the tests with coverage and enforce the threshold in pyproject.toml. +cov *args: + uv run pytest --cov "$@" + +# Format the code and apply safe lint fixes. +fix: + uv run ruff format + uv run ruff check --fix + +# Install the locked docs dependencies and build the site. +docs: + pnpm --dir docs install --frozen-lockfile + pnpm --dir docs build + +# Serve the docs with live reload. +docs-dev: + pnpm --dir docs dev + +_require-tools: + @command -v uv >/dev/null || { echo "uv is missing: https://docs.astral.sh/uv/getting-started/installation/" >&2; exit 1; } + @command -v pnpm >/dev/null || { echo "pnpm is missing: https://pnpm.io/installation (Node: docs/.node-version)" >&2; exit 1; } From 3a8491b171e5c1d1b9540efcdae88fdbb66bef0b Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 18:51:29 +0200 Subject: [PATCH 04/10] ci: run just check on a pinned toolchain and gate releases on CI CI runs one OS x Python 3.10-3.14 matrix of just check, plus coverage, docs, and the Nix build, with SHA-pinned actions and a pinned uv. Release now runs after CI passes on main, builds and smoke-tests one artifact, publishes it to PyPI, then tags and releases the same files; each job checks its own target so re-running failed jobs finishes a partial release. --- .github/workflows/ci.yml | 93 ++++++++ .github/workflows/python-package.yml | 70 ------ .github/workflows/release.yml | 322 ++++++++++++++++----------- 3 files changed, 281 insertions(+), 204 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/python-package.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ff6646a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI + +# Release (release.yml) runs after this workflow succeeds on main, so every job +# here gates publishing. Keep the workflow name in sync with release.yml. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + UV_VERSION: "0.13.0" + JUST_VERSION: "1.58.0" + +jobs: + check: + name: check (${{ matrix.os }}, Python ${{ matrix.python }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version: ${{ env.UV_VERSION }} + python-version: ${{ matrix.python }} + enable-cache: true + - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0 + with: + just-version: ${{ env.JUST_VERSION }} + - run: just check + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version: ${{ env.UV_VERSION }} + enable-cache: true + - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0 + with: + just-version: ${{ env.JUST_VERSION }} + - run: just cov + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # Reads the pnpm version from packageManager in docs/package.json. + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + package_json_file: docs/package.json + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version-file: docs/.node-version + cache: pnpm + cache-dependency-path: docs/pnpm-lock.yaml + - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0 + with: + just-version: ${{ env.JUST_VERSION }} + - run: just docs + + # Builds the flake package (which runs the test suite) so Nix packaging + # breaks visibly when pyproject.toml gains a dependency nixpkgs lacks. + nix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 + - name: Check flake + run: nix flake check -L + - name: Smoke-test the packaged CLI + run: nix run . -- --version diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml deleted file mode 100644 index b99a9b5..0000000 --- a/.github/workflows/python-package.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: CI - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - workflow_dispatch: - -jobs: - test: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - version: "latest" - - - name: Install dependencies using uv - run: | - uv sync --extra dev - - - name: Lint with ruff - run: | - uv run ruff check . - uv run ruff format --check . - - - name: Type check with pyrefly - run: | - uv run pyrefly check - - - name: Test with pytest - run: | - uv run pytest --cov=kicad_lib_manager --cov-report=xml - - - name: Upload coverage to Codecov - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' - uses: codecov/codecov-action@v4 - with: - file: ./coverage.xml - fail_ci_if_error: false - - # Builds the flake package (which runs the test suite) so Nix packaging - # breaks visibly when pyproject.toml gains a dependency nixpkgs lacks. - nix: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Nix - uses: cachix/install-nix-action@v31 - - - name: Check flake - run: nix flake check -L - - - name: Smoke-test the packaged CLI - run: nix run . -- --version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1542c8a..6b2086d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,143 +1,197 @@ name: Release +# Publishes the version in pyproject.toml after CI passes on a push to main: +# plan -> build once and smoke-test -> PyPI -> tag and GitHub release. +# Nothing happens when the version is already released. If a run fails part +# way, use "Re-run failed jobs" on that run: finished jobs keep their results +# and each remaining job completes its own target. +# +# PyPI trusted publishing is bound to this file name (release.yml); keep the +# publishing job here. + on: - push: - branches: [ main ] - workflow_dispatch: + # workflow_run always runs this file from main with repository permissions. + # It is safe because the plan job only proceeds for pushes to main of this + # repository, which already passed review and CI. + workflow_run: # zizmor: ignore[dangerous-triggers] + workflows: [CI] + types: [completed] + branches: [main] + +permissions: {} + +concurrency: + group: release + cancel-in-progress: false + +defaults: + run: + shell: bash + +env: + UV_VERSION: "0.13.0" + # The commit CI verified; every job builds and tags exactly this commit. + RELEASE_SHA: ${{ github.event.workflow_run.head_sha }} jobs: - build-and-release: + plan: + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_repository.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + name: ${{ steps.state.outputs.name }} + version: ${{ steps.state.outputs.version }} + on-pypi: ${{ steps.state.outputs.on-pypi }} + has-release: ${{ steps.state.outputs.has-release }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ env.RELEASE_SHA }} + persist-credentials: false + - name: Find what is already released + id: state + env: + GH_TOKEN: ${{ github.token }} + run: | + read -r name version < <(python3 -c 'import tomllib; p = tomllib.load(open("pyproject.toml", "rb"))["project"]; print(p["name"], p["version"])') + tag="v$version" + + status=$(curl -sS -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/$name/$version/json") + case "$status" in + 200) on_pypi=true ;; + 404) on_pypi=false ;; + *) echo "::error::PyPI lookup for $name $version returned HTTP $status"; exit 1 ;; + esac + + if gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json tagName > /dev/null 2> gh-error.txt; then + has_release=true + elif grep -q 'release not found' gh-error.txt; then + has_release=false + else + cat gh-error.txt >&2 + exit 1 + fi + + if [[ $on_pypi == true && $has_release == false ]]; then + echo "::error::$name $version is on PyPI but has no GitHub release. Use 'Re-run failed jobs' on the Release run that published it; a new run cannot prove which commit PyPI holds." + exit 1 + fi + if [[ $on_pypi == false && $has_release == true ]]; then + tag_sha=$(gh api "repos/$GITHUB_REPOSITORY/commits/$tag" --jq .sha) + if [[ $tag_sha != "$RELEASE_SHA" ]]; then + echo "::error::Release $tag exists at $tag_sha, not $RELEASE_SHA; bump the version." + exit 1 + fi + fi + + { + echo "name=$name" + echo "version=$version" + echo "on-pypi=$on_pypi" + echo "has-release=$has_release" + } >> "$GITHUB_OUTPUT" + echo "$name $version: on PyPI=$on_pypi, GitHub release=$has_release" >> "$GITHUB_STEP_SUMMARY" + + build: + needs: plan + if: needs.plan.outputs.on-pypi == 'false' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ env.RELEASE_SHA }} + persist-credentials: false + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version: ${{ env.UV_VERSION }} + enable-cache: false + - name: Build the sdist and wheel + run: uv build --no-sources + - name: Check the package metadata + run: uvx twine@7.0.0 check --strict dist/* + - name: Smoke-test the wheel + env: + VERSION: ${{ needs.plan.outputs.version }} + run: | + wheels=(dist/*.whl) + if [[ ${#wheels[@]} -ne 1 ]]; then + echo "::error::Expected one wheel, found: ${wheels[*]}" + exit 1 + fi + output=$(uvx --isolated --from "${wheels[0]}" kilm --version) + echo "$output" + grep -qF "version $VERSION" <<< "$output" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: dist + path: dist/ + if-no-files-found: error + + pypi: + needs: [plan, build] runs-on: ubuntu-latest permissions: - contents: write id-token: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build twine - - - name: Get version from pyproject.toml - id: get_version - run: | - VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Version: $VERSION" - - - name: Check if release exists - id: check_release - run: | - if curl -s "https://api.github.com/repos/${{ github.repository }}/releases/tags/v${{ steps.get_version.outputs.version }}" | grep -q '"id"'; then - echo "release_exists=true" >> $GITHUB_OUTPUT - echo "Release v${{ steps.get_version.outputs.version }} already exists" - else - echo "release_exists=false" >> $GITHUB_OUTPUT - echo "Release v${{ steps.get_version.outputs.version }} does not exist" - fi - - - name: Build package - if: steps.check_release.outputs.release_exists == 'false' - run: | - python -m build - - - name: Generate release notes - if: steps.check_release.outputs.release_exists == 'false' - id: release_notes - env: - GH_TOKEN: ${{ github.token }} - run: | - # Get the previous release tag - PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "") - - if [ -z "$PREVIOUS_TAG" ]; then - # First release, get all commits - COMMITS=$(git log --oneline --no-merges) - else - # Get commits since last release - COMMITS=$(git log --oneline --no-merges ${PREVIOUS_TAG}..HEAD) - fi - - # Get PR information - PRS=$(gh pr list --state merged --search "merged:>${PREVIOUS_TAG:-1970-01-01}" --json number,title,url --jq '.[] | "- #\(.number): \(.title) (\(.url))"' 2>/dev/null || echo "") - - # Generate release notes - cat > release_notes.md << EOF - ## What's Changed in v${{ steps.get_version.outputs.version }} - - ### Commits - $(echo "$COMMITS" | sed 's/^/- /') - - EOF - - if [ ! -z "$PRS" ]; then - cat >> release_notes.md << EOF - - ### Pull Requests - $(echo "$PRS") - - EOF - fi - - cat >> release_notes.md << 'EOF' - - ### Installation - - **Using pipx (recommended for CLI tools):** - ```bash - pipx install kilm - ``` - - **Using pip:** - ```bash - pip install kilm - ``` - - **From release assets:** - 1. Download the wheel file from this release - 2. Install with pipx or pip: - ```bash - pipx install kilm-${{ steps.get_version.outputs.version }}-py3-none-any.whl - # or - pip install kilm-${{ steps.get_version.outputs.version }}-py3-none-any.whl - ``` - - ### Updating - Run `kilm update` to upgrade an existing installation. - EOF - - # Store release notes for next step - echo "release_notes<> $GITHUB_OUTPUT - cat release_notes.md >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Create Release - if: steps.check_release.outputs.release_exists == 'false' - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ steps.get_version.outputs.version }} - name: Release v${{ steps.get_version.outputs.version }} - body: ${{ steps.release_notes.outputs.release_notes }} - files: | - dist/*.whl - dist/*.tar.gz - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish to PyPI - if: steps.check_release.outputs.release_exists == 'false' - uses: pypa/gh-action-pypi-publish@release/v1 - with: - print-hash: true - verbose: true \ No newline at end of file + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + # A re-run after a partial upload publishes only the missing files. + skip-existing: true + print-hash: true + + github: + # Runs only after this run published to PyPI, so the tag marks the commit + # whose artifacts PyPI holds. + needs: [plan, pypi] + if: needs.plan.outputs.has-release == 'false' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: dist + path: dist/ + - name: Verify PyPI serves the built files + env: + NAME: ${{ needs.plan.outputs.name }} + VERSION: ${{ needs.plan.outputs.version }} + run: | + built=$(cd dist && sha256sum -- * | sort -k2) + # PyPI's JSON API can lag behind an upload by a few minutes. + for _ in $(seq 30); do + published=$(curl -fsS "https://pypi.org/pypi/$NAME/$VERSION/json" \ + | jq -r '.urls[] | "\(.digests.sha256) \(.filename)"' | sort -k2) || published="" + [[ $published == "$built" ]] && exit 0 + sleep 10 + done + echo "::error::PyPI files for $NAME $VERSION differ from the built artifact" + diff <(echo "$built") <(echo "$published") + - name: Create the tag and GitHub release + env: + GH_TOKEN: ${{ github.token }} + NAME: ${{ needs.plan.outputs.name }} + VERSION: ${{ needs.plan.outputs.version }} + run: | + tag="v$VERSION" + if tag_sha=$(gh api "repos/$GITHUB_REPOSITORY/commits/$tag" --jq .sha 2> /dev/null) \ + && [[ $tag_sha != "$RELEASE_SHA" ]]; then + echo "::error::Tag $tag already points at $tag_sha, not $RELEASE_SHA" + exit 1 + fi + gh release create "$tag" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --target "$RELEASE_SHA" \ + --title "Release $tag" \ + --generate-notes \ + --notes "Install with \`pipx install $NAME==$VERSION\` or \`uv tool install $NAME==$VERSION\`; upgrade an existing install with \`kilm update\`." From 3dd87aaec652d6f62bda47ef6c7827c936c7530b Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 18:51:30 +0200 Subject: [PATCH 05/10] chore: remove unused spec-kit scaffolding and stale PLAN.md --- .claude/commands/plan.md | 37 ---- .claude/commands/specify.md | 13 -- .claude/commands/tasks.md | 59 ------ PLAN.md | 239 ------------------------ memory/constitution.md | 50 ----- memory/constitution_update_checklist.md | 85 --------- scripts/check-task-prerequisites.sh | 62 ------ scripts/common.sh | 77 -------- scripts/create-new-feature.sh | 96 ---------- scripts/get-feature-paths.sh | 23 --- scripts/setup-plan.sh | 44 ----- scripts/update-agent-context.sh | 234 ----------------------- 12 files changed, 1019 deletions(-) delete mode 100644 .claude/commands/plan.md delete mode 100644 .claude/commands/specify.md delete mode 100644 .claude/commands/tasks.md delete mode 100644 PLAN.md delete mode 100644 memory/constitution.md delete mode 100644 memory/constitution_update_checklist.md delete mode 100644 scripts/check-task-prerequisites.sh delete mode 100644 scripts/common.sh delete mode 100644 scripts/create-new-feature.sh delete mode 100644 scripts/get-feature-paths.sh delete mode 100644 scripts/setup-plan.sh delete mode 100644 scripts/update-agent-context.sh diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md deleted file mode 100644 index b86ef5a..0000000 --- a/.claude/commands/plan.md +++ /dev/null @@ -1,37 +0,0 @@ - -Plan how to implement the specified feature. - -This is the second step in the Spec-Driven Development lifecycle. - -Given the implementation details provided as an argument, do this: - -1. Run `scripts/setup-plan.sh --json` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. -2. Read and analyze the feature specification to understand: - - The feature requirements and user stories - - Functional and non-functional requirements - - Success criteria and acceptance criteria - - Any technical constraints or dependencies mentioned - -3. Read the constitution at `/memory/constitution.md` to understand constitutional requirements. - -4. Execute the implementation plan template: - - Load `/templates/plan-template.md` (already copied to IMPL_PLAN path) - - Set Input path to FEATURE_SPEC - - Run the Execution Flow (main) function steps 1-10 - - The template is self-contained and executable - - Follow error handling and gate checks as specified - - Let the template guide artifact generation in $SPECS_DIR: - * Phase 0 generates research.md - * Phase 1 generates data-model.md, contracts/, quickstart.md - * Phase 2 generates tasks.md - - Incorporate user-provided details from arguments into Technical Context: $ARGUMENTS - - Update Progress Tracking as you complete each phase - -5. Verify execution completed: - - Check Progress Tracking shows all phases complete - - Ensure all required artifacts were generated - - Confirm no ERROR states in execution - -6. Report results with branch name, file paths, and generated artifacts. - -Use absolute paths with the repository root for all file operations to avoid path issues. diff --git a/.claude/commands/specify.md b/.claude/commands/specify.md deleted file mode 100644 index c46f104..0000000 --- a/.claude/commands/specify.md +++ /dev/null @@ -1,13 +0,0 @@ - -Start a new feature by creating a specification and feature branch. - -This is the first step in the Spec-Driven Development lifecycle. - -Given the feature description provided as an argument, do this: - -1. Run the script `scripts/create-new-feature.sh --json "$ARGUMENTS"` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. -2. Load `templates/spec-template.md` to understand required sections. -3. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. -4. Report completion with branch name, spec file path, and readiness for the next phase. - -Note: The script creates and checks out the new branch and initializes the spec file before writing. diff --git a/.claude/commands/tasks.md b/.claude/commands/tasks.md deleted file mode 100644 index 1fd64be..0000000 --- a/.claude/commands/tasks.md +++ /dev/null @@ -1,59 +0,0 @@ - -Break down the plan into executable tasks. - -This is the third step in the Spec-Driven Development lifecycle. - -Given the context provided as an argument, do this: - -1. Run `scripts/check-task-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. -2. Load and analyze available design documents: - - Always read plan.md for tech stack and libraries - - IF EXISTS: Read data-model.md for entities - - IF EXISTS: Read contracts/ for API endpoints - - IF EXISTS: Read research.md for technical decisions - - IF EXISTS: Read quickstart.md for test scenarios - - Note: Not all projects have all documents. For example: - - CLI tools might not have contracts/ - - Simple libraries might not need data-model.md - - Generate tasks based on what's available - -3. Generate tasks following the template: - - Use `/templates/tasks-template.md` as the base - - Replace example tasks with actual tasks based on: - * **Setup tasks**: Project init, dependencies, linting - * **Test tasks [P]**: One per contract, one per integration scenario - * **Core tasks**: One per entity, service, CLI command, endpoint - * **Integration tasks**: DB connections, middleware, logging - * **Polish tasks [P]**: Unit tests, performance, docs - -4. Task generation rules: - - Each contract file → contract test task marked [P] - - Each entity in data-model → model creation task marked [P] - - Each endpoint → implementation task (not parallel if shared files) - - Each user story → integration test marked [P] - - Different files = can be parallel [P] - - Same file = sequential (no [P]) - -5. Order tasks by dependencies: - - Setup before everything - - Tests before implementation (TDD) - - Models before services - - Services before endpoints - - Core before integration - - Everything before polish - -6. Include parallel execution examples: - - Group [P] tasks that can run together - - Show actual Task agent commands - -7. Create FEATURE_DIR/tasks.md with: - - Correct feature name from implementation plan - - Numbered tasks (T001, T002, etc.) - - Clear file paths for each task - - Dependency notes - - Parallel execution guidance - -Context for task generation: $ARGUMENTS - -The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context. diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index dd5d766..0000000 --- a/PLAN.md +++ /dev/null @@ -1,239 +0,0 @@ -# KiLM (KiCad Library Manager) Plan - Cross-Platform KiCad Library Management - -KiLM is a **command-line tool for managing KiCad libraries** across projects and workstations. It provides reliable library installation, configuration management, and project template support. - -## Goals - -- **Cross-platform KiCad support** - Automatic detection of KiCad configurations on Windows, macOS, Linux -- **Library management** - Add symbol and footprint libraries from centralized repositories -- **Configuration management** - Set environment variables and manage KiCad settings -- **Project templates** - Create and manage standardized project templates -- **Backup and restore** - Timestamped backups of configuration files -- **Developer workflow** - Integrate with existing KiCad workflows seamlessly - -## Non-Goals - -- Creating KiCad libraries (use KiCad's built-in tools) -- PCB design or schematic capture (handled by KiCad) -- Version control for design files (use Git directly) -- Web interface or GUI (CLI-focused tool) - ---- - -## High-Level Architecture - -**KiLM Workflow**: - -1. **Detection**: Automatically find KiCad installations and configuration files -2. **Library Management**: Add/remove libraries from centralized repositories -3. **Configuration**: Update KiCad settings and environment variables -4. **Templates**: Create projects from standardized templates -5. **Backup**: Maintain timestamped backups for safety - -**User Workflow**: -```bash -# Initialize library setup -kilm init - -# Configure KiCad to use libraries -kilm setup - -# Check current status -kilm status - -# Add libraries -kilm list -kilm pin my-favorite-library - -# Create projects from templates -kilm template create MyProject --template basic-project -``` - ---- - -## CLI Architecture - -KiLM follows a **simple, focused command structure**: - -### Core Commands -- **`kilm init`** - Initialize library configuration -- **`kilm setup`** - Configure KiCad to use managed libraries -- **`kilm status`** - Show current configuration and library status -- **`kilm list`** - List available libraries from repositories - -### Library Management -- **`kilm pin `** - Pin favorite libraries for quick access -- **`kilm unpin `** - Unpin libraries -- **`kilm add-3d `** - Add 3D model libraries -- **`kilm sync`** - Update library definitions - -### Configuration & Templates -- **`kilm config`** - Manage configuration settings -- **`kilm template`** - Project template management -- **`kilm add-hook`** - Add project creation hooks - -### Design Principles -- **Cross-platform first** - Support Windows, macOS, Linux equally -- **Safe operations** - Always backup before making changes -- **Clear feedback** - Informative messages and status reporting -- **Backward compatibility** - Work with KiCad 8.x and newer - ---- - -## KiCad Integration Model - -### KiCad Configuration Files -- **Symbol libraries**: Managed via `sym-lib-table` -- **Footprint libraries**: Managed via `fp-lib-table` -- **Environment variables**: Set in `kicad_common.json` -- **Project templates**: Managed in dedicated template directories - -### Cross-Platform Support -- **Windows**: `%APPDATA%/kicad/` and similar paths -- **macOS**: `~/Library/Preferences/kicad/` and `~/Documents/KiCad/` -- **Linux**: `~/.config/kicad/` and `~/Documents/KiCad/` - -### Library Types -- **GitHub repositories** - Clone and manage Git-based libraries -- **Local directories** - Add existing local library paths -- **Downloaded archives** - Extract and manage ZIP/TAR libraries - ---- - -## Configuration Model (Type-Safe) - -KiLM uses **type-safe configuration management**: - -```python -# Type-safe library configuration -class LibraryConfig: - name: str - path: str - type: str # github, local, archive - enabled: bool = True - -# Cross-platform KiCad detection -class KiCadConfig: - version: str - config_path: Path - library_tables: Dict[str, Path] - environment_vars: Dict[str, str] - -# Template system -class ProjectTemplate: - name: str - description: str - variables: Dict[str, str] - files: List[Path] - hooks: Optional[Path] = None -``` - -### Professional Standards -- **No hardcoded paths** - All paths use configuration constants -- **Type safety** - Comprehensive type hints, no `Any` types -- **Error handling** - Graceful failure with informative messages -- **Professional output** - No emojis, clear command-line interface - ---- - -## Current Architecture Status - -### Working Well -- **Comprehensive CLI** - Full command set with Click framework -- **Cross-platform detection** - KiCad configuration discovery -- **Library management** - Add/remove libraries reliably -- **Project templates** - Template creation and management -- **Backup system** - Timestamped configuration backups -- **Good test coverage** - Comprehensive test suite - -### Recently Modernized -- **Build system** - Migrated from setup.py to pyproject.toml with hatchling -- **Code quality** - Removed emojis, extracted hardcoded constants -- **Documentation** - Added PLAN.md and CONTRIBUTING.md -- **Type safety** - Improved type annotations (ongoing) - -### Future Enhancements -- **CLI framework** - Migrate Click → Typer for better type safety -- **Terminal UI** - Add Rich integration for enhanced output -- **Pre-commit hooks** - Automated quality checks -- **Performance** - Optimize library detection and operations - ---- - -## Implementation Architecture - -### Module Organization -``` -kicad_lib_manager/ -├── cli.py # Main CLI entry point (Click-based) -├── constants.py # Configuration constants (NEW) -├── config.py # Configuration management -├── library_manager.py # Core library operations -├── commands/ # Individual CLI commands -│ ├── init.py -│ ├── setup.py -│ ├── status.py -│ ├── template.py -│ └── ... -└── utils/ # Utility modules - ├── backup.py # Configuration backups - ├── file_ops.py # File operations - ├── metadata.py # Library metadata - └── template.py # Template management -``` - -### Key Design Patterns -- **Command pattern** - Each CLI command is a self-contained module -- **Configuration management** - Centralized config with type safety -- **Cross-platform abstraction** - Platform-specific logic isolated -- **Template system** - Jinja2-based project generation -- **Backup strategy** - Always backup before modifications - ---- - -## Quality Standards - -### Code Quality (Professional Standards) -- **Type safety** - Comprehensive type hints, no `Any` types -- **Constants** - No hardcoded strings or magic values -- **Error handling** - Graceful failure with helpful messages -- **Documentation** - Clear docstrings and API documentation -- **Testing** - Unit and integration test coverage - -### User Experience -- **Clear commands** - Intuitive CLI with helpful output -- **Safe operations** - Backup before changes, confirmation prompts -- **Cross-platform** - Identical behavior across operating systems -- **Professional output** - No emojis, consistent formatting - -### Development Standards -- **Modern Python** - Uses pathlib, proper exception handling -- **Build system** - Modern pyproject.toml with hatchling -- **Quality tools** - Black, Ruff, Pyrefly for code quality -- **Documentation** - Comprehensive project documentation - ---- - -## Success Criteria - -- **Cross-platform reliability** - Works identically on Windows, macOS, Linux -- **Safe library management** - No data loss, always backup configurations -- **Developer productivity** - Quick project setup with templates -- **Professional quality** - Clean code, comprehensive documentation -- **KiCad integration** - Seamless integration with existing workflows -- **Type safety** - Complete type coverage with no `Any` types - ---- - -## Modernization Status - -### Phase 1: Infrastructure COMPLETED -- [x] **Modern build system** - pyproject.toml with hatchling -- [x] **Professional standards** - Removed emojis, extracted constants -- [x] **Task tracking** - Proper documentation workflow - -### Phase 2: Enhancement (Future) -- [ ] **CLI modernization** - Migrate Click → Typer + Rich -- [ ] **Type safety** - Complete Any type elimination -- [ ] **Quality pipeline** - Pre-commit hooks, automated testing -- [ ] **Performance** - Optimize operations, better caching \ No newline at end of file diff --git a/memory/constitution.md b/memory/constitution.md deleted file mode 100644 index 1ed8d77..0000000 --- a/memory/constitution.md +++ /dev/null @@ -1,50 +0,0 @@ -# [PROJECT_NAME] Constitution - - -## Core Principles - -### [PRINCIPLE_1_NAME] - -[PRINCIPLE_1_DESCRIPTION] - - -### [PRINCIPLE_2_NAME] - -[PRINCIPLE_2_DESCRIPTION] - - -### [PRINCIPLE_3_NAME] - -[PRINCIPLE_3_DESCRIPTION] - - -### [PRINCIPLE_4_NAME] - -[PRINCIPLE_4_DESCRIPTION] - - -### [PRINCIPLE_5_NAME] - -[PRINCIPLE_5_DESCRIPTION] - - -## [SECTION_2_NAME] - - -[SECTION_2_CONTENT] - - -## [SECTION_3_NAME] - - -[SECTION_3_CONTENT] - - -## Governance - - -[GOVERNANCE_RULES] - - -**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] - \ No newline at end of file diff --git a/memory/constitution_update_checklist.md b/memory/constitution_update_checklist.md deleted file mode 100644 index 7f15d7f..0000000 --- a/memory/constitution_update_checklist.md +++ /dev/null @@ -1,85 +0,0 @@ -# Constitution Update Checklist - -When amending the constitution (`/memory/constitution.md`), ensure all dependent documents are updated to maintain consistency. - -## Templates to Update - -### When adding/modifying ANY article: -- [ ] `/templates/plan-template.md` - Update Constitution Check section -- [ ] `/templates/spec-template.md` - Update if requirements/scope affected -- [ ] `/templates/tasks-template.md` - Update if new task types needed -- [ ] `/.claude/commands/plan.md` - Update if planning process changes -- [ ] `/.claude/commands/tasks.md` - Update if task generation affected -- [ ] `/CLAUDE.md` - Update runtime development guidelines - -### Article-specific updates: - -#### Article I (Library-First): -- [ ] Ensure templates emphasize library creation -- [ ] Update CLI command examples -- [ ] Add llms.txt documentation requirements - -#### Article II (CLI Interface): -- [ ] Update CLI flag requirements in templates -- [ ] Add text I/O protocol reminders - -#### Article III (Test-First): -- [ ] Update test order in all templates -- [ ] Emphasize TDD requirements -- [ ] Add test approval gates - -#### Article IV (Integration Testing): -- [ ] List integration test triggers -- [ ] Update test type priorities -- [ ] Add real dependency requirements - -#### Article V (Observability): -- [ ] Add logging requirements to templates -- [ ] Include multi-tier log streaming -- [ ] Update performance monitoring sections - -#### Article VI (Versioning): -- [ ] Add version increment reminders -- [ ] Include breaking change procedures -- [ ] Update migration requirements - -#### Article VII (Simplicity): -- [ ] Update project count limits -- [ ] Add pattern prohibition examples -- [ ] Include YAGNI reminders - -## Validation Steps - -1. **Before committing constitution changes:** - - [ ] All templates reference new requirements - - [ ] Examples updated to match new rules - - [ ] No contradictions between documents - -2. **After updating templates:** - - [ ] Run through a sample implementation plan - - [ ] Verify all constitution requirements addressed - - [ ] Check that templates are self-contained (readable without constitution) - -3. **Version tracking:** - - [ ] Update constitution version number - - [ ] Note version in template footers - - [ ] Add amendment to constitution history - -## Common Misses - -Watch for these often-forgotten updates: -- Command documentation (`/commands/*.md`) -- Checklist items in templates -- Example code/commands -- Domain-specific variations (web vs mobile vs CLI) -- Cross-references between documents - -## Template Sync Status - -Last sync check: 2025-07-16 -- Constitution version: 2.1.1 -- Templates aligned: ❌ (missing versioning, observability details) - ---- - -*This checklist ensures the constitution's principles are consistently applied across all project documentation.* \ No newline at end of file diff --git a/scripts/check-task-prerequisites.sh b/scripts/check-task-prerequisites.sh deleted file mode 100644 index 87fca37..0000000 --- a/scripts/check-task-prerequisites.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -# Check that implementation plan exists and find optional design documents -# Usage: ./check-task-prerequisites.sh [--json] - -set -e - -JSON_MODE=false -for arg in "$@"; do - case "$arg" in - --json) JSON_MODE=true ;; - --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; - esac -done - -# Source common functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths -eval $(get_feature_paths) - -# Check if on feature branch -check_feature_branch "$CURRENT_BRANCH" || exit 1 - -# Check if feature directory exists -if [[ ! -d "$FEATURE_DIR" ]]; then - echo "ERROR: Feature directory not found: $FEATURE_DIR" - echo "Run /specify first to create the feature structure." - exit 1 -fi - -# Check for implementation plan (required) -if [[ ! -f "$IMPL_PLAN" ]]; then - echo "ERROR: plan.md not found in $FEATURE_DIR" - echo "Run /plan first to create the plan." - exit 1 -fi - -if $JSON_MODE; then - # Build JSON array of available docs that actually exist - docs=() - [[ -f "$RESEARCH" ]] && docs+=("research.md") - [[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") - ([[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]) && docs+=("contracts/") - [[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") - # join array into JSON - json_docs=$(printf '"%s",' "${docs[@]}") - json_docs="[${json_docs%,}]" - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" -else - # List available design documents (optional) - echo "FEATURE_DIR:$FEATURE_DIR" - echo "AVAILABLE_DOCS:" - - # Use common check functions - check_file "$RESEARCH" "research.md" - check_file "$DATA_MODEL" "data-model.md" - check_dir "$CONTRACTS_DIR" "contracts/" - check_file "$QUICKSTART" "quickstart.md" -fi - -# Always succeed - task generation should work with whatever docs are available \ No newline at end of file diff --git a/scripts/common.sh b/scripts/common.sh deleted file mode 100644 index d636491..0000000 --- a/scripts/common.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash -# Common functions and variables for all scripts - -# Get repository root -get_repo_root() { - git rev-parse --show-toplevel -} - -# Get current branch -get_current_branch() { - git rev-parse --abbrev-ref HEAD -} - -# Check if current branch is a feature branch -# Returns 0 if valid, 1 if not -check_feature_branch() { - local branch="$1" - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then - echo "ERROR: Not on a feature branch. Current branch: $branch" - echo "Feature branches should be named like: 001-feature-name" - return 1 - fi - return 0 -} - -# Get feature directory path -get_feature_dir() { - local repo_root="$1" - local branch="$2" - echo "$repo_root/specs/$branch" -} - -# Get all standard paths for a feature -# Usage: eval $(get_feature_paths) -# Sets: REPO_ROOT, CURRENT_BRANCH, FEATURE_DIR, FEATURE_SPEC, IMPL_PLAN, TASKS -get_feature_paths() { - local repo_root=$(get_repo_root) - local current_branch=$(get_current_branch) - local feature_dir=$(get_feature_dir "$repo_root" "$current_branch") - - echo "REPO_ROOT='$repo_root'" - echo "CURRENT_BRANCH='$current_branch'" - echo "FEATURE_DIR='$feature_dir'" - echo "FEATURE_SPEC='$feature_dir/spec.md'" - echo "IMPL_PLAN='$feature_dir/plan.md'" - echo "TASKS='$feature_dir/tasks.md'" - echo "RESEARCH='$feature_dir/research.md'" - echo "DATA_MODEL='$feature_dir/data-model.md'" - echo "QUICKSTART='$feature_dir/quickstart.md'" - echo "CONTRACTS_DIR='$feature_dir/contracts'" -} - -# Check if a file exists and report -check_file() { - local file="$1" - local description="$2" - if [[ -f "$file" ]]; then - echo " ✓ $description" - return 0 - else - echo " ✗ $description" - return 1 - fi -} - -# Check if a directory exists and has files -check_dir() { - local dir="$1" - local description="$2" - if [[ -d "$dir" ]] && [[ -n "$(ls -A "$dir" 2>/dev/null)" ]]; then - echo " ✓ $description" - return 0 - else - echo " ✗ $description" - return 1 - fi -} \ No newline at end of file diff --git a/scripts/create-new-feature.sh b/scripts/create-new-feature.sh deleted file mode 100644 index 69ea3c4..0000000 --- a/scripts/create-new-feature.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/bash -# Create a new feature with branch, directory structure, and template -# Usage: ./create-new-feature.sh "feature description" -# ./create-new-feature.sh --json "feature description" - -set -e - -JSON_MODE=false - -# Collect non-flag args -ARGS=() -for arg in "$@"; do - case "$arg" in - --json) - JSON_MODE=true - ;; - --help|-h) - echo "Usage: $0 [--json] "; exit 0 ;; - *) - ARGS+=("$arg") ;; - esac -done - -FEATURE_DESCRIPTION="${ARGS[*]}" -if [ -z "$FEATURE_DESCRIPTION" ]; then - echo "Usage: $0 [--json] " >&2 - exit 1 -fi - -# Get repository root -REPO_ROOT=$(git rev-parse --show-toplevel) -SPECS_DIR="$REPO_ROOT/specs" - -# Create specs directory if it doesn't exist -mkdir -p "$SPECS_DIR" - -# Find the highest numbered feature directory -HIGHEST=0 -if [ -d "$SPECS_DIR" ]; then - for dir in "$SPECS_DIR"/*; do - if [ -d "$dir" ]; then - dirname=$(basename "$dir") - number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") - number=$((10#$number)) - if [ "$number" -gt "$HIGHEST" ]; then - HIGHEST=$number - fi - fi - done -fi - -# Generate next feature number with zero padding -NEXT=$((HIGHEST + 1)) -FEATURE_NUM=$(printf "%03d" "$NEXT") - -# Create branch name from description -BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | \ - tr '[:upper:]' '[:lower:]' | \ - sed 's/[^a-z0-9]/-/g' | \ - sed 's/-\+/-/g' | \ - sed 's/^-//' | \ - sed 's/-$//') - -# Extract 2-3 meaningful words -WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//') - -# Final branch name -BRANCH_NAME="${FEATURE_NUM}-${WORDS}" - -# Create and switch to new branch -git checkout -b "$BRANCH_NAME" - -# Create feature directory -FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" -mkdir -p "$FEATURE_DIR" - -# Copy template if it exists -TEMPLATE="$REPO_ROOT/templates/spec-template.md" -SPEC_FILE="$FEATURE_DIR/spec.md" - -if [ -f "$TEMPLATE" ]; then - cp "$TEMPLATE" "$SPEC_FILE" -else - echo "Warning: Template not found at $TEMPLATE" >&2 - touch "$SPEC_FILE" -fi - -if $JSON_MODE; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' \ - "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" -else - # Output results for the LLM to use (legacy key: value format) - echo "BRANCH_NAME: $BRANCH_NAME" - echo "SPEC_FILE: $SPEC_FILE" - echo "FEATURE_NUM: $FEATURE_NUM" -fi \ No newline at end of file diff --git a/scripts/get-feature-paths.sh b/scripts/get-feature-paths.sh deleted file mode 100644 index bfe5087..0000000 --- a/scripts/get-feature-paths.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# Get paths for current feature branch without creating anything -# Used by commands that need to find existing feature files - -set -e - -# Source common functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths -eval $(get_feature_paths) - -# Check if on feature branch -check_feature_branch "$CURRENT_BRANCH" || exit 1 - -# Output paths (don't create anything) -echo "REPO_ROOT: $REPO_ROOT" -echo "BRANCH: $CURRENT_BRANCH" -echo "FEATURE_DIR: $FEATURE_DIR" -echo "FEATURE_SPEC: $FEATURE_SPEC" -echo "IMPL_PLAN: $IMPL_PLAN" -echo "TASKS: $TASKS" \ No newline at end of file diff --git a/scripts/setup-plan.sh b/scripts/setup-plan.sh deleted file mode 100644 index 28bd056..0000000 --- a/scripts/setup-plan.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -# Setup implementation plan structure for current branch -# Returns paths needed for implementation plan generation -# Usage: ./setup-plan.sh [--json] - -set -e - -JSON_MODE=false -for arg in "$@"; do - case "$arg" in - --json) JSON_MODE=true ;; - --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; - esac -done - -# Source common functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths -eval $(get_feature_paths) - -# Check if on feature branch -check_feature_branch "$CURRENT_BRANCH" || exit 1 - -# Create specs directory if it doesn't exist -mkdir -p "$FEATURE_DIR" - -# Copy plan template if it exists -TEMPLATE="$REPO_ROOT/templates/plan-template.md" -if [ -f "$TEMPLATE" ]; then - cp "$TEMPLATE" "$IMPL_PLAN" -fi - -if $JSON_MODE; then - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \ - "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" -else - # Output all paths for LLM use - echo "FEATURE_SPEC: $FEATURE_SPEC" - echo "IMPL_PLAN: $IMPL_PLAN" - echo "SPECS_DIR: $FEATURE_DIR" - echo "BRANCH: $CURRENT_BRANCH" -fi \ No newline at end of file diff --git a/scripts/update-agent-context.sh b/scripts/update-agent-context.sh deleted file mode 100644 index 51fa640..0000000 --- a/scripts/update-agent-context.sh +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/bash -# Incrementally update agent context files based on new feature plan -# Supports: CLAUDE.md, GEMINI.md, and .github/copilot-instructions.md -# O(1) operation - only reads current context file and new plan.md - -set -e - -REPO_ROOT=$(git rev-parse --show-toplevel) -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH" -NEW_PLAN="$FEATURE_DIR/plan.md" - -# Determine which agent context files to update -CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" -GEMINI_FILE="$REPO_ROOT/GEMINI.md" -COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md" - -# Allow override via argument -AGENT_TYPE="$1" - -if [ ! -f "$NEW_PLAN" ]; then - echo "ERROR: No plan.md found at $NEW_PLAN" - exit 1 -fi - -echo "=== Updating agent context files for feature $CURRENT_BRANCH ===" - -# Extract tech from new plan -NEW_LANG=$(grep "^**Language/Version**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Language\/Version**: //' | grep -v "NEEDS CLARIFICATION" || echo "") -NEW_FRAMEWORK=$(grep "^**Primary Dependencies**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Primary Dependencies**: //' | grep -v "NEEDS CLARIFICATION" || echo "") -NEW_TESTING=$(grep "^**Testing**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Testing**: //' | grep -v "NEEDS CLARIFICATION" || echo "") -NEW_DB=$(grep "^**Storage**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Storage**: //' | grep -v "N/A" | grep -v "NEEDS CLARIFICATION" || echo "") -NEW_PROJECT_TYPE=$(grep "^**Project Type**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Project Type**: //' || echo "") - -# Function to update a single agent context file -update_agent_file() { - local target_file="$1" - local agent_name="$2" - - echo "Updating $agent_name context file: $target_file" - - # Create temp file for new context - local temp_file=$(mktemp) - - # If file doesn't exist, create from template - if [ ! -f "$target_file" ]; then - echo "Creating new $agent_name context file..." - - # Check if this is the SDD repo itself - if [ -f "$REPO_ROOT/templates/agent-file-template.md" ]; then - cp "$REPO_ROOT/templates/agent-file-template.md" "$temp_file" - else - echo "ERROR: Template not found at $REPO_ROOT/templates/agent-file-template.md" - return 1 - fi - - # Replace placeholders - sed -i.bak "s/\[PROJECT NAME\]/$(basename $REPO_ROOT)/" "$temp_file" - sed -i.bak "s/\[DATE\]/$(date +%Y-%m-%d)/" "$temp_file" - sed -i.bak "s/\[EXTRACTED FROM ALL PLAN.MD FILES\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" "$temp_file" - - # Add project structure based on type - if [[ "$NEW_PROJECT_TYPE" == *"web"* ]]; then - sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|backend/\nfrontend/\ntests/|" "$temp_file" - else - sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|src/\ntests/|" "$temp_file" - fi - - # Add minimal commands - if [[ "$NEW_LANG" == *"Python"* ]]; then - COMMANDS="cd src && pytest && ruff check ." - elif [[ "$NEW_LANG" == *"Rust"* ]]; then - COMMANDS="cargo test && cargo clippy" - elif [[ "$NEW_LANG" == *"JavaScript"* ]] || [[ "$NEW_LANG" == *"TypeScript"* ]]; then - COMMANDS="npm test && npm run lint" - else - COMMANDS="# Add commands for $NEW_LANG" - fi - sed -i.bak "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$COMMANDS|" "$temp_file" - - # Add code style - sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file" - - # Add recent changes - sed -i.bak "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" "$temp_file" - - rm "$temp_file.bak" - else - echo "Updating existing $agent_name context file..." - - # Extract manual additions - local manual_start=$(grep -n "" "$target_file" | cut -d: -f1) - local manual_end=$(grep -n "" "$target_file" | cut -d: -f1) - - if [ ! -z "$manual_start" ] && [ ! -z "$manual_end" ]; then - sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt - fi - - # Parse existing file and create updated version - python3 - << EOF -import re -import sys -from datetime import datetime - -# Read existing file -with open("$target_file", 'r') as f: - content = f.read() - -# Check if new tech already exists -tech_section = re.search(r'## Active Technologies\n(.*?)\n\n', content, re.DOTALL) -if tech_section: - existing_tech = tech_section.group(1) - - # Add new tech if not already present - new_additions = [] - if "$NEW_LANG" and "$NEW_LANG" not in existing_tech: - new_additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)") - if "$NEW_DB" and "$NEW_DB" not in existing_tech and "$NEW_DB" != "N/A": - new_additions.append(f"- $NEW_DB ($CURRENT_BRANCH)") - - if new_additions: - updated_tech = existing_tech + "\n" + "\n".join(new_additions) - content = content.replace(tech_section.group(0), f"## Active Technologies\n{updated_tech}\n\n") - -# Update project structure if needed -if "$NEW_PROJECT_TYPE" == "web" and "frontend/" not in content: - struct_section = re.search(r'## Project Structure\n\`\`\`\n(.*?)\n\`\`\`', content, re.DOTALL) - if struct_section: - updated_struct = struct_section.group(1) + "\nfrontend/src/ # Web UI" - content = re.sub(r'(## Project Structure\n\`\`\`\n).*?(\n\`\`\`)', - f'\\1{updated_struct}\\2', content, flags=re.DOTALL) - -# Add new commands if language is new -if "$NEW_LANG" and f"# {NEW_LANG}" not in content: - commands_section = re.search(r'## Commands\n\`\`\`bash\n(.*?)\n\`\`\`', content, re.DOTALL) - if not commands_section: - commands_section = re.search(r'## Commands\n(.*?)\n\n', content, re.DOTALL) - - if commands_section: - new_commands = commands_section.group(1) - if "Python" in "$NEW_LANG": - new_commands += "\ncd src && pytest && ruff check ." - elif "Rust" in "$NEW_LANG": - new_commands += "\ncargo test && cargo clippy" - elif "JavaScript" in "$NEW_LANG" or "TypeScript" in "$NEW_LANG": - new_commands += "\nnpm test && npm run lint" - - if "```bash" in content: - content = re.sub(r'(## Commands\n\`\`\`bash\n).*?(\n\`\`\`)', - f'\\1{new_commands}\\2', content, flags=re.DOTALL) - else: - content = re.sub(r'(## Commands\n).*?(\n\n)', - f'\\1{new_commands}\\2', content, flags=re.DOTALL) - -# Update recent changes (keep only last 3) -changes_section = re.search(r'## Recent Changes\n(.*?)(\n\n|$)', content, re.DOTALL) -if changes_section: - changes = changes_section.group(1).strip().split('\n') - changes.insert(0, f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK") - # Keep only last 3 - changes = changes[:3] - content = re.sub(r'(## Recent Changes\n).*?(\n\n|$)', - f'\\1{chr(10).join(changes)}\\2', content, flags=re.DOTALL) - -# Update date -content = re.sub(r'Last updated: \d{4}-\d{2}-\d{2}', - f'Last updated: {datetime.now().strftime("%Y-%m-%d")}', content) - -# Write to temp file -with open("$temp_file", 'w') as f: - f.write(content) -EOF - - # Restore manual additions if they exist - if [ -f /tmp/manual_additions.txt ]; then - # Remove old manual section from temp file - sed -i.bak '//,//d' "$temp_file" - # Append manual additions - cat /tmp/manual_additions.txt >> "$temp_file" - rm /tmp/manual_additions.txt "$temp_file.bak" - fi - fi - - # Move temp file to final location - mv "$temp_file" "$target_file" - echo "✅ $agent_name context file updated successfully" -} - -# Update files based on argument or detect existing files -case "$AGENT_TYPE" in - "claude") - update_agent_file "$CLAUDE_FILE" "Claude Code" - ;; - "gemini") - update_agent_file "$GEMINI_FILE" "Gemini CLI" - ;; - "copilot") - update_agent_file "$COPILOT_FILE" "GitHub Copilot" - ;; - "") - # Update all existing files - [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code" - [ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI" - [ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot" - - # If no files exist, create based on current directory or ask user - if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then - echo "No agent context files found. Creating Claude Code context file by default." - update_agent_file "$CLAUDE_FILE" "Claude Code" - fi - ;; - *) - echo "ERROR: Unknown agent type '$AGENT_TYPE'. Use: claude, gemini, copilot, or leave empty for all." - exit 1 - ;; -esac -echo "" -echo "Summary of changes:" -if [ ! -z "$NEW_LANG" ]; then - echo "- Added language: $NEW_LANG" -fi -if [ ! -z "$NEW_FRAMEWORK" ]; then - echo "- Added framework: $NEW_FRAMEWORK" -fi -if [ ! -z "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ]; then - echo "- Added database: $NEW_DB" -fi - -echo "" -echo "Usage: $0 [claude|gemini|copilot]" -echo " - No argument: Update all existing agent context files" -echo " - claude: Update only CLAUDE.md" -echo " - gemini: Update only GEMINI.md" -echo " - copilot: Update only .github/copilot-instructions.md" \ No newline at end of file From 4d8cb4443662d4b385943ff549f442c29e15d9b7 Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 18:51:30 +0200 Subject: [PATCH 06/10] docs: add AGENTS.md and document the just workflow AGENTS.md is the single agent and contributor instruction file; CLAUDE.md imports it. CONTRIBUTING, README, and the development, contributing, installation, and getting-started pages now use just and Python 3.10+. --- AGENTS.md | 109 +++++++ CLAUDE.md | 272 +----------------- CONTRIBUTING.md | 87 ++---- README.md | 18 +- .../content/docs/community/contributing.mdx | 50 ++-- .../content/docs/community/development.mdx | 166 +++-------- .../content/docs/guides/getting-started.mdx | 2 +- docs/src/content/docs/guides/installation.mdx | 9 +- 8 files changed, 205 insertions(+), 508 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..39b4daa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,109 @@ +# KiLM + +KiLM (KiCad Library Manager) is the `kilm` command-line tool for managing KiCad +symbol, footprint, 3D-model, and project-template libraries across projects and +workstations. It is published to PyPI as `kilm`, supports Python 3.10+ on +Windows, macOS, and Linux, and primarily targets KiCad 9 and 8. User documentation lives +at https://kilm.aristovnik.me and is built from `docs/`. + +## Commands + +Prerequisites: [uv](https://docs.astral.sh/uv/), [just](https://just.systems), +and pnpm with the Node version in `docs/.node-version` (setup installs the docs +dependencies too). On Windows, `just` runs recipes with the `sh` from Git for +Windows. + +| Command | Purpose | +| --- | --- | +| `just` | List the commands. | +| `just setup` | Install the locked Python and docs dependencies and the git hooks. Safe to rerun, including in a new worktree. | +| `just check` | Everything CI requires: `ruff format --check`, `ruff check`, `ty check`, and the tests. Never modifies files. | +| `just test [args]` | Fast test run without coverage; arguments go to pytest. | +| `just cov` | Tests with coverage; fails below `fail_under` in `pyproject.toml`. | +| `just fix` | Format and apply safe lint fixes. | +| `just docs` / `just docs-dev` | Build the docs site / serve it with live reload. | + +The recipes run tools through `uv run` with `UV_LOCKED=1`, so they fail instead +of silently rewriting `uv.lock`. Change dependencies with `uv add` or by editing +`pyproject.toml` and running `uv lock`, then commit `uv.lock`. Dev tools belong +in the `dev` dependency group. The pre-commit hook runs ruff and ty on staged +files; tests run in `just check`. + +## Layout + +- `kicad_lib_manager/main.py`: Typer app and command registration. Register + leaf commands with `app.command(...)` and only real groups (`config`, + `template`) with `add_typer`; otherwise help shows a spurious + `COMMAND [ARGS]...` (issue #13). +- `kicad_lib_manager/commands//`: one package per command, with + `command.py` and a collocated `docs.mdx`. `docs/scripts/sync-embedded-docs.ts` + copies each `docs.mdx` into the site's CLI reference, so update it in the same + change as the command. +- `kicad_lib_manager/services/` and `interfaces/`: configuration, KiCad, + library, and self-update services and their protocols. +- `kicad_lib_manager/utils/`: file operations, backups, metadata, templates, + environment variables, git helpers. +- `tests/`: pytest suite. +- `docs/`: Astro Starlight site; guides in `docs/src/content/docs/`. +- `flake.nix`, `pkgs/kilm.nix`: Nix package, built and tested by CI. It reads + name, version, and dependencies from `pyproject.toml`, so a new runtime + dependency must exist in nixpkgs under the same name. +- `.github/workflows/`: `ci.yml` (checks, coverage, docs, Nix) and `release.yml`. + +## Standards + +- `pyproject.toml` holds the ruff rules and the ty configuration. Never weaken + a rule, threshold, or type to make a check pass; fix the code. +- Every suppression names the rule and states why, for example + `# noqa: S603 -- arguments are a fixed git command` or + `# ty: ignore[invalid-assignment] -- `. Blanket `# noqa` and + `# type: ignore` are not allowed. +- Full type hints. Do not add `Any`; use precise types, `TypedDict`, protocols, + or generics. +- Use `pathlib.Path` for paths and keep behavior identical on Windows, macOS, + and Linux. +- No emojis in code, comments, or CLI output. Put fixed values in + `utils/constants.py` rather than inline literals. +- Give public modules, classes, and functions a docstring that states their + contract. Comment intent and invariants, not mechanics. + +## Tests + +Add tests at contracts and the main path. A bug fix starts with a failing test +that reproduces it. Tests must not touch the real KiCad configuration or home +directory; use `tmp_path` and monkeypatching. + +## Releases + +1. Bump `version` in `pyproject.toml`, run `uv lock`, and merge that change to + `main` through a pull request. +2. After CI passes on that commit, `release.yml` builds the sdist and wheel + once, checks and smoke-tests them, publishes to PyPI through trusted + publishing, then creates the `v` tag and GitHub release with the + same files. Commits whose version is already released do nothing. +3. If a run fails part way, use "Re-run failed jobs" on that run. Each job + checks its own target, so the rerun finishes what is missing. A new run for + a later commit refuses to create a GitHub release for a version it did not + publish. + +PyPI trusts the workflow file name `release.yml`; renaming it breaks +publishing. + +## Pull requests + +- Use conventional commit subjects (`feat:`, `fix:`, `docs:`, `chore:`). + Pull requests are squash-merged. +- CI must pass on the final head commit. For first-time contributors GitHub + holds workflows for approval: read the diff first, and do not approve runs + that change `.github/workflows/`, hooks, or build configuration without + understanding them, because `main` publishes releases. +- Review external pull requests against these standards. Ask for a failing + test with each bug fix and for `docs.mdx` updates with behavior changes. When + only small fixes remain and the contributor allows maintainer edits, push + them to the branch rather than asking for another round. + +## Agent notes + +Keep working notes under `.claude/doc/` (gitignored): tasks in +`tasks/YYYY-MM-DD-SEQ-slug.md`, decisions in `adr/ADR-####-title.md`, and agent +reports in `agent-reports/`. Pass the current task file to any delegated agent. diff --git a/CLAUDE.md b/CLAUDE.md index 7f8d5e7..43c994c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,271 +1 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Core Principles: Simplicity & Type Safety - -KiLM (KiCad Library Manager) prioritizes **professional code standards** and **complete type safety**: - -- **Type-first development**: Everything uses proper type hints - no `Any`, no dynamic typing -- **Professional standards**: No emojis in code, no hardcoded values, proper constants -- **CLI-focused**: Simple, reliable command-line interface for KiCad library management -- **Cross-platform**: Support for Windows, macOS, Linux KiCad installations - -## Architecture Overview - -KiLM is a **command-line tool for managing KiCad libraries** across projects and workstations: - -**Core Functionality**: -- KiCad configuration detection across platforms -- Library management (symbol and footprint libraries) -- Environment variable configuration -- Project template management -- Backup and restore of KiCad configurations - -## Development Commands - -### Python CLI Development -```bash -# Install Python package in development mode -pip install -e ".[dev]" - -# Install pre-commit hooks (once implemented) -pre-commit install - -# Test basic functionality -kilm status -kilm --help -``` - -### Testing & Quality -```bash -# Run tests with coverage -pytest --cov=kicad_lib_manager --cov-report=html - -# Type checking, formatting, and linting -pyrefly # Type check (required - no "any" types allowed) -ruff format . -ruff check . - -# All quality checks -pre-commit run --all-files -``` - -### CI/CD and Releases -```bash -# Create a new release (automated via GitHub Actions) -# 1. Update version in kicad_lib_manager/__init__.py -# 2. Push version tag: -git tag v0.3.1 -git push origin v0.3.1 - -# This triggers: -# - Automated testing and quality checks -# - PyPI publishing with trusted publishing -# - Draft GitHub release with auto-generated notes -# - Multi-platform compatibility verification -``` - -**Release Process:** -1. **Tag-based releases**: Push version tags to trigger automated releases -2. **Draft releases**: GitHub releases created as drafts for manual review -3. **Automatic PyPI**: Publishes to PyPI immediately via trusted publishing -4. **Auto-generated notes**: Release notes include commits, PRs, and install instructions - -## Code Quality Standards - -### Type Safety Requirements -- **No Any types**: All functions must have proper type hints -- **Pydantic models**: Use Pydantic for configuration validation where applicable -- **Type checking**: Must pass pyrefly type checking without errors - -### Professional Code Standards -- **No emojis**: Keep code and output professional - avoid emojis in code, comments, or CLI output -- **No hardcoding**: Use constants, configuration files, or environment variables -- **Proper error handling**: Consistent error patterns with informative messages -- **Cross-platform paths**: Use pathlib.Path for all file operations -- **Context7**: Often use context 7 MCP when dealing with new code and packages - -## CLI Architecture - -### Command Structure -``` -kilm # Main CLI entry point -├── init # Initialize library -├── setup # Configure KiCad to use libraries -├── status # Show current configuration -├── list # List available libraries -├── pin/unpin # Pin/unpin favorite libraries -├── add-3d # Add 3D model libraries -├── config # Configuration management -├── sync # Update/sync library content (was 'update') -├── update # Update KiLM itself (breaking change in 0.4.0) -├── add-hook # Add project hooks -├── import # Import vendor part ZIPs into a library -└── template # Project template management -``` - -## BREAKING CHANGES in v0.4.0 - -### Command Restructuring -- **`kilm update`** now updates KiLM itself (self-update functionality) -- **`kilm sync`** updates library content (was `kilm update`) -- Added deprecation banner for transition period -- Full auto-update functionality with installation method detection - -### New Features -- **Self-Update System**: Detects installation method (pip, pipx, conda, uv, homebrew) -- **PyPI Integration**: Checks for latest versions with proper caching -- **Update Preferences**: Configurable update checking and frequency -- **Professional UX**: Non-intrusive notifications with method-specific guidance - -### Core Modules -- **CLI Layer** (`main.py`): Typer-based command interface with Rich output -- **Commands** (`commands/`): Individual command implementations -- **Library Manager** (`library_manager.py`): Core library management logic -- **Configuration** (`config.py`): KiCad configuration handling with update preferences -- **Auto-Update** (`auto_update.py`): Self-update functionality with installation detection -- **Utilities** (`utils/`): File operations, backups, metadata, templates - -## Development Workflow - MANDATORY - -### Task Documentation -- **ALWAYS create task file**: For ANY work request, immediately create `.claude/doc/tasks/[date]-[seq]-[task-name].md` -- **Update throughout**: Document progress, decisions, blockers in real-time -- **Include context**: Always pass current task file path to agents for context sharing - -### Code Quality Workflow -- **After completing ANY code changes**: Prompt user "Should I run the code-reviewer to check for quality issues?" -- **Never assume**: Don't run code-reviewer automatically without asking -- **Update task file**: Document review results and any issues found - -### Agent Management -- **Use agents for all complex tasks**: Code reviews, documentation, analysis, implementation planning -- **Always provide task context**: Give agents the current task file path when applicable -- **Delegate, don't duplicate**: Use specialized agents instead of handling complex tasks directly -- **Give quality context to agents**: Point them to current task, tell them to use linters and type checkers - -### Agent Context Management (Required) - -- **Persist context in repo (important)**: Agents MUST save context under `.claude/doc/` to ensure continuity across runs. - - Tasks: `.claude/doc/tasks/YYYY-MM-DD-SEQ-slug.md` (SEQ is a 3-digit daily sequence starting at `001`. Agents only have the date—remember to increment SEQ if multiple tasks are created on the same day.) - - Decisions/ADR: `.claude/doc/adr/ADR-####-short-title.md` - - General agent reports: `.claude/doc/agent-reports/YYYY-MM-DD-SEQ-agent-report.md` -- **Pass context to agents (including general agents)**: When invoking any agent, ALWAYS include the path to the current task file. If the agent has no repo-specific instructions, explicitly tell it to save an `agent-report` (and append a short update to the task) using the same naming scheme. -- **Round-trip updates**: After each agent step, append a concise update (what changed, why, next) to the active task file. -- **Minimal duplication**: Reference prior notes; keep canonical decisions in ADRs and link from task files. -- **Example context**: - - Current task: `.claude/doc/tasks/YYYY-MM-DD-SEQ-task-name.md` - - Agent should also write: `.claude/doc/agent-reports/YYYY-MM-DD-SEQ-agent-report.md` - - Instruction: "Always provide current task path and increment SEQ for same-day tasks." - -## Key Implementation Patterns - -### KiCad Configuration Management -```python -# Professional, type-safe configuration handling -from pathlib import Path -from typing import Dict, List, Optional -from pydantic import BaseModel - -class KiCadConfig(BaseModel): - """Type-safe KiCad configuration model.""" - libraries: List[str] - environment_vars: Dict[str, str] - backup_enabled: bool = True - -def detect_kicad_config_path() -> Optional[Path]: - """Detect KiCad configuration path across platforms.""" - # Cross-platform detection logic - pass -``` - -### Library Management -```python -# Type-safe library operations -from typing import Protocol - -class LibraryManager(Protocol): - """Protocol for library management operations.""" - - def add_library(self, library_path: Path, library_type: str) -> bool: - """Add library to KiCad configuration.""" - ... - - def remove_library(self, library_name: str) -> bool: - """Remove library from KiCad configuration.""" - ... -``` - -## SOLID Principles for CLI Tools - -- **S**ingle Responsibility: Each command does one thing well -- **O**pen/Closed: Extensible for new KiCad features without modifying core -- **L**iskov Substitution: All library types implement consistent interfaces -- **I**nterface Segregation: Small, focused command interfaces -- **D**ependency Inversion: Depend on abstractions, not concrete implementations - -## Integration with KiCad - -### KiCad Version Support -- **KiCad 9.x**: Primary support target -- **KiCad 8.x**: Full compatibility - -### Configuration Files -- **Symbol libraries**: `.kicad_sym` files in symbol table -- **Footprint libraries**: `.pretty` directories in footprint table -- **Environment variables**: `kicad_common.json` configuration -- **Project templates**: Template directory management - -## Adding New Commands - -**Step 1**: Create command module -```python -# commands/new_command/command.py -from typing import Annotated, Optional -import typer -from rich.console import Console - -console = Console() - -def new_command( - option: Annotated[ - Optional[str], - typer.Option("--option", help="Command option") - ] = None, -) -> None: - """New command description.""" - # Implementation with proper type hints and Rich output - console.print("Command executed successfully!") -``` - -**Step 2**: Register in CLI -```python -# main.py - Add to the CLI app -from .commands.new_command.command import new_command - -app.command("new-command")(new_command) -``` - -**Step 3**: Add tests -```python -# tests/test_new_command.py -def test_new_command(): - """Test new command functionality.""" - # Comprehensive test coverage - pass -``` - -## Modernization Roadmap - -### Phase 1: Infrastructure -- Modern build system (pyproject.toml with hatchling) -- Complete type safety (zero Any types, pyrefly validation) -- Professional code standards (no emojis, constants extracted) -- Comprehensive documentation structure - -### Phase 2: Enhancement -- Modern CLI framework (Typer + Rich for better UX) -- Development tooling (pre-commit hooks, quality pipeline) -- Enhanced error handling and user experience -- Cross-platform support improvements \ No newline at end of file +@AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0533aa4..db9bdc2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,75 +1,34 @@ # Contributing to KiLM -Thank you for your interest in contributing to KiLM (KiCad Library Manager)! +Bug reports, feature requests, documentation fixes, and code are welcome. +Search the [issues](https://github.com/barisgit/KiLM/issues) first; for a bug, +include `kilm --version`, your KiCad version and operating system, the steps +to reproduce, and the full error output. -**[Complete Contributing Guide](https://kilm.aristovnik.me/community/contributing/)** +## Development -This file provides a quick reference. For detailed guidelines, development setup, and coding standards, see our comprehensive documentation. - -## Contribution Types - -- **Bug Reports** - Help identify and fix issues -- **Feature Requests** - Share ideas for improvements -- **Documentation** - Improve guides and examples -- **Testing** - Add tests or improve coverage -- **Code** - Fix bugs or implement features - -## Quick Development Setup - -```bash -# 1. Fork and clone -git clone https://github.com/YOUR-USERNAME/kilm.git -cd kilm - -# 2. Set up development environment -python -m venv .venv -source .venv/bin/activate # Windows: .venv\Scripts\activate - -# 3. Install with dev dependencies -pip install -e ".[dev]" - -# 4. Verify setup -kilm --version -pytest -``` - -**[Complete Development Setup Guide](https://kilm.aristovnik.me/community/development/)** - -## Quality Standards - -Before submitting, ensure your code meets our standards: +Install [uv](https://docs.astral.sh/uv/), [just](https://just.systems), and +[pnpm](https://pnpm.io) with the Node version in `docs/.node-version` (setup also +installs the docs dependencies). ```bash -# Type checking (required - zero "Any" types) -pyrefly - -# Code formatting -ruff format . - -# Linting -ruff check --fix . - -# Testing -pytest --cov=kicad_lib_manager +git clone https://github.com//KiLM.git +cd KiLM +just setup # locked dependencies and git hooks; safe to rerun +just check # everything CI runs ``` -## Pull Request Process - -1. **Create feature branch**: `git checkout -b feature/my-feature` -2. **Make changes** following coding standards -3. **Add tests** for new functionality -4. **Run quality checks** (above commands) -5. **Commit** with descriptive messages (`feat:`, `fix:`, `docs:`) -6. **Push and create PR** with clear description - -**[Complete Pull Request Guidelines](https://kilm.aristovnik.me/community/contributing/#pull-request-workflow)** - -## Getting Help +`just` lists the other commands (`test`, `cov`, `fix`, `docs`, `docs-dev`). +[AGENTS.md](AGENTS.md) describes the layout, coding standards, and release +process; it applies to human and AI contributors alike. -- **Documentation**: [Official Documentation](https://kilm.aristovnik.me) -- **Issues**: [GitHub Issues](https://github.com/barisgit/kilm/issues) for bugs and features -- **Discussions**: [GitHub Discussions](https://github.com/barisgit/kilm/discussions) for questions +## Pull requests -## License +1. Branch from `main` and keep each pull request to one change. +2. Add a test with each feature, and a failing test first with each bug fix. +3. Update the command's collocated `docs.mdx` when behavior changes. +4. Run `just check`, then open the pull request with a conventional commit + title (`feat:`, `fix:`, `docs:`, `chore:`) and link the issue it closes. -By contributing, you agree that your contributions will be licensed under the MIT License. \ No newline at end of file +By contributing, you agree that your contributions are licensed under the MIT +License. diff --git a/README.md b/README.md index be4c25f..dac029c 100644 --- a/README.md +++ b/README.md @@ -88,15 +88,15 @@ Add to a NixOS / home-manager config via the overlay: ## Contributing -Contributions are welcome! See our comprehensive guides: +Contributions are welcome. With [uv](https://docs.astral.sh/uv/), +[just](https://just.systems), and [pnpm](https://pnpm.io) installed: -- **[Contributing Guidelines](https://kilm.aristovnik.me/community/contributing/)** - Issue reporting, pull requests, coding standards -- **[Development Setup](https://kilm.aristovnik.me/community/development/)** - Local development environment and tools - -**Quick Start for Contributors:** ```bash -git clone https://github.com/barisgit/kilm.git -cd kilm -pip install -e ".[dev]" -pytest # Run all tests +git clone https://github.com/barisgit/KiLM.git +cd KiLM +just setup # locked dependencies and git hooks +just check # everything CI runs ``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the pull request process and +[AGENTS.md](AGENTS.md) for the layout and coding standards. diff --git a/docs/src/content/docs/community/contributing.mdx b/docs/src/content/docs/community/contributing.mdx index 05037e8..a4999b5 100644 --- a/docs/src/content/docs/community/contributing.mdx +++ b/docs/src/content/docs/community/contributing.mdx @@ -56,22 +56,15 @@ Include the following information: Make your changes following the coding standards below. 5. **Quality Checks** - - Run the complete quality pipeline: + + Run everything CI runs; it never modifies files: + ```bash - # Type checking (required - zero "Any" types) - pyrefly - - # Code formatting - ruff format . - - # Linting - ruff check --fix . - - # Tests with coverage - pytest --cov=kicad_lib_manager + just check ``` + `just fix` formats the code and applies safe lint fixes. + 6. **Commit Changes** Use descriptive commit messages: @@ -97,27 +90,16 @@ Include the following information: ## Coding Standards -### Type Safety (Mandatory) -- **Complete type hints**: All functions must have proper type annotations -- **Zero "Any" types**: Use specific types, protocols, or generics instead -- **Type checking**: Code must pass `pyrefly` type checking without errors - -### Code Quality -- **Professional standards**: No emojis in code, comments, or output -- **No hardcoding**: Use constants, configuration, or environment variables -- **Cross-platform**: Use `pathlib.Path` for all file operations -- **Error handling**: Consistent error patterns with informative messages - -### Testing Requirements -- **New features**: Must include corresponding tests -- **Bug fixes**: Add regression tests where applicable -- **Coverage**: Aim for high test coverage of new code -- **Integration tests**: Test KiCad integration where possible +The full standards, project layout, and release process are in +[AGENTS.md](https://github.com/barisgit/KiLM/blob/main/AGENTS.md). In short: -### Documentation -- **Docstrings**: Use clear, concise docstrings for public functions -- **Comments**: Explain complex logic, not obvious operations -- **User-facing changes**: Update relevant documentation files +- **Types**: complete type hints with no `Any`; the `ty` type checker must pass. +- **Gates**: never weaken a ruff rule or threshold to pass a check. Every + suppression names its rule and states its reason. +- **Code**: no emojis in code, comments, or output; `pathlib.Path` for paths; + identical behavior on Windows, macOS, and Linux. +- **Tests**: a test with each feature, and a failing test first with each bug fix. +- **Docs**: update the command's collocated `docs.mdx` when behavior changes. ## Code Review Process @@ -129,7 +111,7 @@ All Pull Requests undergo review for: - **Documentation**: Clear and up-to-date ## Getting Help diff --git a/docs/src/content/docs/community/development.mdx b/docs/src/content/docs/community/development.mdx index d172edc..a18595d 100644 --- a/docs/src/content/docs/community/development.mdx +++ b/docs/src/content/docs/community/development.mdx @@ -1,153 +1,69 @@ --- title: Development Setup -description: Complete guide for setting up a KiLM development environment with modern Python tooling. +description: Set up a KiLM development checkout and run the same checks as CI. --- -import { Steps, Aside } from "@astrojs/starlight/components"; - -This guide covers setting up a professional development environment for KiLM with type safety, code quality tools, and testing infrastructure. +import { Steps } from "@astrojs/starlight/components"; ## Prerequisites -- **Python 3.9+**: Required for KiLM development -- **Git**: For version control and contribution workflow -- **KiCad 6.x+**: For testing KiLM integration (run at least once) +- [uv](https://docs.astral.sh/uv/getting-started/installation/), which installs a + supported Python (3.10 or newer) when none is available +- [just](https://just.systems/man/en/packages.html) +- Git; on Windows, Git for Windows also provides the `sh` that `just` uses +- [pnpm](https://pnpm.io/installation) and the Node.js version in + `docs/.node-version`, for the documentation site +- KiCad, run at least once, to try KiLM against a real configuration -## Development Workflow +## Set up a checkout -1. **Clone the Repository** - - ```bash - git clone https://github.com/barisgit/kilm.git - cd kilm - ``` +1. Clone the repository, or your fork: -2. **Create Virtual Environment** - ```bash - # Using Python's built-in venv - python -m venv .venv - source .venv/bin/activate # On Windows: .venv\Scripts\activate + git clone https://github.com/barisgit/KiLM.git + cd KiLM ``` -3. **Install Development Dependencies** - +2. Install the locked dependencies and the git hooks: + ```bash - # Install KiLM in editable mode with dev dependencies - pip install -e ".[dev]" + just setup ``` - This installs: - - KiLM in editable mode (changes reflect immediately) - - Development tools: pytest, ruff, pyrefly - - Pre-commit hooks and coverage tools - - - -## Verification - -Verify your development setup: - -```bash -# Check KiLM installation -kilm --version -kilm --help - -# Verify KiCad detection -kilm status -``` - -## Code Quality Standards - -KiLM follows strict code quality standards: - -### Type Safety (Required) -```bash -# Type checking with pyrefly (zero "Any" types allowed) -pyrefly - -# Check for type issues -pyrefly --check kicad_lib_manager/ -``` + Setup is safe to rerun, including in a new git worktree. -### Code Formatting -```bash -# Format code with Ruff -ruff format . +3. Run everything CI runs: -# Check formatting without applying changes -ruff format --check . -``` - -### Linting and Analysis -```bash -# Run Ruff linting and analysis -ruff check . - -# Fix auto-fixable issues -ruff check --fix . - -# Format imports and code -ruff format . -``` - -### Testing -```bash -# Run all tests -pytest - -# Run with coverage reporting -pytest --cov=kicad_lib_manager --cov-report=html - -# Run specific test file -pytest tests/test_config_commands.py - -# Run tests with verbose output -pytest -v -``` - -## Development Tools - -### Pre-commit Hooks (Recommended) -```bash -# Install pre-commit hooks -pre-commit install - -# Run all hooks on all files -pre-commit run --all-files + ```bash + just check + ``` -# Update hook repositories -pre-commit autoupdate -``` + -### Quality Pipeline -```bash -# Run complete quality check (to be implemented) -# This should include: pyrefly, ruff, pytest -make quality # or equivalent script -``` +Run the CLI from the checkout with `uv run kilm --help`. - +## Commands -## Project Architecture +| Command | Purpose | +| --- | --- | +| `just` | List the commands. | +| `just check` | Formatting check, lint (ruff), type check (ty), and tests. Never modifies files. | +| `just test [args]` | Fast test run; arguments go to pytest, for example `just test -k sync`. | +| `just cov` | Tests with coverage and the minimum threshold. | +| `just fix` | Format the code and apply safe lint fixes. | +| `just docs` | Build this documentation site. | +| `just docs-dev` | Serve this documentation site with live reload. | -### Core Modules -- **`cli.py`**: Typer-based command interface (modern CLI with better help/UX) -- **`commands/`**: Individual command implementations -- **`library_manager.py`**: Core library management logic -- **`config.py`**: KiCad configuration handling -- **`utils/`**: File operations, backups, metadata, templates +The git hook formats, lints, and type-checks staged Python files on commit. The +tools run at the versions locked in `uv.lock`, the same as in CI. -### Development Principles -- **Professional code standards**: No emojis, no hardcoded values -- **Cross-platform compatibility**: Support Windows, macOS, Linux -- **SOLID principles**: Clean, maintainable architecture -- **CLI-focused**: Simple, reliable command-line interface +## Standards -## Next Steps +The ruff rules and type-checker configuration live in `pyproject.toml`; fix the +code rather than weakening a rule. Every suppression names its rule and gives a +reason. [AGENTS.md](https://github.com/barisgit/KiLM/blob/main/AGENTS.md) +describes the project layout, coding standards, and release process. -Ready to contribute? See the [Contributing Guide](/community/contributing/) for submission guidelines and coding standards. +Ready to contribute? See the [Contributing Guide](/community/contributing/). diff --git a/docs/src/content/docs/guides/getting-started.mdx b/docs/src/content/docs/guides/getting-started.mdx index 249ef59..1beadcd 100644 --- a/docs/src/content/docs/guides/getting-started.mdx +++ b/docs/src/content/docs/guides/getting-started.mdx @@ -10,7 +10,7 @@ This guide covers the essential workflows for setting up and using KiLM (KiCad L ## Prerequisites - **KiCad 6.x or newer:** Must be run at least once to generate configuration files -- **Python 3.9 or newer:** Required for KiLM installation and execution +- **Python 3.10 or newer:** Required for KiLM installation and execution - **Git:** Optional, but recommended for library version control and collaboration - **KiLM:** [Install KiLM](/guides/installation/) using pip, pipx, or uv diff --git a/docs/src/content/docs/guides/installation.mdx b/docs/src/content/docs/guides/installation.mdx index 6048b57..b57994a 100644 --- a/docs/src/content/docs/guides/installation.mdx +++ b/docs/src/content/docs/guides/installation.mdx @@ -5,11 +5,11 @@ description: How to install KiLM on your system. import { Aside } from "@astrojs/starlight/components"; -KiLM (KiCad Library Manager) is a Python CLI tool that requires Python 3.9+ and KiCad 6.x+ to be installed and run at least once. +KiLM (KiCad Library Manager) is a Python CLI tool that requires Python 3.10+ and KiCad 6.x+ to be installed and run at least once. ## System Requirements -- **Python 3.9 or newer** +- **Python 3.10 or newer** - **KiCad 6.x or newer** (must be launched at least once to create configuration directories) - **Operating System:** Windows, macOS, or Linux @@ -87,8 +87,9 @@ For contributors or those wanting the latest development version: git clone https://github.com/barisgit/kilm.git cd kilm -# Install in editable mode with development dependencies -pip install -e ".[dev]" +# Install the locked dependencies, then run the CLI from the checkout +just setup +uv run kilm --version ``` See the [Development Setup](/community/development/) guide for more details. From 2d2ff57807550cd92330375d407dddef2295d321 Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 22:33:27 +0200 Subject: [PATCH 07/10] fix(template): keep the first library's template on a name clash find_all_templates warned that it kept the existing template but replaced it with the later one, which also lacked source_library and library_path. --- kicad_lib_manager/utils/template.py | 6 +++--- tests/test_template.py | 30 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/kicad_lib_manager/utils/template.py b/kicad_lib_manager/utils/template.py index 2267a44..999230d 100644 --- a/kicad_lib_manager/utils/template.py +++ b/kicad_lib_manager/utils/template.py @@ -1043,12 +1043,12 @@ def find_all_templates(config: Any) -> dict[str, TemplateMetadata]: all_templates[name]["source_library"] = library.get("name", "unknown") all_templates[name]["library_path"] = library_path else: + # Libraries are searched in configured order, so the first one wins. click.echo( f"Warning: Duplicate template name '{name}' found.", err=True ) - click.echo(f" Existing: {all_templates[name]['path']}", err=True) - click.echo(f" New: {template['path']}", err=True) - all_templates[name] = template + click.echo(f" Using: {all_templates[name]['path']}", err=True) + click.echo(f" Ignoring: {template['path']}", err=True) return all_templates diff --git a/tests/test_template.py b/tests/test_template.py index c048e3e..9332bdb 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -18,6 +18,7 @@ TEMPLATES_DIR, create_project_from_template, create_template_metadata, + find_all_templates, render_filename, render_filename_custom, render_template_file, @@ -304,5 +305,34 @@ def test_create_project_dry_run(self): assert not project_dir.exists() +class _LibraryConfig: + """Config stand-in exposing only the libraries find_all_templates reads.""" + + def __init__(self, libraries: list[dict[str, str]]) -> None: + self._libraries = libraries + + def get_libraries(self) -> list[dict[str, str]]: + return self._libraries + + +def test_find_all_templates_keeps_first_duplicate(tmp_path: Path) -> None: + """The first configured library wins a template name clash.""" + libraries = [] + for library_name in ("first", "second"): + template_dir = tmp_path / library_name / TEMPLATES_DIR / "board" + template_dir.mkdir(parents=True) + write_template_metadata( + template_dir, create_template_metadata("board", description=library_name) + ) + libraries.append({"name": library_name, "path": str(tmp_path / library_name)}) + + templates = find_all_templates(_LibraryConfig(libraries)) + + board = templates["board"] + assert board["description"] == "first" + assert board["source_library"] == "first" + assert board["library_path"] == str(tmp_path / "first") + + if __name__ == "__main__": pytest.main() From a972dc7a4b3a546bbd4ea5a5ec46e4336fba5d33 Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 22:35:58 +0200 Subject: [PATCH 08/10] chore(setup): run git hooks from each checkout's own lock pre-commit install writes the installing checkout's .venv into the hook that all worktrees share, so setup in a worktree broke commits once it was removed, and other checkouts ran a pre-commit from a different lock. setup now points core.hooksPath at a tracked script that runs uv run --locked pre-commit in the committing checkout. --- .githooks/pre-commit | 4 ++++ .pre-commit-config.yaml | 6 ++++-- justfile | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100755 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..77b3c94 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +# Runs pre-commit from the committing checkout's uv.lock, so every worktree uses +# its own locked tool versions. `just setup` sets core.hooksPath to this folder. +exec uv run --locked pre-commit run --hook-stage pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 357f031..ac49083 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,8 @@ # Hooks run the tool versions locked in uv.lock through `uv run`, so they match -# `just check` and CI on every OS. `just setup` installs them. Tests stay out of -# the hook to keep commits fast; `just check` runs them. +# `just check` and CI on every OS. `just setup` points git at .githooks/, whose +# script runs the committing checkout's locked pre-commit, so each worktree uses +# its own uv.lock. Tests stay out of the hook to keep commits fast; `just check` +# runs them. minimum_pre_commit_version: "4.4.0" repos: - repo: local diff --git a/justfile b/justfile index 6555229..add8e79 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,7 @@ default: setup: _require-tools uv sync pnpm --dir docs install --frozen-lockfile - uv run pre-commit install + git config core.hooksPath .githooks @echo "Ready. Next: just check (everything CI runs), just test, just docs-dev" # Run every required check without modifying files; CI runs this. From 76226f6bbe94b1f727280e384e7059e49b9d707e Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 22:35:59 +0200 Subject: [PATCH 09/10] ci(release): refuse a version whose tag points elsewhere before publishing A v tag without a GitHub release skipped the plan check, so PyPI published irreversibly before the github job rejected the tag. --- .github/workflows/release.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b2086d..89ad57b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,11 +80,17 @@ jobs: echo "::error::$name $version is on PyPI but has no GitHub release. Use 'Re-run failed jobs' on the Release run that published it; a new run cannot prove which commit PyPI holds." exit 1 fi - if [[ $on_pypi == false && $has_release == true ]]; then - tag_sha=$(gh api "repos/$GITHUB_REPOSITORY/commits/$tag" --jq .sha) - if [[ $tag_sha != "$RELEASE_SHA" ]]; then - echo "::error::Release $tag exists at $tag_sha, not $RELEASE_SHA; bump the version." - exit 1 + # A tag can exist without a release, so check it on its own: publishing + # to PyPI cannot be undone if the github job then rejects the tag. + if [[ $on_pypi == false ]]; then + tag_ref=$(gh api "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/$tag" \ + --jq ".[] | select(.ref == \"refs/tags/$tag\") | .ref") + if [[ -n $tag_ref ]]; then + tag_sha=$(gh api "repos/$GITHUB_REPOSITORY/commits/$tag_ref" --jq .sha) + if [[ $tag_sha != "$RELEASE_SHA" ]]; then + echo "::error::Tag $tag exists at $tag_sha, not $RELEASE_SHA; bump the version." + exit 1 + fi fi fi From 450604dd81f116a1362ab46609e9c56d11ddca1a Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 12 Oct 2026 22:52:17 +0200 Subject: [PATCH 10/10] ci(release): finish or accept an earlier GitHub publication on re-run The github job trusted the plan job's has-release snapshot. After a run that created the release but failed before the job succeeded, "Re-run failed jobs" kept has-release=false and called gh release create again, which rejects an existing release. The job now runs .github/scripts/github_release.py, which re-reads GitHub every time: it checks the tag points at RELEASE_SHA, creates a draft only when no release exists, uploads only assets whose SHA-256 digest is missing or differs, publishes a draft (creating the tag), and verifies the published release, tag, and assets at the end. A complete release is accepted without writes; a draft or tag for another commit is rejected. The plan job counts only a published release, so a leftover draft is finished by the github job instead of being treated as released. Tests cover a rerun after a failure at each write (create, partial upload, publish), a stale asset, and releases of another commit. --- .github/scripts/github_release.py | 248 ++++++++++++++++++++++++++++++ .github/workflows/release.yml | 29 ++-- AGENTS.md | 2 + pyproject.toml | 6 + tests/test_github_release.py | 142 +++++++++++++++++ 5 files changed, 410 insertions(+), 17 deletions(-) create mode 100644 .github/scripts/github_release.py create mode 100644 tests/test_github_release.py diff --git a/.github/scripts/github_release.py b/.github/scripts/github_release.py new file mode 100644 index 0000000..289ac2b --- /dev/null +++ b/.github/scripts/github_release.py @@ -0,0 +1,248 @@ +"""Create or finish the GitHub release for a version that PyPI already holds. + +The Release workflow's ``github`` job runs this after PyPI serves the built +files. It reads GitHub's current state on every run instead of trusting the +plan job's snapshot, so "Re-run failed jobs" finishes a publication that +stopped part way and accepts one that already finished. + +The release starts as a draft without assets. The built files are uploaded +to it, and only then is it published, which creates the tag at the verified +commit. A published release therefore always has every file, and a draft +marks an unfinished attempt. + +Usage: python3 github_release.py DIST_DIR, with NAME, VERSION, +GITHUB_REPOSITORY, RELEASE_SHA, and GH_TOKEN in the environment. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import subprocess +import sys +from collections.abc import Sequence +from dataclasses import dataclass, field +from pathlib import Path +from typing import Protocol, TypedDict + +DIGEST_PREFIX = "sha256:" + + +class ReleaseError(Exception): + """GitHub holds a release or tag that this commit's files cannot complete.""" + + +@dataclass(frozen=True) +class Release: + """The parts of a GitHub release that decide what is left to publish. + + ``assets`` maps each fully uploaded asset name to its SHA-256 hex digest; + assets that are still uploading or have no digest are left out, so they + count as missing. + """ + + draft: bool + target: str + assets: dict[str, str] = field(default_factory=dict) + + +class GitHub(Protocol): + """The GitHub operations a release needs, addressed by tag name.""" + + def release(self, tag: str) -> Release | None: + """Return the published or draft release for ``tag``, if any.""" + ... + + def tag_commit(self, tag: str) -> str | None: + """Return the commit SHA that ``tag`` points at, if the tag exists.""" + ... + + def create_draft(self, tag: str, target: str, title: str, notes: str) -> None: + """Create a draft release for ``tag`` that will tag ``target``.""" + ... + + def upload(self, tag: str, files: Sequence[Path]) -> None: + """Upload ``files``, replacing assets of the same name.""" + ... + + def publish(self, tag: str) -> None: + """Publish the draft release for ``tag``, creating the tag.""" + ... + + +def file_digest(path: Path) -> str: + """Return the SHA-256 hex digest of ``path``.""" + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def publish_release( + github: GitHub, + *, + tag: str, + commit: str, + files: Sequence[Path], + title: str, + notes: str, +) -> None: + """Make ``tag`` a published release of ``commit`` that holds ``files``. + + Safe to repeat: each step checks GitHub first and only does what is + missing, so a run that stopped after any step is completed by the next + one. Raises ReleaseError when the tag or a draft belongs to another + commit, or when GitHub does not hold the expected state at the end. + """ + _check_tag(github, tag, commit) + release = github.release(tag) + if release is None: + github.create_draft(tag, commit, title, notes) + release = Release(draft=True, target=commit) + elif release.draft and release.target != commit: + msg = f"Draft release {tag} targets {release.target}, not {commit}" + raise ReleaseError(msg) + + missing = [f for f in files if release.assets.get(f.name) != file_digest(f)] + if missing: + github.upload(tag, missing) + if release.draft: + github.publish(tag) + + _verify(github, tag, commit, files) + + +def _check_tag(github: GitHub, tag: str, commit: str) -> None: + tag_commit = github.tag_commit(tag) + if tag_commit not in (None, commit): + msg = f"Tag {tag} points at {tag_commit}, not {commit}" + raise ReleaseError(msg) + + +def _verify(github: GitHub, tag: str, commit: str, files: Sequence[Path]) -> None: + release = github.release(tag) + if release is None or release.draft: + msg = f"Release {tag} is not published" + raise ReleaseError(msg) + if github.tag_commit(tag) != commit: + msg = f"Tag {tag} does not point at {commit}" + raise ReleaseError(msg) + wrong = [f.name for f in files if release.assets.get(f.name) != file_digest(f)] + if wrong: + msg = f"Release {tag} assets differ from the built files: {', '.join(wrong)}" + raise ReleaseError(msg) + + +class _AssetJson(TypedDict): + name: str + state: str + digest: str | None + + +class _ReleaseJson(TypedDict): + isDraft: bool + targetCommitish: str + assets: list[_AssetJson] + + +class _RefJson(TypedDict): + ref: str + + +class GhCli: + """GitHub operations through the ``gh`` CLI, authenticated by GH_TOKEN.""" + + def __init__(self, repo: str) -> None: + """Address the repository ``repo`` (``owner/name``).""" + self.repo = repo + + def release(self, tag: str) -> Release | None: + """Return the release for ``tag``; ``gh release view`` also finds drafts.""" + result = _gh( + "release", "view", tag, "--repo", self.repo, + "--json", "isDraft,targetCommitish,assets", + check=False, + ) # fmt: skip + if result.returncode != 0: + if "release not found" in result.stderr: + return None + raise ReleaseError(result.stderr.strip()) + data: _ReleaseJson = json.loads(result.stdout) + assets = { + asset["name"]: digest.removeprefix(DIGEST_PREFIX) + for asset in data["assets"] + if asset["state"] == "uploaded" + and (digest := asset["digest"] or "").startswith(DIGEST_PREFIX) + } + return Release( + draft=data["isDraft"], target=data["targetCommitish"], assets=assets + ) + + def tag_commit(self, tag: str) -> str | None: + """Return the commit of ``refs/tags/``, peeling annotated tags.""" + ref = f"refs/tags/{tag}" + # matching-refs answers an absent tag with an empty list instead of an error. + refs: list[_RefJson] = json.loads( + _gh("api", f"repos/{self.repo}/git/matching-refs/tags/{tag}").stdout + ) + if all(r["ref"] != ref for r in refs): + return None + return _gh( + "api", f"repos/{self.repo}/commits/{ref}", "--jq", ".sha" + ).stdout.strip() + + def create_draft(self, tag: str, target: str, title: str, notes: str) -> None: + """Create the draft; ``--notes`` is prepended to the generated notes.""" + _gh( + "release", "create", tag, "--repo", self.repo, "--draft", + "--target", target, "--title", title, "--generate-notes", + "--notes", notes, + ) # fmt: skip + + def upload(self, tag: str, files: Sequence[Path]) -> None: + """Upload ``files``; ``--clobber`` replaces partial or stale assets.""" + _gh( + "release", "upload", tag, *map(str, files), "--repo", self.repo, "--clobber" + ) + + def publish(self, tag: str) -> None: + """Publish the draft, which creates the tag at its target commit.""" + _gh("release", "edit", tag, "--repo", self.repo, "--draft=false") + + +def _gh(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]: + result = subprocess.run( # noqa: S603 -- fixed gh subcommands; values come from the workflow + ["gh", *args], capture_output=True, text=True, check=False + ) + if check and result.returncode != 0: + msg = f"gh {args[0]} {args[1]} failed: {result.stderr.strip()}" + raise ReleaseError(msg) + return result + + +def main(argv: Sequence[str]) -> int: + """Publish the files in ``argv[1]`` as the release of the environment's version.""" + name, version = os.environ["NAME"], os.environ["VERSION"] + tag = f"v{version}" + files = sorted(p for p in Path(argv[1]).iterdir() if p.is_file()) + notes = ( + f"Install with `pipx install {name}=={version}` or " + f"`uv tool install {name}=={version}`; " + "upgrade an existing install with `kilm update`." + ) + try: + publish_release( + GhCli(os.environ["GITHUB_REPOSITORY"]), + tag=tag, + commit=os.environ["RELEASE_SHA"], + files=files, + title=f"Release {tag}", + notes=notes, + ) + except ReleaseError as error: + print(f"::error::{error}", file=sys.stderr) + return 1 + print(f"Release {tag} is published with {len(files)} files.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89ad57b..c5e4793 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,8 +67,9 @@ jobs: *) echo "::error::PyPI lookup for $name $version returned HTTP $status"; exit 1 ;; esac - if gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json tagName > /dev/null 2> gh-error.txt; then - has_release=true + # A draft is an unfinished publication, so only a published release counts. + if is_draft=$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft 2> gh-error.txt); then + if [[ $is_draft == false ]]; then has_release=true; else has_release=false; fi elif grep -q 'release not found' gh-error.txt; then has_release=false else @@ -157,13 +158,19 @@ jobs: github: # Runs only after this run published to PyPI, so the tag marks the commit - # whose artifacts PyPI holds. + # whose artifacts PyPI holds. The script re-reads GitHub instead of the + # plan's snapshot, so a re-run finishes or accepts an earlier attempt. needs: [plan, pypi] if: needs.plan.outputs.has-release == 'false' runs-on: ubuntu-latest permissions: contents: write steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ env.RELEASE_SHA }} + persist-credentials: false + sparse-checkout: .github/scripts - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: dist @@ -183,21 +190,9 @@ jobs: done echo "::error::PyPI files for $NAME $VERSION differ from the built artifact" diff <(echo "$built") <(echo "$published") - - name: Create the tag and GitHub release + - name: Create or finish the tag and GitHub release env: GH_TOKEN: ${{ github.token }} NAME: ${{ needs.plan.outputs.name }} VERSION: ${{ needs.plan.outputs.version }} - run: | - tag="v$VERSION" - if tag_sha=$(gh api "repos/$GITHUB_REPOSITORY/commits/$tag" --jq .sha 2> /dev/null) \ - && [[ $tag_sha != "$RELEASE_SHA" ]]; then - echo "::error::Tag $tag already points at $tag_sha, not $RELEASE_SHA" - exit 1 - fi - gh release create "$tag" dist/* \ - --repo "$GITHUB_REPOSITORY" \ - --target "$RELEASE_SHA" \ - --title "Release $tag" \ - --generate-notes \ - --notes "Install with \`pipx install $NAME==$VERSION\` or \`uv tool install $NAME==$VERSION\`; upgrade an existing install with \`kilm update\`." + run: python3 .github/scripts/github_release.py dist diff --git a/AGENTS.md b/AGENTS.md index 39b4daa..1145ff5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,8 @@ files; tests run in `just check`. name, version, and dependencies from `pyproject.toml`, so a new runtime dependency must exist in nixpkgs under the same name. - `.github/workflows/`: `ci.yml` (checks, coverage, docs, Nix) and `release.yml`. +- `.github/scripts/`: Python steps of `release.yml`, tested like the package + (pytest and ty resolve them as top-level modules). ## Standards diff --git a/pyproject.toml b/pyproject.toml index 94ace3e..a2f27f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,8 @@ packages = ["kicad_lib_manager"] [tool.pytest.ini_options] testpaths = ["tests"] +# Release workflow scripts are tested as top-level modules. +pythonpath = [".github/scripts"] python_files = ["test_*.py", "*_test.py"] python_classes = ["Test*"] python_functions = ["test_*"] @@ -87,6 +89,10 @@ addopts = [ "--strict-config", ] +[tool.ty.environment] +# Matches pytest's pythonpath so the tests of .github/scripts resolve. +extra-paths = [".github/scripts"] + [tool.ruff] line-length = 88 diff --git a/tests/test_github_release.py b/tests/test_github_release.py new file mode 100644 index 0000000..6fd3e84 --- /dev/null +++ b/tests/test_github_release.py @@ -0,0 +1,142 @@ +"""Tests for the Release workflow's GitHub publication (.github/scripts).""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import replace +from pathlib import Path + +import pytest +from github_release import Release, ReleaseError, file_digest, publish_release + +TAG = "v1.2.3" +COMMIT = "a" * 40 + + +class JobInterruptedError(Exception): + """The job died after GitHub applied a write (lost response, cancelled runner).""" + + +class FakeGitHub: + """In-memory GitHub whose writes can fail after taking effect.""" + + def __init__(self) -> None: + self.releases: dict[str, Release] = {} + self.tags: dict[str, str] = {} + self.writes: list[str] = [] + self.fail_after: str | None = None + + def release(self, tag: str) -> Release | None: + return self.releases.get(tag) + + def tag_commit(self, tag: str) -> str | None: + return self.tags.get(tag) + + def create_draft(self, tag: str, target: str, title: str, notes: str) -> None: + # gh release create rejects a tag that already has a release. + assert tag not in self.releases, f"release {tag} already exists" + assert title + assert notes + self.releases[tag] = Release(draft=True, target=target) + self._wrote("create_draft") + + def upload(self, tag: str, files: Sequence[Path]) -> None: + release = self.releases[tag] + for path in files: + assets = {**release.assets, path.name: file_digest(path)} + release = self.releases[tag] = replace(release, assets=assets) + # Failing here leaves a partial upload: only the first file arrived. + self._wrote("upload") + + def publish(self, tag: str) -> None: + release = self.releases[tag] + self.tags.setdefault(tag, release.target) + self.releases[tag] = replace(release, draft=False) + self._wrote("publish") + + def _wrote(self, operation: str) -> None: + self.writes.append(operation) + if operation == self.fail_after: + raise JobInterruptedError(operation) + + +@pytest.fixture +def dist(tmp_path: Path) -> list[Path]: + files = [tmp_path / "kilm-1.2.3.tar.gz", tmp_path / "kilm-1.2.3-py3-none-any.whl"] + for path in files: + path.write_bytes(path.name.encode()) + return files + + +def publish(github: FakeGitHub, files: list[Path]) -> None: + publish_release( + github, tag=TAG, commit=COMMIT, files=files, title="Release", notes="Notes" + ) + + +def assert_published(github: FakeGitHub, files: list[Path]) -> None: + release = github.releases[TAG] + assert not release.draft + assert github.tags[TAG] == COMMIT + assert release.assets == {f.name: file_digest(f) for f in files} + + +def test_first_run_creates_uploads_then_publishes(dist: list[Path]) -> None: + github = FakeGitHub() + publish(github, dist) + assert github.writes == ["create_draft", "upload", "upload", "publish"] + assert_published(github, dist) + + +@pytest.mark.parametrize( + ("fail_after", "rerun_writes"), + [ + ("create_draft", ["upload", "upload", "publish"]), + ("upload", ["upload", "publish"]), + ("publish", []), + ], +) +def test_rerun_finishes_an_interrupted_publication( + dist: list[Path], fail_after: str, rerun_writes: list[str] +) -> None: + github = FakeGitHub() + github.fail_after = fail_after + with pytest.raises(JobInterruptedError): + publish(github, dist) + + github.fail_after = None + github.writes.clear() + publish(github, dist) + assert github.writes == rerun_writes + assert_published(github, dist) + + +def test_published_release_with_a_stale_asset_gets_it_replaced( + dist: list[Path], +) -> None: + github = FakeGitHub() + publish(github, dist) + release = github.releases[TAG] + github.releases[TAG] = replace( + release, assets={**release.assets, dist[0].name: "0" * 64} + ) + + github.writes.clear() + publish(github, dist) + assert github.writes == ["upload"] + assert_published(github, dist) + + +@pytest.mark.parametrize("draft", [False, True]) +def test_release_of_another_commit_is_rejected_without_writes( + dist: list[Path], draft: bool +) -> None: + other = "b" * 40 + github = FakeGitHub() + github.releases[TAG] = Release(draft=draft, target=other) + if not draft: + github.tags[TAG] = other + + with pytest.raises(ReleaseError, match=other): + publish(github, dist) + assert github.writes == []