diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57aa048f..e6d556c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,12 @@ jobs: - name: Check backend formatting run: uv run ruff format --check shelfmark + - name: Typecheck backend + run: uv run basedpyright + + - name: Check backend dead code + run: uv run vulture shelfmark + - name: Run tests run: uv run pytest tests/ -x --tb=short diff --git a/Makefile b/Makefile index 866ca122..8e85b5c1 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install install-python-dev dev build preview typecheck frontend-test clean up up down docker-build refresh restart build-serve python-lint python-lint-fix python-format python-format-check +.PHONY: help install install-python-dev dev build preview typecheck frontend-test clean up up down docker-build refresh restart build-serve python-lint python-lint-fix python-format python-format-check python-typecheck python-dead-code python-checks # Frontend directory FRONTEND_DIR := src/frontend @@ -23,6 +23,9 @@ help: @echo " python-lint-fix - Run Ruff with safe auto-fixes" @echo " python-format - Format Python backend code with Ruff" @echo " python-format-check - Check Python backend formatting with Ruff" + @echo " python-typecheck - Run BasedPyright against Python backend code" + @echo " python-dead-code - Run Vulture against Python backend code" + @echo " python-checks - Run all Python static analysis checks" @echo " clean - Remove node_modules and build artifacts" @echo "" @echo "Backend (Docker):" @@ -86,6 +89,16 @@ python-format-check: @echo "Checking Python backend formatting with Ruff..." uv run ruff format --check shelfmark +python-typecheck: + @echo "Running BasedPyright..." + uv run basedpyright + +python-dead-code: + @echo "Running Vulture..." + uv run vulture shelfmark + +python-checks: python-lint python-format-check python-typecheck python-dead-code + # Run frontend unit tests frontend-test: @echo "Running frontend unit tests..." diff --git a/pyproject.toml b/pyproject.toml index 41c9b87d..33bf984c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,10 +35,11 @@ browser = [ [dependency-groups] dev = [ - "pyright>=1.1.408", + "basedpyright>=1.39.0", "pytest", "pytest-xdist>=3.8.0", "ruff==0.15.10", + "vulture>=2.14", ] [tool.pytest.ini_options] @@ -77,7 +78,7 @@ select = [ ] ignore = ["UP035", "TRY003", "E501"] -[tool.pyright] +[tool.basedpyright] include = ["shelfmark"] exclude = [".local", "tests", "**/__pycache__", "**/node_modules"] pythonVersion = "3.14" diff --git a/readme.md b/readme.md index 1d128964..b9ca3565 100644 --- a/readme.md +++ b/readme.md @@ -221,6 +221,10 @@ Log level is configurable via Settings or `LOG_LEVEL` environment variable. ## Development ```bash +# Python tooling +make install-python-dev # Sync Python runtime + dev tools with uv +make python-checks # Run Ruff, BasedPyright, and Vulture + # Frontend development make install # Install dependencies make dev # Start Vite dev server (localhost:5173) diff --git a/shelfmark/download/clients/base_handler.py b/shelfmark/download/clients/base_handler.py index efc75149..d6a19df2 100644 --- a/shelfmark/download/clients/base_handler.py +++ b/shelfmark/download/clients/base_handler.py @@ -789,20 +789,18 @@ class ExternalClientHandler(DownloadHandler, ABC): status_callback=status_callback, ) - # Clean up on success - if result: - self._on_download_complete(task) - self._cleanup_refs[task.task_id] = (client, download_id, protocol) - - return result - except Exception as e: logger.exception("Error during download polling") status_callback("error", str(e)) self._safe_remove_download(client, download_id, protocol, "polling exception") return None - else: - return result + + # Clean up on success + if result: + self._on_download_complete(task) + self._cleanup_refs[task.task_id] = (client, download_id, protocol) + + return result def _handle_completed_file( self, diff --git a/shelfmark/download/http.py b/shelfmark/download/http.py index f015a3f2..cfe06f0b 100644 --- a/shelfmark/download/http.py +++ b/shelfmark/download/http.py @@ -251,7 +251,7 @@ def html_get_page( heartbeat_thread: Thread | None = None if status_callback: - def _heartbeat(stop_event: Event = heartbeat_stop) -> None: + def _heartbeat() -> None: # Keep the download "alive" during long bypass operations so the orchestrator # doesn't flag it as stalled. if cancel_flag and cancel_flag.is_set(): diff --git a/tests/README.md b/tests/README.md index 212768f1..b0454204 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,11 +6,14 @@ This directory contains the test suite for Shelfmark. Tests are organized by sco ```bash # Sync the local Python environment once -uv sync --locked --extra browser +make install-python-dev # Run all unit tests locally (fast, no external dependencies) uv run pytest tests/ -v -m "not integration and not e2e" +# Run Python static analysis +make python-checks + # Run E2E API tests against a running app stack uv run pytest tests/e2e/ -v -m e2e diff --git a/uv.lock b/uv.lock index 76d09495..1bf3781d 100644 --- a/uv.lock +++ b/uv.lock @@ -41,6 +41,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, ] +[[package]] +name = "basedpyright" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/f4/4a77cc1ffb3dab7391642cde30163961d8ee973e9e6b6740c7d15aa3d3ba/basedpyright-1.39.0.tar.gz", hash = "sha256:6666f51c378c7ac45877c4c1c7041ee0b5b83d755ebc82f898f47b6fafe0cc4f", size = 25357403, upload-time = "2026-04-01T12:27:41.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/47/08145d1bcc3083ed20059bdecbde404bd767f91b91e2764ec01cffec9f4b/basedpyright-1.39.0-py3-none-any.whl", hash = "sha256:91b8ad50bc85ee4a985b928f9368c35c99eee5a56c44e99b2442fa12ecc3d670", size = 12353868, upload-time = "2026-04-01T12:27:38.495Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.14.3" @@ -573,12 +585,19 @@ wheels = [ ] [[package]] -name = "nodeenv" -version = "1.10.0" +name = "nodejs-wheel-binaries" +version = "24.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/87/e5755ad739daafce2e152ab609293d65e6c663b399e28a4bbcd0f4af1f45/nodejs_wheel_binaries-24.14.1.tar.gz", hash = "sha256:d00ae0c86d7e1bfa798e8f8ad282db751af157cdcaa1208a1b9a2cf2a85ac821", size = 8056, upload-time = "2026-03-31T14:07:27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b7/9765d9a5d3b95475829ef5965d4a4f6f4badb034ee4e18c2d5f8b9b65d6f/nodejs_wheel_binaries-24.14.1-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9e856ba0f2d3d2659869e6e0f4cae6874faeeeca7f879131a88451356373ac4", size = 54945603, upload-time = "2026-03-31T14:06:58.526Z" }, + { url = "https://files.pythonhosted.org/packages/6f/15/bc2fa51ee31ce597b2af1905081e5a5add07fe0cf619bfa531d7df2f1f1b/nodejs_wheel_binaries-24.14.1-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:634f57829ebfdfe95d096f32a50c5cdd3a6c72a94dcf2b92a8bef9868cccb13e", size = 55119951, upload-time = "2026-03-31T14:07:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/a6/dd/92ff0831262af4bbb5473d4e7964fd27afb0901a2690a6ff7bc3d220d97f/nodejs_wheel_binaries-24.14.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:404b563467129e6a0ea7006a38b3d8af0ebfbc340b31a6a0af2c59ea3af90b7c", size = 59487620, upload-time = "2026-03-31T14:07:06.198Z" }, + { url = "https://files.pythonhosted.org/packages/45/36/bbbee3adf6afd00944e5a86ebd64987dea90bd347090155a4989dc3e8594/nodejs_wheel_binaries-24.14.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:7863c62f8a3946b727831f71375a9ae00205b3258478476034b49c3a1d57ac12", size = 59986044, upload-time = "2026-03-31T14:07:09.846Z" }, + { url = "https://files.pythonhosted.org/packages/05/16/119e4168bf7ed17ad7961d122701c75ac86135fa243a958b960a3f1b7055/nodejs_wheel_binaries-24.14.1-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a3f64daa1235fa6a83c778ded98d5fe4e74979ca54aa2ffb807f0805c57c3abe", size = 61489823, upload-time = "2026-03-31T14:07:13.378Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a6/d581996827b9d1133094dc347f1c4e3d2a70557973ce7a427a03337b1427/nodejs_wheel_binaries-24.14.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:810a48ce096925ead0690f7d143e48fb902ebfc9212097e8f6cb3ac6cbe8f314", size = 62069740, upload-time = "2026-03-31T14:07:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/396d1a48cbf3d5899461bda12fa97ae4010d7e4013e7f28230cb04af5818/nodejs_wheel_binaries-24.14.1-py2.py3-none-win_amd64.whl", hash = "sha256:7a087b6a727fb9242d1cc83c8b121711bd0e9686408d27de48b34b23dfb26ac5", size = 41400067, upload-time = "2026-03-31T14:07:20.513Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/adb21cf549934579e98934531e7f9b038d583fc9c2dd4b82ea01cc31bdd2/nodejs_wheel_binaries-24.14.1-py2.py3-none-win_arm64.whl", hash = "sha256:978fdfe76624c48111ab99ed0f99f9d4c1c682e420b0212ac9e1daee52f20283", size = 39096873, upload-time = "2026-03-31T14:07:23.977Z" }, ] [[package]] @@ -836,19 +855,6 @@ version = "0.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cb/04/2ba023d5f771b645f7be0c281cdacdcd939fe13d1deb331fc5ed1a6b3a98/PyRect-0.2.0.tar.gz", hash = "sha256:f65155f6df9b929b67caffbd57c0947c5ae5449d3b580d178074bffb47a09b78", size = 17219, upload-time = "2022-03-16T04:45:52.36Z" } -[[package]] -name = "pyright" -version = "1.1.408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, -] - [[package]] name = "pyscreeze" version = "1.0.1" @@ -1273,10 +1279,11 @@ browser = [ [package.dev-dependencies] dev = [ - { name = "pyright" }, + { name = "basedpyright" }, { name = "pytest" }, { name = "pytest-xdist" }, { name = "ruff" }, + { name = "vulture" }, ] [package.metadata] @@ -1309,10 +1316,11 @@ provides-extras = ["browser"] [package.metadata.requires-dev] dev = [ - { name = "pyright", specifier = ">=1.1.408" }, + { name = "basedpyright", specifier = ">=1.39.0" }, { name = "pytest" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "ruff", specifier = "==0.15.10" }, + { name = "vulture", specifier = ">=2.14" }, ] [[package]] @@ -1463,6 +1471,15 @@ socks = [ { name = "pysocks" }, ] +[[package]] +name = "vulture" +version = "2.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/3e/4d08c5903b2c0c70cad583c170cc4a663fc6a61e2ad00b711fcda61358cd/vulture-2.16.tar.gz", hash = "sha256:f8d9f6e2af03011664a3c6c240c9765b3f392917d3135fddca6d6a68d359f717", size = 52680, upload-time = "2026-03-25T14:41:27.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/f935130312330614811dae2ea9df3f395f6d63889eb6c2e68c14507152ee/vulture-2.16-py3-none-any.whl", hash = "sha256:6e0f1c312cef1c87856957e5c2ca9608834a7c794c2180477f30bf0e4cc58eee", size = 26993, upload-time = "2026-03-25T14:41:26.21Z" }, +] + [[package]] name = "websocket-client" version = "1.9.0"