From cee9970626fb462ae92b2c92dc27f917182f3bab Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 15:12:34 +0100 Subject: [PATCH 01/25] Setup ty. --- .github/workflows/main.yml | 1 - justfile | 6 +- pyproject.toml | 34 ++------- uv.lock | 143 +++++++------------------------------ 4 files changed, 34 insertions(+), 150 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7a9564c..241b5bbd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,6 @@ jobs: - name: Install just uses: extractions/setup-just@v3 - run: just typing - - run: just typing-nb run-tests: diff --git a/justfile b/justfile index f8e27aca..10c25b1b 100644 --- a/justfile +++ b/justfile @@ -16,11 +16,7 @@ test-nb: # Run type checking typing: - uv run --group typing --no-dev --isolated mypy - -# Run type checking on notebooks -typing-nb: - uv run --group typing --no-dev --isolated nbqa mypy --ignore-missing-imports . + uv run --group typing ty check src # Run linting lint: diff --git a/pyproject.toml b/pyproject.toml index 7f19dd16..40ebb1f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ test = [ "coiled>=1.42.0", "cloudpickle>=3.0.0", ] -typing = ["mypy>=1.11.0", "nbqa>=1.8.5"] +typing = ["ty>=0.0.5"] [project.urls] Changelog = "https://pytask-dev.readthedocs.io/en/stable/changes.html" @@ -167,33 +167,11 @@ filterwarnings = [ "ignore:The --rsyncdir command line argument:DeprecationWarning", ] -[tool.mypy] -files = ["src", "tests"] -check_untyped_defs = true -disallow_any_generics = true -disallow_incomplete_defs = true -disallow_untyped_defs = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -disable_error_code = ["import-untyped"] - -[[tool.mypy.overrides]] -module = "tests.*" -disallow_untyped_defs = false -ignore_errors = true - -[[tool.mypy.overrides]] -module = ["click_default_group", "networkx"] -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = ["_pytask.coiled_utils"] -disable_error_code = ["import-not-found"] - -[[tool.mypy.overrides]] -module = ["_pytask.hookspecs"] -disable_error_code = ["empty-body"] +[tool.ty.src] +exclude = ["src/_pytask/_hashlib.py"] + +[tool.ty.terminal] +error-on-warning = true [tool.coverage.report] exclude_also = [ diff --git a/uv.lock b/uv.lock index dfaaaac2..38651248 100644 --- a/uv.lock +++ b/uv.lock @@ -215,19 +215,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/bc/3f66af9beb683728e06ca08797e4e9d3e44f432f339718cae3ba856a9cad/autodocsumm-0.2.14-py3-none-any.whl", hash = "sha256:3bad8717fc5190802c60392a7ab04b9f3c97aa9efa8b3780b3d81d615bfe5dc0", size = 14640, upload-time = "2024-10-23T18:51:45.115Z" }, ] -[[package]] -name = "autopep8" -version = "2.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycodestyle" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" }, -] - [[package]] name = "babel" version = "2.17.0" @@ -2023,60 +2010,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481, upload-time = "2025-05-19T14:16:36.024Z" }, ] -[[package]] -name = "mypy" -version = "1.18.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, - { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, - { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "myst-nb" version = "1.2.0" @@ -2173,22 +2106,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl", hash = "sha256:c6fbe6e48b60cacac14af40b38bf338a3b88f47f085c54ac5b8639ff0babaf4b", size = 12818, upload-time = "2024-12-23T18:33:44.566Z" }, ] -[[package]] -name = "nbqa" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "autopep8" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tokenize-rt" }, - { name = "tomli" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/76/62d2609924cf34445148cd6b5de694cf64c179cc416cac93182579620e57/nbqa-1.9.1.tar.gz", hash = "sha256:a1f4bcf587c597302fed295951001fc4e1be4ce0e77e1ab1b25ac2fbe3db0cdd", size = 38348, upload-time = "2024-11-10T12:21:58.333Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl", hash = "sha256:95552d2f6c2c038136252a805aa78d85018aef922586270c3a074332737282e5", size = 35259, upload-time = "2024-11-10T12:21:56.731Z" }, -] - [[package]] name = "nest-asyncio" version = "1.6.0" @@ -2408,15 +2325,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, ] -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -2689,15 +2597,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312, upload-time = "2025-03-29T17:33:30.669Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424, upload-time = "2025-03-29T17:33:29.405Z" }, -] - [[package]] name = "pycparser" version = "2.22" @@ -2810,8 +2709,7 @@ test = [ { name = "syrupy" }, ] typing = [ - { name = "mypy" }, - { name = "nbqa" }, + { name = "ty" }, ] [package.metadata] @@ -2865,10 +2763,7 @@ test = [ { name = "pytest-xdist", specifier = ">=3.6.1" }, { name = "syrupy", specifier = ">=4.5.0" }, ] -typing = [ - { name = "mypy", specifier = ">=1.11.0" }, - { name = "nbqa", specifier = ">=1.8.5" }, -] +typing = [{ name = "ty", specifier = ">=0.0.5" }] [[package]] name = "pytest" @@ -3760,15 +3655,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/44/aa5c8b10b2cce7a053018e0d132bd58e27527a0243c4985383d5b6fd93e9/tblib-3.1.0-py3-none-any.whl", hash = "sha256:670bb4582578134b3d81a84afa1b016128b429f3d48e6cbbaecc9d15675e984e", size = 12552, upload-time = "2025-03-31T12:58:26.142Z" }, ] -[[package]] -name = "tokenize-rt" -version = "6.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/ed/8f07e893132d5051d86a553e749d5c89b2a4776eb3a579b72ed61f8559ca/tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", size = 5476, upload-time = "2025-05-23T23:48:00.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44", size = 6004, upload-time = "2025-05-23T23:47:58.812Z" }, -] - [[package]] name = "toml" version = "0.10.2" @@ -3866,6 +3752,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "ty" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/db/6299d478000f4f1c6f9bf2af749359381610ffc4cbe6713b66e436ecf6e7/ty-0.0.5.tar.gz", hash = "sha256:983da6330773ff71e2b249810a19c689f9a0372f6e21bbf7cde37839d05b4346", size = 4806218, upload-time = "2025-12-20T21:19:17.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/c1f61ba378b4191e641bb36c07b7fcc70ff844d61be7a4bf2fea7472b4a9/ty-0.0.5-py3-none-linux_armv6l.whl", hash = "sha256:1594cd9bb68015eb2f5a3c68a040860f3c9306dc6667d7a0e5f4df9967b460e2", size = 9785554, upload-time = "2025-12-20T21:19:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f9/b37b77c03396bd779c1397dae4279b7ad79315e005b3412feed8812a4256/ty-0.0.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7c0140ba980233d28699d9ddfe8f43d0b3535d6a3bbff9935df625a78332a3cf", size = 9603995, upload-time = "2025-12-20T21:19:15.256Z" }, + { url = "https://files.pythonhosted.org/packages/7d/70/4e75c11903b0e986c0203040472627cb61d6a709e1797fb08cdf9d565743/ty-0.0.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:15de414712cde92048ae4b1a77c4dc22920bd23653fe42acaf73028bad88f6b9", size = 9145815, upload-time = "2025-12-20T21:19:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/89/05/93983dfcf871a41dfe58e5511d28e6aa332a1f826cc67333f77ae41a2f8a/ty-0.0.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:438aa51ad6c5fae64191f8d58876266e26f9250cf09f6624b6af47a22fa88618", size = 9619849, upload-time = "2025-12-20T21:19:19.084Z" }, + { url = "https://files.pythonhosted.org/packages/82/b6/896ab3aad59f846823f202e94be6016fb3f72434d999d2ae9bd0f28b3af9/ty-0.0.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b3d373fd96af1564380caf153600481c676f5002ee76ba8a7c3508cdff82ee0", size = 9606611, upload-time = "2025-12-20T21:19:24.583Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ae/098e33fc92330285ed843e2750127e896140c4ebd2d73df7732ea496f588/ty-0.0.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8453692503212ad316cf8b99efbe85a91e5f63769c43be5345e435a1b16cba5a", size = 10029523, upload-time = "2025-12-20T21:19:07.055Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/f4b4c33758b9295e9aca0de9645deca0f4addd21d38847228723a6e780fc/ty-0.0.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2e4c454139473abbd529767b0df7a795ed828f780aef8d0d4b144558c0dc4446", size = 10870892, upload-time = "2025-12-20T21:19:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c5/4e3e7e88389365aa1e631c99378711cf0c9d35a67478cb4720584314cf44/ty-0.0.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:426d4f3b82475b1ec75f3cc9ee5a667c8a4ae8441a09fcd8e823a53b706d00c7", size = 10599291, upload-time = "2025-12-20T21:19:26.557Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5d/138f859ea87bd95e17b9818e386ae25a910e46521c41d516bf230ed83ffc/ty-0.0.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5710817b67c6b2e4c0224e4f319b7decdff550886e9020f6d46aa1ce8f89a609", size = 10413515, upload-time = "2025-12-20T21:19:11.094Z" }, + { url = "https://files.pythonhosted.org/packages/27/21/1cbcd0d3b1182172f099e88218137943e0970603492fb10c7c9342369d9a/ty-0.0.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23c55ef08882c7c5ced1ccb90b4eeefa97f690aea254f58ac0987896c590f76", size = 10144992, upload-time = "2025-12-20T21:19:13.225Z" }, + { url = "https://files.pythonhosted.org/packages/ad/30/fdac06a5470c09ad2659a0806497b71f338b395d59e92611f71b623d05a0/ty-0.0.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b9e4c1a28a23b14cf8f4f793f4da396939f16c30bfa7323477c8cc234e352ac4", size = 9606408, upload-time = "2025-12-20T21:19:09.212Z" }, + { url = "https://files.pythonhosted.org/packages/09/93/e99dcd7f53295192d03efd9cbcec089a916f49cad4935c0160ea9adbd53d/ty-0.0.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4e9ebb61529b9745af662e37c37a01ad743cdd2c95f0d1421705672874d806cd", size = 9630040, upload-time = "2025-12-20T21:19:38.165Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f8/6d1e87186e4c35eb64f28000c1df8fd5f73167ce126c5e3dd21fd1204a23/ty-0.0.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5eb191a8e332f50f56dfe45391bdd7d43dd4ef6e60884710fd7ce84c5d8c1eb5", size = 9754016, upload-time = "2025-12-20T21:19:32.79Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/20f989342cb3115852dda404f1d89a10a3ce93f14f42b23f095a3d1a00c9/ty-0.0.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:92ed7451a1e82ee134a2c24ca43b74dd31e946dff2b08e5c34473e6b051de542", size = 10252877, upload-time = "2025-12-20T21:19:20.787Z" }, + { url = "https://files.pythonhosted.org/packages/57/9d/fc66fa557443233dfad9ae197ff3deb70ae0efcfb71d11b30ef62f5cdcc3/ty-0.0.5-py3-none-win32.whl", hash = "sha256:71f6707e4c1c010c158029a688a498220f28bb22fdb6707e5c20e09f11a5e4f2", size = 9212640, upload-time = "2025-12-20T21:19:30.817Z" }, + { url = "https://files.pythonhosted.org/packages/68/b6/05c35f6dea29122e54af0e9f8dfedd0a100c721affc8cc801ebe2bc2ed13/ty-0.0.5-py3-none-win_amd64.whl", hash = "sha256:2b8b754a0d7191e94acdf0c322747fec34371a4d0669f5b4e89549aef28814ae", size = 10034701, upload-time = "2025-12-20T21:19:28.311Z" }, + { url = "https://files.pythonhosted.org/packages/df/ca/4201ed5cb2af73912663d0c6ded927c28c28b3c921c9348aa8d2cfef4853/ty-0.0.5-py3-none-win_arm64.whl", hash = "sha256:83bea5a5296caac20d52b790ded2b830a7ff91c4ed9f36730fe1f393ceed6654", size = 9566474, upload-time = "2025-12-20T21:19:22.518Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.0" From 9dd5c914fe953b2ed2794307f70bf83fc7345e4b Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 15:16:29 +0100 Subject: [PATCH 02/25] Fix traceback errors. --- src/_pytask/traceback.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index 9e1435af..72cc699d 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -2,14 +2,14 @@ from __future__ import annotations +from dataclasses import dataclass +from dataclasses import field from pathlib import Path from types import TracebackType from typing import TYPE_CHECKING from typing import ClassVar import pluggy -from attrs import define -from attrs import field from rich.traceback import Traceback as RichTraceback import _pytask @@ -41,10 +41,10 @@ OptionalExceptionInfo: TypeAlias = ExceptionInfo | tuple[None, None, None] -@define +@dataclass class Traceback: exc_info: OptionalExceptionInfo - show_locals: bool = field() + show_locals: bool = field(default_factory=lambda: Traceback._show_locals) _show_locals: ClassVar[bool] = False suppress: ClassVar[tuple[Path, ...]] = ( @@ -53,10 +53,6 @@ class Traceback: TREE_UTIL_LIB_DIRECTORY, ) - @show_locals.default - def _show_locals_default(self) -> bool: - return self._show_locals - def __rich_console__( self, console: Console, console_options: ConsoleOptions ) -> RenderResult: From 0348196593732ce94b55d0cbdc95de04b5e545ae Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 15:32:18 +0100 Subject: [PATCH 03/25] Fix. --- src/_pytask/dag_command.py | 5 +++-- src/_pytask/hookspecs.py | 16 ++++++++-------- src/_pytask/task_utils.py | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/_pytask/dag_command.py b/src/_pytask/dag_command.py index b099c045..796c768f 100644 --- a/src/_pytask/dag_command.py +++ b/src/_pytask/dag_command.py @@ -183,9 +183,10 @@ def build_dag(raw_config: dict[str, Any]) -> nx.DiGraph: session = Session.from_config(config) - except (ConfigurationError, Exception): # noqa: BLE001 # pragma: no cover + except (ConfigurationError, Exception) as e: # pragma: no cover console.print_exception() - session = Session(exit_code=ExitCode.CONFIGURATION_FAILED) + msg = "Failed to configure session for dag." + raise ConfigurationError(msg) from e else: session.hook.pytask_log_session_header(session=session) diff --git a/src/_pytask/hookspecs.py b/src/_pytask/hookspecs.py index 02ce0721..79804109 100644 --- a/src/_pytask/hookspecs.py +++ b/src/_pytask/hookspecs.py @@ -67,7 +67,7 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None: @hookspec(firstresult=True) -def pytask_configure(pm: PluginManager, raw_config: dict[str, Any]) -> dict[str, Any]: +def pytask_configure(pm: PluginManager, raw_config: dict[str, Any]) -> dict[str, Any]: # type: ignore[invalid-return-type] """Configure pytask. The main hook implementation which controls the configuration and calls subordinated @@ -117,7 +117,7 @@ def pytask_collect(session: Session) -> Any: @hookspec(firstresult=True) -def pytask_ignore_collect(path: Path, config: dict[str, Any]) -> bool: +def pytask_ignore_collect(path: Path, config: dict[str, Any]) -> bool: # type: ignore[invalid-return-type] """Ignore collected path. This hook is indicates for each directory and file whether it should be ignored. @@ -138,7 +138,7 @@ def pytask_collect_modify_tasks(session: Session, tasks: list[PTask]) -> None: @hookspec(firstresult=True) def pytask_collect_file_protocol( session: Session, path: Path, reports: list[CollectionReport] -) -> list[CollectionReport]: +) -> list[CollectionReport]: # type: ignore[invalid-return-type] """Start protocol to collect files. The protocol calls the subordinate hook :func:`pytask_collect_file` which might @@ -166,7 +166,7 @@ def pytask_collect_file_log(session: Session, reports: list[CollectionReport]) - @hookspec(firstresult=True) def pytask_collect_task_protocol( session: Session, path: Path | None, name: str, obj: Any -) -> CollectionReport | None: +) -> CollectionReport | None: # type: ignore[invalid-return-type] """Start protocol to collect tasks.""" @@ -180,7 +180,7 @@ def pytask_collect_task_setup( @hookspec(firstresult=True) def pytask_collect_task( session: Session, path: Path | None, name: str, obj: Any -) -> PTask: +) -> PTask: # type: ignore[invalid-return-type] """Collect a single task.""" @@ -196,14 +196,14 @@ def pytask_collect_task_teardown(session: Session, task: PTask) -> None: @hookspec(firstresult=True) def pytask_collect_node( session: Session, path: Path, node_info: NodeInfo -) -> PNode | PProvisionalNode | None: +) -> PNode | PProvisionalNode | None: # type: ignore[invalid-return-type] """Collect a node which is a dependency or a product of a task.""" @hookspec(firstresult=True) def pytask_collect_log( session: Session, reports: list[CollectionReport], tasks: list[PTask] -) -> None: +) -> None: # type: ignore[invalid-return-type] """Log errors occurring during the collection. This hook reports errors during the collection. @@ -243,7 +243,7 @@ def pytask_execute_build(session: Session) -> Any: @hookspec(firstresult=True) -def pytask_execute_task_protocol(session: Session, task: PTask) -> ExecutionReport: +def pytask_execute_task_protocol(session: Session, task: PTask) -> ExecutionReport: # type: ignore[invalid-return-type] """Run the protocol for executing a test. This hook runs all stages of the execution process, setup, execution, and teardown diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py index 9dbfb049..f44b31cd 100644 --- a/src/_pytask/task_utils.py +++ b/src/_pytask/task_utils.py @@ -188,7 +188,7 @@ def _parse_name(func: Callable[..., Any], name: str | None) -> str: func = func.func if hasattr(func, "__name__"): - return func.__name__ + return str(func.__name__) msg = "Cannot infer name for task function." raise NotImplementedError(msg) From d0e2a76e67c78827f90fbf938d31f8506b61035c Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 16:04:41 +0100 Subject: [PATCH 04/25] Add protocol for task functions. --- src/_pytask/collect.py | 5 +++-- src/_pytask/collect_utils.py | 7 ++++--- src/_pytask/mark/structures.py | 3 ++- src/_pytask/mark_utils.py | 5 +++-- src/_pytask/task_utils.py | 7 ++++--- src/_pytask/typing.py | 18 +++++++++++++++++- src/pytask/__init__.py | 2 ++ 7 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index d5db1403..d19e555b 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -52,6 +52,7 @@ from _pytask.task_utils import COLLECTED_TASKS from _pytask.task_utils import parse_collected_tasks_with_task_marker from _pytask.task_utils import task as task_decorator +from _pytask.typing import TaskFunction from _pytask.typing import is_task_function if TYPE_CHECKING: @@ -115,7 +116,7 @@ def _collect_from_tasks(session: Session) -> None: for raw_task in to_list(session.config.get("tasks", ())): if is_task_function(raw_task): - if not hasattr(raw_task, "pytask_meta"): + if not isinstance(raw_task, TaskFunction): raw_task = task_decorator()(raw_task) # noqa: PLW2901 path = get_file(raw_task) @@ -339,7 +340,7 @@ def pytask_collect_task( markers = get_all_marks(obj) - if hasattr(obj, "pytask_meta"): + if isinstance(obj, TaskFunction): attributes = { **obj.pytask_meta.attributes, "collection_id": obj.pytask_meta._id, diff --git a/src/_pytask/collect_utils.py b/src/_pytask/collect_utils.py index 23451b2c..141ccc31 100644 --- a/src/_pytask/collect_utils.py +++ b/src/_pytask/collect_utils.py @@ -21,6 +21,7 @@ from _pytask.tree_util import tree_leaves from _pytask.tree_util import tree_map_with_path from _pytask.typing import ProductType +from _pytask.typing import TaskFunction from _pytask.typing import no_default if TYPE_CHECKING: @@ -57,7 +58,7 @@ def parse_dependencies_from_task_function( """Parse dependencies from task function.""" dependencies = {} - task_kwargs = obj.pytask_meta.kwargs if hasattr(obj, "pytask_meta") else {} + task_kwargs = obj.pytask_meta.kwargs if isinstance(obj, TaskFunction) else {} signature_defaults = parse_keyword_arguments_from_signature_defaults(obj) kwargs = {**signature_defaults, **task_kwargs} kwargs.pop("produces", None) @@ -174,7 +175,7 @@ def parse_products_from_task_function( out: dict[str, Any] = {} - task_kwargs = obj.pytask_meta.kwargs if hasattr(obj, "pytask_meta") else {} + task_kwargs = obj.pytask_meta.kwargs if isinstance(obj, TaskFunction) else {} signature_defaults = parse_keyword_arguments_from_signature_defaults(obj) kwargs = {**signature_defaults, **task_kwargs} @@ -226,7 +227,7 @@ def parse_products_from_task_function( ) out[parameter_name] = collected_products - task_produces = obj.pytask_meta.produces if hasattr(obj, "pytask_meta") else None + task_produces = obj.pytask_meta.produces if isinstance(obj, TaskFunction) else None if task_produces: has_task_decorator = True collected_products = _collect_nodes_and_provisional_nodes( diff --git a/src/_pytask/mark/structures.py b/src/_pytask/mark/structures.py index 76b5e2a8..2dbb61b3 100644 --- a/src/_pytask/mark/structures.py +++ b/src/_pytask/mark/structures.py @@ -10,6 +10,7 @@ from _pytask.mark_utils import get_all_marks from _pytask.models import CollectionMetadata +from _pytask.typing import TaskFunction from _pytask.typing import is_task_function if TYPE_CHECKING: @@ -166,7 +167,7 @@ def store_mark(obj: Callable[..., Any], mark: Mark) -> None: """ assert isinstance(mark, Mark), mark - if hasattr(obj, "pytask_meta"): + if isinstance(obj, TaskFunction): obj.pytask_meta.markers = [*get_unpacked_marks(obj), mark] else: obj.pytask_meta = CollectionMetadata( # type: ignore[attr-defined] diff --git a/src/_pytask/mark_utils.py b/src/_pytask/mark_utils.py index 4ee0e50c..f4f8b188 100644 --- a/src/_pytask/mark_utils.py +++ b/src/_pytask/mark_utils.py @@ -11,6 +11,7 @@ from _pytask.models import CollectionMetadata from _pytask.node_protocols import PTask +from _pytask.typing import TaskFunction if TYPE_CHECKING: from _pytask.mark import Mark @@ -21,14 +22,14 @@ def get_all_marks(obj_or_task: Any | PTask) -> list[Mark]: if isinstance(obj_or_task, PTask): return obj_or_task.markers obj = obj_or_task - return obj.pytask_meta.markers if hasattr(obj, "pytask_meta") else [] + return obj.pytask_meta.markers if isinstance(obj, TaskFunction) else [] def set_marks(obj_or_task: Any | PTask, marks: list[Mark]) -> Any | PTask: """Set marks on a callable or task.""" if isinstance(obj_or_task, PTask): obj_or_task.markers = marks - elif hasattr(obj_or_task, "pytask_meta"): + elif isinstance(obj_or_task, TaskFunction): obj_or_task.pytask_meta.markers = marks else: obj_or_task.pytask_meta = CollectionMetadata(markers=marks) diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py index f44b31cd..0edab0cf 100644 --- a/src/_pytask/task_utils.py +++ b/src/_pytask/task_utils.py @@ -18,6 +18,7 @@ from _pytask.models import CollectionMetadata from _pytask.shared import find_duplicates from _pytask.shared import unwrap_task_function +from _pytask.typing import TaskFunction from _pytask.typing import is_task_function if TYPE_CHECKING: @@ -143,7 +144,7 @@ def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: parsed_name = _parse_name(unwrapped, name) parsed_after = _parse_after(after) - if hasattr(unwrapped, "pytask_meta"): + if isinstance(unwrapped, TaskFunction): unwrapped.pytask_meta.after = parsed_after unwrapped.pytask_meta.is_generator = is_generator unwrapped.pytask_meta.id_ = id @@ -163,7 +164,7 @@ def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: produces=produces, ) - if coiled_kwargs and hasattr(unwrapped, "pytask_meta"): + if coiled_kwargs and isinstance(unwrapped, TaskFunction): unwrapped.pytask_meta.attributes["coiled_kwargs"] = coiled_kwargs # Store it in the global variable ``COLLECTED_TASKS`` to avoid garbage @@ -206,7 +207,7 @@ def _parse_after( if isinstance(after, list): new_after = [] for func in after: - if not hasattr(func, "pytask_meta"): + if not isinstance(func, TaskFunction): func = task()(func) # noqa: PLW2901 new_after.append(func.pytask_meta._id) # type: ignore[attr-defined] return new_after diff --git a/src/_pytask/typing.py b/src/_pytask/typing.py index d433ea06..1ea48759 100644 --- a/src/_pytask/typing.py +++ b/src/_pytask/typing.py @@ -6,12 +6,15 @@ from typing import Any from typing import Final from typing import Literal +from typing import Protocol +from typing import runtime_checkable from attrs import define if TYPE_CHECKING: from typing import TypeAlias + from _pytask.models import CollectionMetadata from pytask import PTask @@ -19,11 +22,24 @@ "NoDefault", "Product", "ProductType", + "TaskFunction", "is_task_function", "no_default", ] +@runtime_checkable +class TaskFunction(Protocol): + """Protocol for functions decorated with @task that have pytask_meta attached.""" + + pytask_meta: CollectionMetadata + __name__: str + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + """Call the task function.""" + ... + + @define(frozen=True) class ProductType: """A class to mark products.""" @@ -34,7 +50,7 @@ class ProductType: def is_task_function(obj: Any) -> bool: - """Check if an object is a task function.""" + """Check if an object could be decorated as a task function.""" return (callable(obj) and hasattr(obj, "__name__")) or ( isinstance(obj, functools.partial) and hasattr(obj.func, "__name__") ) diff --git a/src/pytask/__init__.py b/src/pytask/__init__.py index a71d6646..d994b403 100644 --- a/src/pytask/__init__.py +++ b/src/pytask/__init__.py @@ -74,6 +74,7 @@ from _pytask.task_utils import task from _pytask.traceback import Traceback from _pytask.typing import Product +from _pytask.typing import TaskFunction from _pytask.typing import is_task_function from _pytask.warnings_utils import WarningReport from _pytask.warnings_utils import parse_warning_filter @@ -131,6 +132,7 @@ "State", "Task", "TaskExecutionStatus", + "TaskFunction", "TaskOutcome", "TaskWithoutPath", "Traceback", From 04793cf12993307f1ac2f32274196e3fd4894d43 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 16:29:45 +0100 Subject: [PATCH 05/25] Fix more. --- src/_pytask/capture.py | 4 ++-- src/_pytask/data_catalog.py | 1 + src/_pytask/traceback.py | 15 ++++++++++++--- src/_pytask/tree_util.py | 1 + 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/_pytask/capture.py b/src/_pytask/capture.py index 0e2a47fd..e5ef2571 100644 --- a/src/_pytask/capture.py +++ b/src/_pytask/capture.py @@ -146,7 +146,7 @@ def __init__(self, other: TextIO) -> None: self._other = other super().__init__() - def write(self, s: str) -> int: + def write(self, s: str, /) -> int: # ty: ignore[invalid-method-override] super().write(s) return self._other.write(s) @@ -209,7 +209,7 @@ def truncate(self, size: int | None = None) -> int: # noqa: ARG002 msg = "Cannot truncate stdin." raise UnsupportedOperation(msg) - def write(self, data: str) -> int: # noqa: ARG002 + def write(self, data: str, /) -> int: # noqa: ARG002 # ty: ignore[invalid-method-override] msg = "Cannot write to stdin." raise UnsupportedOperation(msg) diff --git a/src/_pytask/data_catalog.py b/src/_pytask/data_catalog.py index ed50a5b1..8f773b93 100644 --- a/src/_pytask/data_catalog.py +++ b/src/_pytask/data_catalog.py @@ -115,6 +115,7 @@ def add(self, name: str, node: PNode | PProvisionalNode | Any = None) -> None: if node is None: filename = hashlib.sha256(name.encode()).hexdigest() if isinstance(self.default_node, PPathNode): + assert self.path is not None self._entries[name] = self.default_node( name=name, path=self.path / f"{filename}.pkl" ) diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index 72cc699d..0255c17c 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -31,6 +31,8 @@ ] +assert pluggy.__file__ is not None +assert _pytask.__file__ is not None _PLUGGY_DIRECTORY = Path(pluggy.__file__).parent _PYTASK_DIRECTORY = Path(_pytask.__file__).parent @@ -66,9 +68,12 @@ def __rich_console__( # The tracebacks returned by pytask-parallel are strings. if isinstance(filtered_exc_info[2], str): yield filtered_exc_info[2] - else: + elif filtered_exc_info[0] is not None and filtered_exc_info[1] is not None: yield RichTraceback.from_exception( - *filtered_exc_info, show_locals=self.show_locals + filtered_exc_info[0], + filtered_exc_info[1], + filtered_exc_info[2], + show_locals=self.show_locals, ) @@ -99,8 +104,12 @@ def _remove_internal_traceback_frames_from_exc_info( ) if isinstance(exc_info[2], TracebackType): + # If exc_info[2] is TracebackType, we know exc_info is ExceptionInfo, not + # (None, None, None) # noqa: ERA001 + assert exc_info[0] is not None + assert exc_info[1] is not None filtered_traceback = _filter_internal_traceback_frames(exc_info, suppress) - exc_info = (*exc_info[:2], filtered_traceback) + exc_info = (exc_info[0], exc_info[1], filtered_traceback) return exc_info diff --git a/src/_pytask/tree_util.py b/src/_pytask/tree_util.py index 834b0a05..26e7cc6f 100644 --- a/src/_pytask/tree_util.py +++ b/src/_pytask/tree_util.py @@ -23,6 +23,7 @@ "tree_structure", ] +assert optree.__file__ is not None TREE_UTIL_LIB_DIRECTORY = Path(optree.__file__).parent From a84021e66072ccbf766cc0c6cff2f194384e86c4 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 16:55:10 +0100 Subject: [PATCH 06/25] Fix all typing issues. --- src/_pytask/build.py | 19 ++++++++++++++++--- src/_pytask/cache.py | 4 +++- src/_pytask/capture.py | 3 ++- src/_pytask/click.py | 9 +++++---- src/_pytask/coiled_utils.py | 8 ++++---- src/_pytask/collect.py | 2 +- src/_pytask/collect_utils.py | 2 +- src/_pytask/config_utils.py | 8 ++++++-- src/_pytask/console.py | 12 +++++++++--- src/_pytask/dag_command.py | 14 +++++++++++--- src/_pytask/data_catalog.py | 29 +++++++++++++++-------------- src/_pytask/debugging.py | 2 +- src/_pytask/logging.py | 2 +- src/_pytask/mark/expression.py | 2 +- src/_pytask/provisional.py | 7 +++++-- src/_pytask/task_utils.py | 5 +++-- src/_pytask/traceback.py | 4 +++- 17 files changed, 87 insertions(+), 45 deletions(-) diff --git a/src/_pytask/build.py b/src/_pytask/build.py index ea242b59..4832bd78 100644 --- a/src/_pytask/build.py +++ b/src/_pytask/build.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING from typing import Any from typing import Literal +from typing import cast import click @@ -65,7 +66,7 @@ def pytask_unconfigure(session: Session) -> None: path.write_text(json.dumps(HashPathCache._cache)) -def build( # noqa: C901, PLR0912, PLR0913 +def build( # noqa: C901, PLR0912, PLR0913, PLR0915 *, capture: Literal["fd", "no", "sys", "tee-sys"] | CaptureMethod = CaptureMethod.FD, check_casing_of_paths: bool = True, @@ -230,10 +231,22 @@ def build( # noqa: C901, PLR0912, PLR0913 raw_config = {**DEFAULTS_FROM_CLI, **raw_config} - raw_config["paths"] = parse_paths(raw_config["paths"]) + paths_value = raw_config["paths"] + # Type narrow to Path or list for parse_paths + if isinstance(paths_value, Path): + raw_config["paths"] = parse_paths(paths_value) + elif isinstance(paths_value, list): + # Cast to list[Path] for type checker + paths_list = cast("list[Path]", paths_value) + raw_config["paths"] = parse_paths(paths_list) + else: + msg = f"paths must be Path or list, got {type(paths_value)}" + raise TypeError(msg) # noqa: TRY301 if raw_config["config"] is not None: - raw_config["config"] = Path(raw_config["config"]).resolve() + config_value = raw_config["config"] + assert isinstance(config_value, (str, Path)) + raw_config["config"] = Path(config_value).resolve() raw_config["root"] = raw_config["config"].parent else: ( diff --git a/src/_pytask/cache.py b/src/_pytask/cache.py index 5c122833..7d6b879f 100644 --- a/src/_pytask/cache.py +++ b/src/_pytask/cache.py @@ -31,7 +31,9 @@ class Cache: cache_info: CacheInfo = field(factory=CacheInfo) def memoize(self, func: Callable[..., Any]) -> Callable[..., Any]: - prefix = f"{func.__module__}.{func.__name__}:" + func_module = getattr(func, "__module__", "") + func_name = getattr(func, "__name__", "") + prefix = f"{func_module}.{func_name}:" argspec = inspect.getfullargspec(func) @functools.wraps(func) diff --git a/src/_pytask/capture.py b/src/_pytask/capture.py index e5ef2571..fc7aa7c0 100644 --- a/src/_pytask/capture.py +++ b/src/_pytask/capture.py @@ -129,7 +129,8 @@ def mode(self) -> str: # TextIOWrapper doesn't expose a mode, but at least some of our # tests check it. assert hasattr(self.buffer, "mode") - return cast("str", self.buffer.mode.replace("b", "")) + mode_value = cast("str", self.buffer.mode) + return mode_value.replace("b", "") class CaptureIO(io.TextIOWrapper): diff --git a/src/_pytask/click.py b/src/_pytask/click.py index 215a8c05..f5b643e1 100644 --- a/src/_pytask/click.py +++ b/src/_pytask/click.py @@ -37,8 +37,8 @@ if importlib.metadata.version("click") < "8.2": from click.parser import split_opt else: - from click.parser import ( # type: ignore[attr-defined, no-redef, unused-ignore] - _split_opt as split_opt, + from click.parser import ( # type: ignore[attr-defined, no-redef, unused-ignore, unresolved-import] + _split_opt as split_opt, # ty: ignore[unresolved-import] ) @@ -114,7 +114,7 @@ def format_help( else: formatted_name = Text(command_name, style="command") - commands_table.add_row(formatted_name, highlighter(command.help)) + commands_table.add_row(formatted_name, highlighter(command.help or "")) console.print( Panel( @@ -177,12 +177,13 @@ def parse_args(self, ctx: Context, args: list[str]) -> list[str]: _value, args = param.handle_parse_result(ctx, opts, args) if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + args_list = list(args) if not isinstance(args, list) else args ctx.fail( ngettext( "Got unexpected extra argument ({args})", "Got unexpected extra arguments ({args})", len(args), - ).format(args=" ".join(map(str, args))) + ).format(args=" ".join(str(arg) for arg in args_list)) ) ctx.args = args diff --git a/src/_pytask/coiled_utils.py b/src/_pytask/coiled_utils.py index 7643933b..252caefa 100644 --- a/src/_pytask/coiled_utils.py +++ b/src/_pytask/coiled_utils.py @@ -26,9 +26,9 @@ class Function: # type: ignore[no-redef] def extract_coiled_function_kwargs(func: Function) -> dict[str, Any]: """Extract the kwargs for a coiled function.""" return { - "cluster_kwargs": func._cluster_kwargs, + "cluster_kwargs": func._cluster_kwargs, # ty: ignore[possibly-missing-attribute] "keepalive": func.keepalive, - "environ": func._environ, - "local": func._local, - "name": func._name, + "environ": func._environ, # ty: ignore[possibly-missing-attribute] + "local": func._local, # ty: ignore[possibly-missing-attribute] + "name": func._name, # ty: ignore[possibly-missing-attribute] } diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index d19e555b..764af8e7 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -120,7 +120,7 @@ def _collect_from_tasks(session: Session) -> None: raw_task = task_decorator()(raw_task) # noqa: PLW2901 path = get_file(raw_task) - name = raw_task.pytask_meta.name + name = raw_task.pytask_meta.name # ty: ignore[possibly-missing-attribute] if has_mark(raw_task, "task"): # When tasks with @task are passed to the programmatic interface diff --git a/src/_pytask/collect_utils.py b/src/_pytask/collect_utils.py index 141ccc31..d9b7262c 100644 --- a/src/_pytask/collect_utils.py +++ b/src/_pytask/collect_utils.py @@ -358,6 +358,6 @@ def create_name_of_python_node(node_info: NodeInfo) -> str: """Create name of PythonNode.""" node_name = node_info.task_name + "::" + node_info.arg_name if node_info.path: - suffix = "-".join(map(str, node_info.path)) + suffix = "-".join(str(p) for p in node_info.path) node_name += "::" + suffix return node_name diff --git a/src/_pytask/config_utils.py b/src/_pytask/config_utils.py index 64d4b4b0..ce97547c 100644 --- a/src/_pytask/config_utils.py +++ b/src/_pytask/config_utils.py @@ -18,7 +18,7 @@ if sys.version_info >= (3, 11): # pragma: no cover import tomllib else: # pragma: no cover - import tomli as tomllib + import tomli as tomllib # ty: ignore[unresolved-import] __all__ = ["find_project_root_and_config", "read_config", "set_defaults_from_config"] @@ -51,7 +51,11 @@ def set_defaults_from_config( if not context.params["paths"]: context.params["paths"] = (Path.cwd(),) - context.params["paths"] = parse_paths(context.params["paths"]) + paths = context.params["paths"] + # Convert to list if tuple since parse_paths expects Path | list[Path] + if isinstance(paths, tuple): + paths = list(paths) + context.params["paths"] = parse_paths(paths) ( context.params["root"], context.params["config"], diff --git a/src/_pytask/console.py b/src/_pytask/console.py index a0082f4b..8451ff66 100644 --- a/src/_pytask/console.py +++ b/src/_pytask/console.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import TYPE_CHECKING from typing import Any +from typing import cast from rich.console import Console from rich.console import RenderableType @@ -223,14 +224,18 @@ def get_file( # noqa: PLR0911 if hasattr(function, "__wrapped__"): source_file = inspect.getsourcefile(function) if source_file and Path(source_file) in skipped_paths: - return get_file(function.__wrapped__) + wrapped = cast("Callable[..., Any]", function.__wrapped__) + return get_file(wrapped) source_file = inspect.getsourcefile(function) if source_file: # pragma: no cover if "" in source_file or "ipykernel" in source_file: return None if "" in source_file: try: - return Path(function.__globals__["__file__"]).absolute().resolve() + globals_dict = cast( + "dict[str, Any]", getattr(function, "__globals__", {}) + ) + return Path(globals_dict["__file__"]).absolute().resolve() except KeyError: return None return Path(source_file).absolute().resolve() @@ -242,7 +247,8 @@ def _get_source_lines(function: Callable[..., Any]) -> int: if isinstance(function, functools.partial): return _get_source_lines(function.func) if hasattr(function, "__wrapped__"): - return _get_source_lines(function.__wrapped__) + wrapped = cast("Callable[..., Any]", function.__wrapped__) + return _get_source_lines(wrapped) return inspect.getsourcelines(function)[1] diff --git a/src/_pytask/dag_command.py b/src/_pytask/dag_command.py index 796c768f..27d8fc09 100644 --- a/src/_pytask/dag_command.py +++ b/src/_pytask/dag_command.py @@ -153,12 +153,20 @@ def build_dag(raw_config: dict[str, Any]) -> nx.DiGraph: # Add defaults from cli. from _pytask.cli import DEFAULTS_FROM_CLI # noqa: PLC0415 - raw_config = {**DEFAULTS_FROM_CLI, **raw_config} + raw_config = {**DEFAULTS_FROM_CLI, **raw_config} # ty: ignore[invalid-assignment] - raw_config["paths"] = parse_paths(raw_config["paths"]) + paths_value = raw_config["paths"] + # Type narrow to Path or list for parse_paths + if isinstance(paths_value, (Path, list)): + raw_config["paths"] = parse_paths(paths_value) + else: + msg = f"paths must be Path or list, got {type(paths_value)}" + raise TypeError(msg) # noqa: TRY301 if raw_config["config"] is not None: - raw_config["config"] = Path(raw_config["config"]).resolve() + config_value = raw_config["config"] + assert isinstance(config_value, (str, Path)) + raw_config["config"] = Path(config_value).resolve() raw_config["root"] = raw_config["config"].parent else: ( diff --git a/src/_pytask/data_catalog.py b/src/_pytask/data_catalog.py index 8f773b93..3f88a5f3 100644 --- a/src/_pytask/data_catalog.py +++ b/src/_pytask/data_catalog.py @@ -10,12 +10,11 @@ import inspect import pickle import re +from dataclasses import dataclass +from dataclasses import field from pathlib import Path from typing import Any -from attrs import define -from attrs import field - from _pytask.config_utils import find_project_root_and_config from _pytask.data_catalog_utils import DATA_CATALOG_NAME_FIELD from _pytask.exceptions import NodeNotCollectedError @@ -40,7 +39,7 @@ def _get_parent_path_of_data_catalog_module(stacklevel: int = 2) -> Path: return Path.cwd() -@define(kw_only=True) +@dataclass(kw_only=True) class DataCatalog: """A data catalog. @@ -61,28 +60,30 @@ class DataCatalog: """ default_node: type[PNode] = PickleNode - name: str = field(default="default") + name: str = "default" path: Path | None = None - _entries: dict[str, PNode | PProvisionalNode] = field(factory=dict) - _instance_path: Path = field(factory=_get_parent_path_of_data_catalog_module) + _entries: dict[str, PNode | PProvisionalNode] = field(default_factory=dict) + _instance_path: Path = field( + default_factory=_get_parent_path_of_data_catalog_module + ) _session_config: dict[str, Any] = field( - factory=lambda *x: {"check_casing_of_paths": True} # noqa: ARG005 + default_factory=lambda: {"check_casing_of_paths": True} ) - @name.validator - def _check(self, attribute: str, value: str) -> None: # noqa: ARG002 + def __post_init__(self) -> None: + # Validate name _rich_traceback_omit = True - if not isinstance(value, str): + if not isinstance(self.name, str): msg = "The name of a data catalog must be a string." raise TypeError(msg) - if not re.match(r"[a-zA-Z0-9-_]+", value): + if not re.match(r"[a-zA-Z0-9-_]+", self.name): msg = ( "The name of a data catalog must be a string containing only letters, " "numbers, hyphens, and underscores." ) raise ValueError(msg) - def __attrs_post_init__(self) -> None: + # Initialize paths and load persisted nodes root_path, _ = find_project_root_and_config((self._instance_path,)) self._session_config["paths"] = (root_path,) @@ -143,6 +144,6 @@ def add(self, name: str, node: PNode | PProvisionalNode | Any = None) -> None: node = self._entries[name] if hasattr(node, "attributes"): - node.attributes[DATA_CATALOG_NAME_FIELD] = self.name + node.attributes[DATA_CATALOG_NAME_FIELD] = self.name # ty: ignore[invalid-assignment] else: warn_about_upcoming_attributes_field_on_nodes() diff --git a/src/_pytask/debugging.py b/src/_pytask/debugging.py index 85e1dc8b..b986171f 100644 --- a/src/_pytask/debugging.py +++ b/src/_pytask/debugging.py @@ -114,7 +114,7 @@ def pytask_post_parse(config: dict[str, Any]) -> None: PytaskPDB._saved.append( (pdb.set_trace, PytaskPDB._pluginmanager, PytaskPDB._config) ) - pdb.set_trace = PytaskPDB.set_trace + pdb.set_trace = PytaskPDB.set_trace # ty: ignore[invalid-assignment] PytaskPDB._pluginmanager = config["pm"] PytaskPDB._config = config diff --git a/src/_pytask/logging.py b/src/_pytask/logging.py index ce4bd6cb..cb309b88 100644 --- a/src/_pytask/logging.py +++ b/src/_pytask/logging.py @@ -137,7 +137,7 @@ def _format_duration(duration: float) -> str: i for i in duration_tuples if i[1] not in ("second", "seconds") ] - return ", ".join([" ".join(map(str, i)) for i in duration_tuples]) + return ", ".join([" ".join(str(x) for x in i) for i in duration_tuples]) def _humanize_time( # noqa: C901, PLR0912 diff --git a/src/_pytask/mark/expression.py b/src/_pytask/mark/expression.py index 20d3f902..ec415b9a 100644 --- a/src/_pytask/mark/expression.py +++ b/src/_pytask/mark/expression.py @@ -180,7 +180,7 @@ def not_expr(s: Scanner) -> ast.expr: if ident: return ast.Name(IDENT_PREFIX + ident.value, ast.Load()) s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) - return None + return None # ty: ignore[invalid-return-type] # Unreachable: reject() raises class MatcherAdapter(Mapping[str, bool]): diff --git a/src/_pytask/provisional.py b/src/_pytask/provisional.py index 85c355c7..48e3b29b 100644 --- a/src/_pytask/provisional.py +++ b/src/_pytask/provisional.py @@ -103,11 +103,14 @@ def pytask_execute_task(session: Session, task: PTask) -> None: session.hook.pytask_collect_modify_tasks( session=session, tasks=session.tasks ) + # Append the last collection report after successful modification + if report: + session.collection_reports.append(report) except Exception: # noqa: BLE001 # pragma: no cover - report = ExecutionReport.from_task_and_exception( + exec_report = ExecutionReport.from_task_and_exception( task=task, exc_info=sys.exc_info() ) - session.collection_reports.append(report) + session.execution_reports.append(exec_report) recreate_dag(session, task) diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py index 0edab0cf..b5265a23 100644 --- a/src/_pytask/task_utils.py +++ b/src/_pytask/task_utils.py @@ -257,15 +257,16 @@ def _parse_tasks_with_preliminary_names( def _parse_task(task: Callable[..., Any]) -> tuple[str, Callable[..., Any]]: """Parse a single task.""" meta = task.pytask_meta # type: ignore[attr-defined] + task_name = getattr(task, "__name__", "_") - if meta.name is None and task.__name__ == "_": + if meta.name is None and task_name == "_": msg = ( "A task function either needs 'name' passed by the ``@task`` " "decorator or the function name of the task function must not be '_'." ) raise ValueError(msg) - parsed_name = task.__name__ if meta.name is None else meta.name + parsed_name = task_name if meta.name is None else meta.name parsed_kwargs = _parse_task_kwargs(meta.kwargs) signature_kwargs = parse_keyword_arguments_from_signature_defaults(task) diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index 0255c17c..dbe7174f 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -108,7 +108,9 @@ def _remove_internal_traceback_frames_from_exc_info( # (None, None, None) # noqa: ERA001 assert exc_info[0] is not None assert exc_info[1] is not None - filtered_traceback = _filter_internal_traceback_frames(exc_info, suppress) + # Create properly typed tuple for type checker + exception_info: ExceptionInfo = (exc_info[0], exc_info[1], exc_info[2]) + filtered_traceback = _filter_internal_traceback_frames(exception_info, suppress) exc_info = (exc_info[0], exc_info[1], filtered_traceback) return exc_info From c87914feb2a395115d0aae724ac3a32091646357 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 18:07:19 +0100 Subject: [PATCH 07/25] Remove signature changes. --- src/_pytask/capture.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytask/capture.py b/src/_pytask/capture.py index fc7aa7c0..c4201b1f 100644 --- a/src/_pytask/capture.py +++ b/src/_pytask/capture.py @@ -147,7 +147,7 @@ def __init__(self, other: TextIO) -> None: self._other = other super().__init__() - def write(self, s: str, /) -> int: # ty: ignore[invalid-method-override] + def write(self, s: str) -> int: # ty: ignore[invalid-method-override] super().write(s) return self._other.write(s) @@ -210,7 +210,7 @@ def truncate(self, size: int | None = None) -> int: # noqa: ARG002 msg = "Cannot truncate stdin." raise UnsupportedOperation(msg) - def write(self, data: str, /) -> int: # noqa: ARG002 # ty: ignore[invalid-method-override] + def write(self, data: str) -> int: # noqa: ARG002 # ty: ignore[invalid-method-override] msg = "Cannot write to stdin." raise UnsupportedOperation(msg) From 8977f34777fb7fe57c9c54e1e23f58164f8fab66 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 18:48:02 +0100 Subject: [PATCH 08/25] Fix.2 --- src/_pytask/build.py | 18 +++++++++--------- src/_pytask/config_utils.py | 8 ++++++-- src/_pytask/dag_command.py | 15 ++++++++++----- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/_pytask/build.py b/src/_pytask/build.py index 4832bd78..83b4c3bd 100644 --- a/src/_pytask/build.py +++ b/src/_pytask/build.py @@ -232,20 +232,20 @@ def build( # noqa: C901, PLR0912, PLR0913, PLR0915 raw_config = {**DEFAULTS_FROM_CLI, **raw_config} paths_value = raw_config["paths"] - # Type narrow to Path or list for parse_paths - if isinstance(paths_value, Path): - raw_config["paths"] = parse_paths(paths_value) - elif isinstance(paths_value, list): - # Cast to list[Path] for type checker - paths_list = cast("list[Path]", paths_value) - raw_config["paths"] = parse_paths(paths_list) - else: + # Convert tuple to list since parse_paths expects Path | list[Path] + if isinstance(paths_value, tuple): + paths_value = list(paths_value) + if not isinstance(paths_value, (Path, list)): msg = f"paths must be Path or list, got {type(paths_value)}" raise TypeError(msg) # noqa: TRY301 + # Cast is justified - we validated at runtime + raw_config["paths"] = parse_paths(cast("Path | list[Path]", paths_value)) if raw_config["config"] is not None: config_value = raw_config["config"] - assert isinstance(config_value, (str, Path)) + if not isinstance(config_value, (str, Path)): + msg = f"config must be str or Path, got {type(config_value)}" + raise TypeError(msg) # noqa: TRY301 raw_config["config"] = Path(config_value).resolve() raw_config["root"] = raw_config["config"].parent else: diff --git a/src/_pytask/config_utils.py b/src/_pytask/config_utils.py index ce97547c..7d1101c3 100644 --- a/src/_pytask/config_utils.py +++ b/src/_pytask/config_utils.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING from typing import Any +from typing import cast import click @@ -52,10 +53,13 @@ def set_defaults_from_config( context.params["paths"] = (Path.cwd(),) paths = context.params["paths"] - # Convert to list if tuple since parse_paths expects Path | list[Path] if isinstance(paths, tuple): paths = list(paths) - context.params["paths"] = parse_paths(paths) + if not isinstance(paths, (Path, list)): + msg = f"paths must be Path or list, got {type(paths)}" + raise TypeError(msg) + # Cast is justified - we validated at runtime + context.params["paths"] = parse_paths(cast("Path | list[Path]", paths)) ( context.params["root"], context.params["config"], diff --git a/src/_pytask/dag_command.py b/src/_pytask/dag_command.py index 27d8fc09..7e209622 100644 --- a/src/_pytask/dag_command.py +++ b/src/_pytask/dag_command.py @@ -6,6 +6,7 @@ import sys from pathlib import Path from typing import Any +from typing import cast import click import networkx as nx @@ -156,16 +157,20 @@ def build_dag(raw_config: dict[str, Any]) -> nx.DiGraph: raw_config = {**DEFAULTS_FROM_CLI, **raw_config} # ty: ignore[invalid-assignment] paths_value = raw_config["paths"] - # Type narrow to Path or list for parse_paths - if isinstance(paths_value, (Path, list)): - raw_config["paths"] = parse_paths(paths_value) - else: + # Convert tuple to list since parse_paths expects Path | list[Path] + if isinstance(paths_value, tuple): + paths_value = list(paths_value) + if not isinstance(paths_value, (Path, list)): msg = f"paths must be Path or list, got {type(paths_value)}" raise TypeError(msg) # noqa: TRY301 + # Cast is justified - we validated at runtime + raw_config["paths"] = parse_paths(cast("Path | list[Path]", paths_value)) if raw_config["config"] is not None: config_value = raw_config["config"] - assert isinstance(config_value, (str, Path)) + if not isinstance(config_value, (str, Path)): + msg = f"config must be str or Path, got {type(config_value)}" + raise TypeError(msg) # noqa: TRY301 raw_config["config"] = Path(config_value).resolve() raw_config["root"] = raw_config["config"].parent else: From 9d66522716fd4d923d6a136a513214cd29cf636b Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 21:24:12 +0100 Subject: [PATCH 09/25] Fix. --- src/_pytask/typing.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/_pytask/typing.py b/src/_pytask/typing.py index 1ea48759..097156ac 100644 --- a/src/_pytask/typing.py +++ b/src/_pytask/typing.py @@ -30,10 +30,14 @@ @runtime_checkable class TaskFunction(Protocol): - """Protocol for functions decorated with @task that have pytask_meta attached.""" + """Protocol for callables decorated with @task that have pytask_meta attached. + + Note: This includes regular functions, functools.partial objects, and any other + callable that has been decorated with @task and has pytask_meta attached. + We don't require __name__ to support functools.partial. + """ pytask_meta: CollectionMetadata - __name__: str def __call__(self, *args: Any, **kwargs: Any) -> Any: """Call the task function.""" From 916bdd358788f86ea72204c43287c0d14c3fc226 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 21:29:28 +0100 Subject: [PATCH 10/25] Fix. --- .github/workflows/main.yml | 4 ++++ CHANGELOG.md | 1 + 2 files changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 241b5bbd..686babd0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,10 @@ jobs: enable-cache: true - name: Install just uses: extractions/setup-just@v3 + - name: Install graphviz + run: | + sudo apt-get update + sudo apt-get install graphviz graphviz-dev - run: just typing run-tests: diff --git a/CHANGELOG.md b/CHANGELOG.md index 36613b99..10be33a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and default pickle protocol. - {pull}`???` adapts the interactive debugger integration to Python 3.14's updated `pdb` behaviour and keeps pytest-style capturing intact. +- {pull}`734` migrates from mypy to ty for type checking. ## 0.5.7 - 2025-11-22 From 6bf575e286d7dcae57479eea1edf82eea938745f Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 22 Dec 2025 21:36:01 +0100 Subject: [PATCH 11/25] Fix. --- pyproject.toml | 4 +++- uv.lock | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 40ebb1f6..31dbf877 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,10 +73,12 @@ test = [ "pytest-xdist>=3.6.1", "syrupy>=4.5.0", "aiohttp>=3.11.0", # For HTTPPath tests. +] +typing = [ + "ty>=0.0.5", "coiled>=1.42.0", "cloudpickle>=3.0.0", ] -typing = ["ty>=0.0.5"] [project.urls] Changelog = "https://pytask-dev.readthedocs.io/en/stable/changes.html" diff --git a/uv.lock b/uv.lock index 38651248..8430a6d6 100644 --- a/uv.lock +++ b/uv.lock @@ -2697,8 +2697,6 @@ plugin-list = [ ] test = [ { name = "aiohttp" }, - { name = "cloudpickle" }, - { name = "coiled" }, { name = "deepdiff" }, { name = "nbmake" }, { name = "pexpect" }, @@ -2709,6 +2707,8 @@ test = [ { name = "syrupy" }, ] typing = [ + { name = "cloudpickle" }, + { name = "coiled" }, { name = "ty" }, ] @@ -2752,8 +2752,6 @@ plugin-list = [ ] test = [ { name = "aiohttp", specifier = ">=3.11.0" }, - { name = "cloudpickle", specifier = ">=3.0.0" }, - { name = "coiled", specifier = ">=1.42.0" }, { name = "deepdiff", specifier = ">=7.0.0" }, { name = "nbmake", specifier = ">=1.5.5" }, { name = "pexpect", specifier = ">=4.9.0" }, @@ -2763,7 +2761,11 @@ test = [ { name = "pytest-xdist", specifier = ">=3.6.1" }, { name = "syrupy", specifier = ">=4.5.0" }, ] -typing = [{ name = "ty", specifier = ">=0.0.5" }] +typing = [ + { name = "cloudpickle", specifier = ">=3.0.0" }, + { name = "coiled", specifier = ">=1.42.0" }, + { name = "ty", specifier = ">=0.0.5" }, +] [[package]] name = "pytest" From 89edbb41df9b58d1f07df5b9007e9d994ef8a3b9 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 23 Dec 2025 11:29:08 +0100 Subject: [PATCH 12/25] More fixes for tests.! --- justfile | 2 +- pyproject.toml | 8 ++++++++ src/_pytask/cache.py | 22 +++++++++++++++++++--- tests/test_cache.py | 4 +++- tests/test_capture.py | 2 +- tests/test_collect.py | 13 ++++++++----- tests/test_collect_command.py | 14 +++++++------- 7 files changed, 47 insertions(+), 18 deletions(-) diff --git a/justfile b/justfile index 10c25b1b..22f71be1 100644 --- a/justfile +++ b/justfile @@ -16,7 +16,7 @@ test-nb: # Run type checking typing: - uv run --group typing ty check src + uv run --group typing ty check # Run linting lint: diff --git a/pyproject.toml b/pyproject.toml index 31dbf877..4236ea4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -170,6 +170,14 @@ filterwarnings = [ ] [tool.ty.src] +include = [ + "tests/test_build.py", + "tests/test_cache.py", + "tests/test_capture.py", + "tests/test_clean.py", + "tests/test_cli.py", + "tests/test_collect.py", +] exclude = ["src/_pytask/_hashlib.py"] [tool.ty.terminal] diff --git a/src/_pytask/cache.py b/src/_pytask/cache.py index 7d6b879f..7af8b592 100644 --- a/src/_pytask/cache.py +++ b/src/_pytask/cache.py @@ -8,6 +8,9 @@ from inspect import FullArgSpec from typing import TYPE_CHECKING from typing import Any +from typing import ParamSpec +from typing import Protocol +from typing import TypeVar from attrs import define from attrs import field @@ -17,6 +20,19 @@ if TYPE_CHECKING: from collections.abc import Callable +P = ParamSpec("P") +R = TypeVar("R") + + +class MemoizedCallable(Protocol[P, R]): + """A callable that has been memoized and has a cache attribute.""" + + cache: Cache + + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: + """Call the memoized function.""" + ... + @define class CacheInfo: @@ -30,14 +46,14 @@ class Cache: _sentinel: Any = field(factory=object) cache_info: CacheInfo = field(factory=CacheInfo) - def memoize(self, func: Callable[..., Any]) -> Callable[..., Any]: + def memoize(self, func: Callable[P, R]) -> MemoizedCallable[P, R]: func_module = getattr(func, "__module__", "") func_name = getattr(func, "__name__", "") prefix = f"{func_module}.{func_name}:" argspec = inspect.getfullargspec(func) @functools.wraps(func) - def wrapped(*args: Any, **kwargs: Any) -> Callable[..., Any]: + def wrapped(*args: P.args, **kwargs: P.kwargs) -> R: key = _make_memoize_key( args, kwargs, typed=False, argspec=argspec, prefix=prefix ) @@ -54,7 +70,7 @@ def wrapped(*args: Any, **kwargs: Any) -> Callable[..., Any]: wrapped.cache = self # type: ignore[attr-defined] - return wrapped + return wrapped # type: ignore[return-value] def add(self, key: str, value: Any) -> None: self._cache[key] = value diff --git a/tests/test_cache.py b/tests/test_cache.py index 218120f3..335623fc 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -36,7 +36,9 @@ def test_cache_add(): def func(a): return a - prefix = f"{func.__module__}.{func.__name__}:" + func_module = getattr(func, "__module__", "") + func_name = getattr(func, "__name__", "") + prefix = f"{func_module}.{func_name}:" argspec = inspect.getfullargspec(func) key = _make_memoize_key((1,), {}, typed=False, argspec=argspec, prefix=prefix) cache.add(key, 1) diff --git a/tests/test_capture.py b/tests/test_capture.py index a48c3ca3..afb25d60 100644 --- a/tests/test_capture.py +++ b/tests/test_capture.py @@ -556,7 +556,7 @@ def test_simple_resume_suspend(self): # Should not crash with missing "_old". assert repr(cap.syscapture) == ( " _state='done' tmpfile={!r}>".format( # noqa: UP032 - cap.syscapture.tmpfile + cap.syscapture.tmpfile # type: ignore[union-attr] ) ) diff --git a/tests/test_collect.py b/tests/test_collect.py index 0bb12ed9..36b27bc0 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -143,7 +143,7 @@ def test_error_with_invalid_file_name_pattern(runner, tmp_path): def test_error_with_invalid_file_name_pattern_(tmp_path): - session = build(paths=tmp_path, task_files=[1]) + session = build(paths=tmp_path, task_files=[1]) # type: ignore[arg-type] assert session.exit_code == ExitCode.CONFIGURATION_FAILED @@ -248,7 +248,9 @@ def test_find_shortest_uniquely_identifiable_names_for_tasks(tmp_path): for base_name in ("base_name_ident_0", "base_name_ident_1"): task = Task( - base_name=base_name, path=path_identifiable_by_base_name, function=None + base_name=base_name, + path=path_identifiable_by_base_name, + function=None, # type: ignore[arg-type] ) tasks.append(task) expected[task.name] = "t.py::" + base_name @@ -258,7 +260,7 @@ def test_find_shortest_uniquely_identifiable_names_for_tasks(tmp_path): for module in ("t.py", "m.py"): module_path = dir_identifiable_by_module_name / module - task = Task(base_name="task_a", path=module_path, function=None) + task = Task(base_name="task_a", path=module_path, function=None) # type: ignore[arg-type] tasks.append(task) expected[task.name] = module + "::task_a" @@ -270,7 +272,7 @@ def test_find_shortest_uniquely_identifiable_names_for_tasks(tmp_path): for base_path in (dir_identifiable_by_folder_a, dir_identifiable_by_folder_b): module_path = base_path / "t.py" - task = Task(base_name="task_t", path=module_path, function=None) + task = Task(base_name="task_t", path=module_path, function=None) # type: ignore[arg-type] tasks.append(task) expected[task.name] = base_path.name + "/t.py::task_t" @@ -308,7 +310,8 @@ def test_collect_tasks_from_modules_with_the_same_name(tmp_path): for report in session.collection_reports ) assert { - report.node.function.__module__ for report in session.collection_reports + report.node.function.__module__ # type: ignore[union-attr] + for report in session.collection_reports } == {"a.task_module", "b.task_module"} diff --git a/tests/test_collect_command.py b/tests/test_collect_command.py index a615648c..75fa0c56 100644 --- a/tests/test_collect_command.py +++ b/tests/test_collect_command.py @@ -321,8 +321,8 @@ def test_print_collected_tasks_without_nodes(capsys): base_name="function", path=Path("task_path.py"), function=function, - depends_on={0: Node("in.txt")}, - produces={0: Node("out.txt")}, + depends_on={"depends_on": Node("in.txt")}, # type: ignore[arg-type] + produces={"produces": Node("out.txt")}, # type: ignore[arg-type] ) ] } @@ -343,8 +343,8 @@ def test_print_collected_tasks_with_nodes(capsys): base_name="function", path=Path("task_path.py"), function=function, - depends_on={"depends_on": PathNode(name="in.txt", path=Path("in.txt"))}, - produces={0: PathNode(name="out.txt", path=Path("out.txt"))}, + depends_on={"depends_on": PathNode(name="in.txt", path=Path("in.txt"))}, # type: ignore[arg-type] + produces={"produces": PathNode(name="out.txt", path=Path("out.txt"))}, # type: ignore[arg-type] ) ] } @@ -366,11 +366,11 @@ def test_find_common_ancestor_of_all_nodes(show_nodes, expected_add): base_name="function", path=Path.cwd() / "src" / "task_path.py", function=function, - depends_on={ + depends_on={ # type: ignore[arg-type] "depends_on": PathNode.from_path(Path.cwd() / "src" / "in.txt") }, - produces={ - 0: PathNode.from_path( + produces={ # type: ignore[arg-type] + "produces": PathNode.from_path( Path.cwd().joinpath("..", "bld", "out.txt").resolve() ) }, From bc912a39998a0394a1d4e2e06218c3941b8feeef Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 26 Dec 2025 10:04:41 +0100 Subject: [PATCH 13/25] Remove more type errors. --- src/_pytask/tree_util.py | 17 ++++++++++++++++- tests/test_collect_command.py | 14 +++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/_pytask/tree_util.py b/src/_pytask/tree_util.py index 26e7cc6f..ee8d137a 100644 --- a/src/_pytask/tree_util.py +++ b/src/_pytask/tree_util.py @@ -4,9 +4,11 @@ import functools from pathlib import Path +from typing import TYPE_CHECKING +from typing import Any +from typing import TypeVar import optree -from optree import PyTree from optree import tree_flatten_with_path as _optree_tree_flatten_with_path from optree import tree_leaves as _optree_tree_leaves from optree import tree_map as _optree_tree_map @@ -23,6 +25,19 @@ "tree_structure", ] +_T = TypeVar("_T") + +if TYPE_CHECKING: + # Use our own recursive type alias for static type checking. + # optree's PyTree uses __class_getitem__ to generate Union types at runtime, + # but type checkers like ty cannot evaluate these dynamic types properly. + # See: https://github.com/metaopt/optree/issues/251 + PyTree = ( + _T | tuple["PyTree[_T]", ...] | list["PyTree[_T]"] | dict[Any, "PyTree[_T]"] + ) +else: + from optree import PyTree + assert optree.__file__ is not None TREE_UTIL_LIB_DIRECTORY = Path(optree.__file__).parent diff --git a/tests/test_collect_command.py b/tests/test_collect_command.py index 75fa0c56..0a4c76b5 100644 --- a/tests/test_collect_command.py +++ b/tests/test_collect_command.py @@ -306,7 +306,7 @@ def task_example_2(path=Path("in_2.txt"), produces=Path("out_2.txt")): ... @define class Node: - path: Path + path: str def state(self): ... @@ -321,8 +321,8 @@ def test_print_collected_tasks_without_nodes(capsys): base_name="function", path=Path("task_path.py"), function=function, - depends_on={"depends_on": Node("in.txt")}, # type: ignore[arg-type] - produces={"produces": Node("out.txt")}, # type: ignore[arg-type] + depends_on={"depends_on": Node("in.txt")}, + produces={"produces": Node("out.txt")}, ) ] } @@ -343,8 +343,8 @@ def test_print_collected_tasks_with_nodes(capsys): base_name="function", path=Path("task_path.py"), function=function, - depends_on={"depends_on": PathNode(name="in.txt", path=Path("in.txt"))}, # type: ignore[arg-type] - produces={"produces": PathNode(name="out.txt", path=Path("out.txt"))}, # type: ignore[arg-type] + depends_on={"depends_on": PathNode(name="in.txt", path=Path("in.txt"))}, + produces={"produces": PathNode(name="out.txt", path=Path("out.txt"))}, ) ] } @@ -366,10 +366,10 @@ def test_find_common_ancestor_of_all_nodes(show_nodes, expected_add): base_name="function", path=Path.cwd() / "src" / "task_path.py", function=function, - depends_on={ # type: ignore[arg-type] + depends_on={ "depends_on": PathNode.from_path(Path.cwd() / "src" / "in.txt") }, - produces={ # type: ignore[arg-type] + produces={ "produces": PathNode.from_path( Path.cwd().joinpath("..", "bld", "out.txt").resolve() ) From 809dd22ade231aef250124b10a781d684e5d3a33 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 26 Dec 2025 10:13:02 +0100 Subject: [PATCH 14/25] Fix:2 --- src/_pytask/cache.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_pytask/cache.py b/src/_pytask/cache.py index 7af8b592..6eea72aa 100644 --- a/src/_pytask/cache.py +++ b/src/_pytask/cache.py @@ -25,7 +25,11 @@ class MemoizedCallable(Protocol[P, R]): - """A callable that has been memoized and has a cache attribute.""" + """A callable that has been memoized and has a cache attribute. + + Note: We intentionally don't include __name__ or __module__ in the protocol + because not all callables have these attributes (e.g., functools.partial). + """ cache: Cache @@ -69,7 +73,6 @@ def wrapped(*args: P.args, **kwargs: P.kwargs) -> R: return value wrapped.cache = self # type: ignore[attr-defined] - return wrapped # type: ignore[return-value] def add(self, key: str, value: Any) -> None: From c7793f234ba382d94ee16c99bd099e7897ff272d Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 26 Dec 2025 10:15:07 +0100 Subject: [PATCH 15/25] Fix. --- src/_pytask/cache.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/_pytask/cache.py b/src/_pytask/cache.py index 6eea72aa..7af8b592 100644 --- a/src/_pytask/cache.py +++ b/src/_pytask/cache.py @@ -25,11 +25,7 @@ class MemoizedCallable(Protocol[P, R]): - """A callable that has been memoized and has a cache attribute. - - Note: We intentionally don't include __name__ or __module__ in the protocol - because not all callables have these attributes (e.g., functools.partial). - """ + """A callable that has been memoized and has a cache attribute.""" cache: Cache @@ -73,6 +69,7 @@ def wrapped(*args: P.args, **kwargs: P.kwargs) -> R: return value wrapped.cache = self # type: ignore[attr-defined] + return wrapped # type: ignore[return-value] def add(self, key: str, value: Any) -> None: From 1c8a78ceec99c1bbe6bda8bc19e859cf0f12af0b Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 28 Dec 2025 16:45:30 +0100 Subject: [PATCH 16/25] Ignore type errors. --- justfile | 2 +- pyproject.toml | 8 --- tests/test_collect.py | 6 +- tests/test_compat.py | 8 +-- tests/test_console.py | 2 +- tests/test_dag.py | 4 +- tests/test_dag_command.py | 2 +- tests/test_dag_utils.py | 20 +++---- tests/test_data_catalog.py | 2 +- tests/test_database.py | 8 +-- tests/test_debugging.py | 12 ++-- tests/test_execute.py | 6 +- .../test_functional_interface.ipynb | 6 +- ...functional_interface_w_relative_path.ipynb | 6 +- tests/test_jupyter/test_task_generator.ipynb | 2 +- tests/test_mark.py | 4 +- tests/test_mark_utils.py | 20 +++---- tests/test_nodes.py | 4 +- tests/test_outcomes.py | 4 +- tests/test_path.py | 6 +- tests/test_persist.py | 24 ++++---- tests/test_profile.py | 2 +- tests/test_skipping.py | 58 +++++++++---------- tests/test_task.py | 2 +- tests/test_task_utils.py | 14 ++--- tests/test_traceback.py | 7 ++- 26 files changed, 118 insertions(+), 121 deletions(-) diff --git a/justfile b/justfile index 22f71be1..6431b00c 100644 --- a/justfile +++ b/justfile @@ -16,7 +16,7 @@ test-nb: # Run type checking typing: - uv run --group typing ty check + uv run --group typing --group test ty check src/ tests/ # Run linting lint: diff --git a/pyproject.toml b/pyproject.toml index 4236ea4b..31dbf877 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -170,14 +170,6 @@ filterwarnings = [ ] [tool.ty.src] -include = [ - "tests/test_build.py", - "tests/test_cache.py", - "tests/test_capture.py", - "tests/test_clean.py", - "tests/test_cli.py", - "tests/test_collect.py", -] exclude = ["src/_pytask/_hashlib.py"] [tool.ty.terminal] diff --git a/tests/test_collect.py b/tests/test_collect.py index 36b27bc0..bfa34dfb 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -294,7 +294,7 @@ def task_example(path_in = Path("in.txt"), produces = Path("out.txt")): assert session.exit_code == ExitCode.OK assert len(session.tasks) == 1 - assert session.tasks[0].depends_on["path_in"].path == tmp_path.joinpath("in.txt") + assert session.tasks[0].depends_on["path_in"].path == tmp_path.joinpath("in.txt") # type: ignore[union-attr] def test_collect_tasks_from_modules_with_the_same_name(tmp_path): @@ -366,7 +366,7 @@ def task_example( session = build(paths=tmp_path) assert session.exit_code == ExitCode.OK product = session.tasks[0].produces["path"] - assert product.name == "product" + assert product.name == "product" # type: ignore[union-attr] def test_error_when_dependency_is_defined_in_kwargs_and_annotation(runner, tmp_path): @@ -488,7 +488,7 @@ def task_example() -> Annotated[str, {node}]: session = build(paths=tmp_path) assert session.exit_code == ExitCode.OK assert tmp_path.joinpath("file.txt").exists() - assert session.tasks[0].produces["return"].name == tmp_path.name + "/file.txt" + assert session.tasks[0].produces["return"].name == tmp_path.name + "/file.txt" # type: ignore[union-attr] def test_error_when_return_annotation_cannot_be_parsed(runner, tmp_path): diff --git a/tests/test_compat.py b/tests/test_compat.py index df4aaf9f..d418cfc5 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -97,7 +97,7 @@ def test_sqlalchemy_version_fallback(): def test_bad_version(monkeypatch): name = "fakemodule" module = types.ModuleType(name) - module.__version__ = "0.9.0" + module.__version__ = "0.9.0" # type: ignore[attr-defined] sys.modules[name] = module monkeypatch.setitem(_MINIMUM_VERSIONS, name, "1.0.0") @@ -113,7 +113,7 @@ def test_bad_version(monkeypatch): result = import_optional_dependency("fakemodule", errors="warn") assert result is None - module.__version__ = "1.0.0" # exact match is OK + module.__version__ = "1.0.0" # type: ignore[attr-defined] # exact match is OK result = import_optional_dependency("fakemodule") assert result is module @@ -122,7 +122,7 @@ def test_submodule(monkeypatch): # Create a fake module with a submodule name = "fakemodule" module = types.ModuleType(name) - module.__version__ = "0.9.0" + module.__version__ = "0.9.0" # type: ignore[attr-defined] sys.modules[name] = module sub_name = "submodule" submodule = types.ModuleType(sub_name) @@ -138,7 +138,7 @@ def test_submodule(monkeypatch): result = import_optional_dependency("fakemodule.submodule", errors="warn") assert result is None - module.__version__ = "1.0.0" # exact match is OK + module.__version__ = "1.0.0" # type: ignore[attr-defined] # exact match is OK result = import_optional_dependency("fakemodule.submodule") assert result is submodule diff --git a/tests/test_console.py b/tests/test_console.py index ecaa3718..19e7840d 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -197,7 +197,7 @@ def test_reduce_node_name(node, paths, expectation, expected): _THIS_FILE, ), (lambda x: x, None, Path(__file__)), - (__unknown_lambda, None, Path(__file__)), # noqa: F821 + (__unknown_lambda, None, Path(__file__)), # type: ignore[unresolved-reference] # noqa: F821 ], ) def test_get_file(task_func, skipped_paths, expected): diff --git a/tests/test_dag.py b/tests/test_dag.py index 5503511f..b85a9a03 100644 --- a/tests/test_dag.py +++ b/tests/test_dag.py @@ -20,8 +20,8 @@ def test_create_dag(): task = Task( base_name="task_dummy", path=root, - function=None, - depends_on={ + function=None, # type: ignore[arg-type] + depends_on={ # type: ignore[arg-type] 0: PathNode.from_path(root / "node_1"), 1: PathNode.from_path(root / "node_2"), }, diff --git a/tests/test_dag_command.py b/tests/test_dag_command.py index d56927fc..1fab52e1 100644 --- a/tests/test_dag_command.py +++ b/tests/test_dag_command.py @@ -12,7 +12,7 @@ from pytask import cli try: - import pygraphviz # noqa: F401 + import pygraphviz # type: ignore[import-not-found] # noqa: F401 except ImportError: # pragma: no cover _IS_PYGRAPHVIZ_INSTALLED = False else: diff --git a/tests/test_dag_utils.py b/tests/test_dag_utils.py index a33c04c7..95f480fc 100644 --- a/tests/test_dag_utils.py +++ b/tests/test_dag_utils.py @@ -20,8 +20,8 @@ def dag(): """Create a dag with five nodes in a line.""" dag = nx.DiGraph() for i in range(4): - task = Task(base_name=str(i), path=Path(), function=None) - next_task = Task(base_name=str(i + 1), path=Path(), function=None) + task = Task(base_name=str(i), path=Path(), function=None) # type: ignore[arg-type] + next_task = Task(base_name=str(i + 1), path=Path(), function=None) # type: ignore[arg-type] dag.add_node(task.signature, task=task) dag.add_node(next_task.signature, task=next_task) dag.add_edge(task.signature, next_task.signature) @@ -84,7 +84,7 @@ def test_node_and_neighbors(dag): Task( base_name="1", path=Path(), - function=None, + function=None, # type: ignore[arg-type] markers=[Mark("try_last", (), {})], ) ], @@ -97,7 +97,7 @@ def test_node_and_neighbors(dag): Task( base_name="1", path=Path(), - function=None, + function=None, # type: ignore[arg-type] markers=[Mark("try_first", (), {})], ) ], @@ -106,7 +106,7 @@ def test_node_and_neighbors(dag): id="test try_first", ), pytest.param( - [Task(base_name="1", path=Path(), function=None, markers=[])], + [Task(base_name="1", path=Path(), function=None, markers=[])], # type: ignore[arg-type] does_not_raise(), {"c12d8d4f7e2e3128d27878d1fb3d8e3583e90e68000a13634dfbf21f4d1456f3": 0}, id="test no priority", @@ -116,14 +116,14 @@ def test_node_and_neighbors(dag): Task( base_name="1", path=Path(), - function=None, + function=None, # type: ignore[arg-type] markers=[Mark("try_first", (), {})], ), - Task(base_name="2", path=Path(), function=None, markers=[]), + Task(base_name="2", path=Path(), function=None, markers=[]), # type: ignore[arg-type] Task( base_name="3", path=Path(), - function=None, + function=None, # type: ignore[arg-type] markers=[Mark("try_last", (), {})], ), ], @@ -172,8 +172,8 @@ def test_instantiate_sorter_from_other_sorter(dag): scheduler.done(task_name) assert scheduler._nodes_done == {name_to_sig[name] for name in (".::0", ".::1")} - task = Task(base_name="5", path=Path(), function=None) - dag.add_node(task.signature, task=Task(base_name="5", path=Path(), function=None)) + task = Task(base_name="5", path=Path(), function=None) # type: ignore[arg-type] + dag.add_node(task.signature, task=Task(base_name="5", path=Path(), function=None)) # type: ignore[arg-type] dag.add_edge(name_to_sig[".::4"], task.signature) new_scheduler = TopologicalSorter.from_dag_and_sorter(dag, scheduler) diff --git a/tests/test_data_catalog.py b/tests/test_data_catalog.py index 4ada810c..04851628 100644 --- a/tests/test_data_catalog.py +++ b/tests/test_data_catalog.py @@ -184,7 +184,7 @@ def task_add_content() -> Annotated[str, data_catalog["new_content"]]: def test_error_when_name_of_node_is_not_string(): data_catalog = DataCatalog() with pytest.raises(TypeError, match="The name of a catalog entry"): - data_catalog.add(True, Path("file.txt")) + data_catalog.add(True, Path("file.txt")) # type: ignore[arg-type] def test_requesting_new_node_with_python_node_as_default(): diff --git a/tests/test_database.py b/tests/test_database.py index 70981d91..045ea134 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -31,7 +31,7 @@ def task_write(path=Path("in.txt"), produces=Path("out.txt")): assert session.exit_code == ExitCode.OK create_database( - make_url( + make_url( # type: ignore[arg-type] "sqlite:///" + tmp_path.joinpath(".pytask", "pytask.sqlite3").as_posix() ) ) @@ -39,15 +39,15 @@ def task_write(path=Path("in.txt"), produces=Path("out.txt")): with DatabaseSession() as db_session: task_id = session.tasks[0].signature out_path = tmp_path.joinpath("out.txt") - in_id = session.tasks[0].depends_on["path"].signature - out_id = session.tasks[0].produces["produces"].signature + in_id = session.tasks[0].depends_on["path"].signature # type: ignore[union-attr] + out_id = session.tasks[0].produces["produces"].signature # type: ignore[union-attr] for id_, path in ( (task_id, task_path), (in_id, in_path), (out_id, out_path), ): - hash_ = db_session.get(State, (task_id, id_)).hash_ + hash_ = db_session.get(State, (task_id, id_)).hash_ # type: ignore[union-attr] assert hash_ == hash_path(path, path.stat().st_mtime) diff --git a/tests/test_debugging.py b/tests/test_debugging.py index f06e9984..600848cc 100644 --- a/tests/test_debugging.py +++ b/tests/test_debugging.py @@ -40,7 +40,7 @@ def _escape_ansi(line): ) def test_capture_callback(value, expected, expectation): with expectation: - result = _pdbcls_callback(None, None, value) + result = _pdbcls_callback(None, None, value) # type: ignore[arg-type] assert result == expected @@ -214,7 +214,7 @@ def task_1(): child = pexpect.spawn(f"pytask {tmp_path.as_posix()}") child.expect("== my_header ==") - assert "PDB set_trace" not in child.before.decode() + assert "PDB set_trace" not in child.before.decode() # type: ignore[union-attr] child.expect("Pdb") child.sendline("c") rest = child.read().decode("utf-8") @@ -385,16 +385,16 @@ def task_1(): child.expect(r"\n\(\(Pdb") child.sendline("c") child.expect("LEAVING RECURSIVE DEBUGGER") - assert b"PDB continue" not in child.before + assert b"PDB continue" not in child.before # type: ignore[operator] # No extra newline. - assert child.before.endswith(b"c\r\nprint_from_foo\r\n") + assert child.before.endswith(b"c\r\nprint_from_foo\r\n") # type: ignore[union-attr] # set_debug should not raise outcomes. Exit, if used recursively. child.sendline("debug 42") child.sendline("q") child.expect("LEAVING RECURSIVE DEBUGGER") - assert b"ENTERING RECURSIVE DEBUGGER" in child.before - assert b"Quitting debugger" not in child.before + assert b"ENTERING RECURSIVE DEBUGGER" in child.before # type: ignore[operator] + assert b"Quitting debugger" not in child.before # type: ignore[operator] child.sendline("c") child.expect(["PDB", "continue", r"\(IO-capturing", r"resumed\)"]) diff --git a/tests/test_execute.py b/tests/test_execute.py index 5a2c659c..d63b809c 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -49,7 +49,7 @@ def task_example(produces=Path("out.txt")): ... assert session.exit_code == ExitCode.FAILED assert len(session.execution_reports) == 1 - assert isinstance(session.execution_reports[0].exc_info[1], NodeNotFoundError) + assert isinstance(session.execution_reports[0].exc_info[1], NodeNotFoundError) # type: ignore[index] def test_task_did_not_produce_multiple_nodes_and_all_are_shown(runner, tmp_path): @@ -116,7 +116,7 @@ def task_3(paths = [Path("deleted.txt"), Path("out_2.txt")]): report = session.execution_reports[2] assert report.outcome == TaskOutcome.FAIL - assert isinstance(report.exc_info[1], NodeNotFoundError) + assert isinstance(report.exc_info[1], NodeNotFoundError) # type: ignore[index] def test_depends_on_and_produces_can_be_used_in_task(tmp_path): @@ -656,7 +656,7 @@ def task_example(): pass def test_pass_non_task_to_functional_api_that_are_ignored(): - session = pytask.build(tasks=None) + session = pytask.build(tasks=None) # type: ignore[arg-type] assert len(session.tasks) == 0 diff --git a/tests/test_jupyter/test_functional_interface.ipynb b/tests/test_jupyter/test_functional_interface.ipynb index 47c3ffde..1d8f304b 100644 --- a/tests/test_jupyter/test_functional_interface.ipynb +++ b/tests/test_jupyter/test_functional_interface.ipynb @@ -27,14 +27,14 @@ "\n", "\n", "def create_text() -> Annotated[int, node_text]:\n", - " return \"This is the text.\"\n", + " return \"This is the text.\" # type: ignore[return-value]\n", "\n", "\n", "node_file = PathNode.from_path(Path(\"file.txt\").resolve())\n", "\n", "\n", "def create_file(text: Annotated[int, node_text]) -> Annotated[str, node_file]:\n", - " return text" + " return text # type: ignore[return-value]" ] }, { @@ -44,7 +44,7 @@ "metadata": {}, "outputs": [], "source": [ - "session = pytask.build(tasks=[create_file, create_text])\n", + "session = pytask.build(tasks=[create_file, create_text]) # type: ignore[arg-type]\n", "assert session.exit_code == ExitCode.OK" ] } diff --git a/tests/test_jupyter/test_functional_interface_w_relative_path.ipynb b/tests/test_jupyter/test_functional_interface_w_relative_path.ipynb index 6e9a40e2..e7a0b592 100644 --- a/tests/test_jupyter/test_functional_interface_w_relative_path.ipynb +++ b/tests/test_jupyter/test_functional_interface_w_relative_path.ipynb @@ -27,14 +27,14 @@ "\n", "\n", "def create_text() -> Annotated[int, node_text]:\n", - " return \"This is the text.\"\n", + " return \"This is the text.\" # type: ignore[return-value]\n", "\n", "\n", "node_file = PathNode(name=\"product\", path=Path(\"file.txt\"))\n", "\n", "\n", "def create_file(text: Annotated[int, node_text]) -> Annotated[str, node_file]:\n", - " return text" + " return text # type: ignore[return-value]" ] }, { @@ -44,7 +44,7 @@ "metadata": {}, "outputs": [], "source": [ - "session = pytask.build(tasks=[create_file, create_text])\n", + "session = pytask.build(tasks=[create_file, create_text]) # type: ignore[arg-type]\n", "assert session.exit_code == ExitCode.OK" ] } diff --git a/tests/test_jupyter/test_task_generator.ipynb b/tests/test_jupyter/test_task_generator.ipynb index cb922e56..b291658d 100644 --- a/tests/test_jupyter/test_task_generator.ipynb +++ b/tests/test_jupyter/test_task_generator.ipynb @@ -37,7 +37,7 @@ "):\n", " for path in paths:\n", "\n", - " @task\n", + " @task # type: ignore[arg-type]\n", " def task_copy(\n", " path: Path = path,\n", " ) -> Annotated[str, path.with_name(path.stem + \"-copy.txt\")]:\n", diff --git a/tests/test_mark.py b/tests/test_mark.py index badf9792..83d629bd 100644 --- a/tests/test_mark.py +++ b/tests/test_mark.py @@ -20,7 +20,7 @@ def test_mark_exists_in_pytask_namespace(attribute): def test_pytask_mark_notcallable() -> None: mark = MarkGenerator() with pytest.raises(TypeError): - mark() + mark() # type: ignore[call-non-callable] @pytest.mark.filterwarnings("ignore:Unknown pytask.mark.foo") @@ -223,7 +223,7 @@ def test_keyword_option_wrong_arguments( tmp_path.joinpath("task_module.py").write_text( textwrap.dedent("def task_func(arg): pass") ) - session = build(paths=tmp_path, **{option: expr}) + session = build(paths=tmp_path, **{option: expr}) # type: ignore[arg-type] assert session.exit_code == ExitCode.DAG_FAILED captured = capsys.readouterr() diff --git a/tests/test_mark_utils.py b/tests/test_mark_utils.py index a916af51..5aa91cb9 100644 --- a/tests/test_mark_utils.py +++ b/tests/test_mark_utils.py @@ -29,7 +29,7 @@ ], ) def test_get_all_marks_from_task(markers, expected): - task = Task(base_name="name", path=Path(), function=None, markers=markers) + task = Task(base_name="name", path=Path(), function=None, markers=markers) # type: ignore[arg-type] result = get_all_marks(task) assert result == expected @@ -53,7 +53,7 @@ def test_get_all_marks_from_obj(markers, expected): def func(): ... if markers is not None: - func.pytask_meta = CollectionMetadata(markers=markers) + func.pytask_meta = CollectionMetadata(markers=markers) # type: ignore[attr-defined] result = get_all_marks(func) assert result == expected @@ -76,7 +76,7 @@ def func(): ... ], ) def test_get_marks_from_task(markers, marker_name, expected): - task = Task(base_name="name", path=Path(), function=None, markers=markers) + task = Task(base_name="name", path=Path(), function=None, markers=markers) # type: ignore[arg-type] result = get_marks(task, marker_name) assert result == expected @@ -102,7 +102,7 @@ def test_get_marks_from_obj(markers, marker_name, expected): def func(): ... if markers is not None: - func.pytask_meta = CollectionMetadata(markers=markers) + func.pytask_meta = CollectionMetadata(markers=markers) # type: ignore[attr-defined] result = get_marks(func, marker_name) assert result == expected @@ -125,7 +125,7 @@ def func(): ... ], ) def test_has_mark_for_task(markers, marker_name, expected): - task = Task(base_name="name", path=Path(), function=None, markers=markers) + task = Task(base_name="name", path=Path(), function=None, markers=markers) # type: ignore[arg-type] result = has_mark(task, marker_name) assert result is expected @@ -147,7 +147,7 @@ def test_has_mark(markers, marker_name, expected): def func(): ... if markers is not None: - func.pytask_meta = CollectionMetadata(markers=markers) + func.pytask_meta = CollectionMetadata(markers=markers) # type: ignore[attr-defined] result = has_mark(func, marker_name) assert result == expected @@ -174,7 +174,7 @@ def func(): ... def test_remove_marks_from_task( markers, marker_name, expected_markers, expected_others ): - task = Task(base_name="name", path=Path(), function=None, markers=markers) + task = Task(base_name="name", path=Path(), function=None, markers=markers) # type: ignore[arg-type] _, result_markers = remove_marks(task, marker_name) assert task.markers == expected_others assert result_markers == expected_markers @@ -205,7 +205,7 @@ def test_remove_marks_from_func( def func(): ... if markers is not None: - func.pytask_meta = CollectionMetadata(markers=markers) + func.pytask_meta = CollectionMetadata(markers=markers) # type: ignore[attr-defined] obj, result_markers = remove_marks(func, marker_name) markers = get_all_marks(obj) @@ -222,7 +222,7 @@ def func(): ... ], ) def test_set_marks_to_task(markers): - task = Task(base_name="name", path=Path(), function=None) + task = Task(base_name="name", path=Path(), function=None) # type: ignore[arg-type] result = set_marks(task, markers) assert result.markers == markers @@ -239,4 +239,4 @@ def test_set_marks_to_obj(markers): def func(): ... result = set_marks(func, markers) - assert result.pytask_meta.markers == markers + assert result.pytask_meta.markers == markers # type: ignore[union-attr] diff --git a/tests/test_nodes.py b/tests/test_nodes.py index cab6a37d..0084349d 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -59,11 +59,11 @@ def test_hash_of_python_node(value, hash_, expected): "7284475a87b8f1aa49c40126c5064269f0ba926265b8fe9158a39a882c6a1512", ), ( - Task(base_name="task", path=Path("task.py"), function=None), + Task(base_name="task", path=Path("task.py"), function=None), # type: ignore[arg-type] "4c96feb6042210c859938d4f6fc835ac1bde64960aeda101d2e2367644f9c22b", ), ( - TaskWithoutPath(name="task", function=None), + TaskWithoutPath(name="task", function=None), # type: ignore[arg-type] "ac80b202671ece4c139a9b2d6e03a499c8b6e016dcd2022ac580fbf1c64fc63b", ), ], diff --git a/tests/test_outcomes.py b/tests/test_outcomes.py index 5e417b20..abf41574 100644 --- a/tests/test_outcomes.py +++ b/tests/test_outcomes.py @@ -11,7 +11,7 @@ @pytest.mark.parametrize("outcome_in_report", CollectionOutcome) def test_count_outcomes_collection(outcome_in_report): - reports = [CollectionReport(outcome_in_report, None, None)] + reports = [CollectionReport(outcome_in_report, None, None)] # type: ignore[arg-type] counts = count_outcomes(reports, CollectionOutcome) @@ -24,7 +24,7 @@ def test_count_outcomes_collection(outcome_in_report): @pytest.mark.parametrize("outcome_in_report", TaskOutcome) def test_count_outcomes_tasks(outcome_in_report): - reports = [ExecutionReport(None, outcome_in_report, None, None)] + reports = [ExecutionReport(None, outcome_in_report, None, None)] # type: ignore[arg-type] counts = count_outcomes(reports, TaskOutcome) diff --git a/tests/test_path.py b/tests/test_path.py index 894ee5e7..23a37e2b 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -9,6 +9,7 @@ from pathlib import PurePosixPath from pathlib import PureWindowsPath from types import ModuleType +from typing import TYPE_CHECKING from typing import Any import pytest @@ -21,6 +22,9 @@ from _pytask.path import relative_to from pytask.path import import_path +if TYPE_CHECKING: + from collections.abc import Generator + @pytest.mark.parametrize( ("path", "source", "include_source", "expected"), @@ -129,7 +133,7 @@ def test_find_case_sensitive_path(tmp_path, path, existing_paths, expected): @pytest.fixture -def simple_module(request, tmp_path: Path) -> Path: +def simple_module(request, tmp_path: Path) -> Generator[Path, None, None]: name = f"mymod_{request.node.name}" fn = tmp_path / f"_src/project/{name}.py" fn.parent.mkdir(parents=True) diff --git a/tests/test_persist.py b/tests/test_persist.py index c5714412..93009214 100644 --- a/tests/test_persist.py +++ b/tests/test_persist.py @@ -62,7 +62,7 @@ def task_dummy(path=Path("in.txt"), produces=Path("out.txt")): assert session.exit_code == ExitCode.OK assert len(session.execution_reports) == 1 assert session.execution_reports[0].outcome == TaskOutcome.PERSISTENCE - assert isinstance(session.execution_reports[0].exc_info[1], Persisted) + assert isinstance(session.execution_reports[0].exc_info[1], Persisted) # type: ignore[index] create_database( "sqlite:///" + tmp_path.joinpath(".pytask", "pytask.sqlite3").as_posix() @@ -70,9 +70,9 @@ def task_dummy(path=Path("in.txt"), produces=Path("out.txt")): with DatabaseSession() as db_session: task_id = session.tasks[0].signature - node_id = session.tasks[0].produces["produces"].signature + node_id = session.tasks[0].produces["produces"].signature # type: ignore[union-attr] - hash_ = db_session.get(State, (task_id, node_id)).hash_ + hash_ = db_session.get(State, (task_id, node_id)).hash_ # type: ignore[union-attr] path = tmp_path.joinpath("out.txt") assert hash_ == hash_path(path, path.stat().st_mtime) @@ -81,7 +81,7 @@ def task_dummy(path=Path("in.txt"), produces=Path("out.txt")): assert session.exit_code == ExitCode.OK assert len(session.execution_reports) == 1 assert session.execution_reports[0].outcome == TaskOutcome.SKIP_UNCHANGED - assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged) + assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged) # type: ignore[index] def test_migrating_a_whole_task_with_persist(tmp_path): @@ -104,7 +104,7 @@ def task_dummy(depends_on=Path("in.txt"), produces=Path("out.txt")): assert session.exit_code == ExitCode.OK assert len(session.execution_reports) == 1 assert session.execution_reports[0].outcome == TaskOutcome.PERSISTENCE - assert isinstance(session.execution_reports[0].exc_info[1], Persisted) + assert isinstance(session.execution_reports[0].exc_info[1], Persisted) # type: ignore[index] @pytest.mark.parametrize( @@ -122,20 +122,20 @@ def test_pytask_execute_task_process_report(monkeypatch, exc_info, expected): ) task = DummyClass() - task.name = None - task.signature = "id" + task.name = None # type: ignore[attr-defined] + task.signature = "id" # type: ignore[attr-defined] session = DummyClass() - session.dag = None + session.dag = None # type: ignore[attr-defined] report = DummyClass() - report.exc_info = exc_info - report.task = task + report.exc_info = exc_info # type: ignore[attr-defined] + report.task = task # type: ignore[attr-defined] - result = pytask_execute_task_process_report(session, report) + result = pytask_execute_task_process_report(session, report) # type: ignore[arg-type] if expected: - assert report.outcome == TaskOutcome.PERSISTENCE + assert report.outcome == TaskOutcome.PERSISTENCE # type: ignore[attr-defined] assert result is True else: assert result is None diff --git a/tests/test_profile.py b/tests/test_profile.py index 0ffde996..eeb8dd41 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -34,7 +34,7 @@ def task_example(): time.sleep(2) with DatabaseSession() as session: runtime = session.get(Runtime, task.signature) - assert runtime.duration > 2 + assert runtime.duration > 2 # type: ignore[union-attr] def test_profile_if_no_tasks_are_collected(tmp_path, runner): diff --git a/tests/test_skipping.py b/tests/test_skipping.py index 64547da6..1e73ee05 100644 --- a/tests/test_skipping.py +++ b/tests/test_skipping.py @@ -34,7 +34,7 @@ def task_dummy(): assert session.execution_reports[0].outcome == TaskOutcome.SUCCESS session = build(paths=tmp_path) - assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged) + assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged) # type: ignore[index] def test_skip_unchanged_w_dependencies_and_products(tmp_path): @@ -55,7 +55,7 @@ def task_dummy(path=Path("in.txt"), produces=Path("out.txt")): session = build(paths=tmp_path) assert session.execution_reports[0].outcome == TaskOutcome.SKIP_UNCHANGED - assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged) + assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged) # type: ignore[index] assert tmp_path.joinpath("out.txt").read_text() == "Original content of in.txt." @@ -73,9 +73,9 @@ def task_second(path=Path("out.txt")): ... session = build(paths=tmp_path) assert session.execution_reports[0].outcome == TaskOutcome.FAIL - assert isinstance(session.execution_reports[0].exc_info[1], Exception) + assert isinstance(session.execution_reports[0].exc_info[1], Exception) # type: ignore[index] assert session.execution_reports[1].outcome == TaskOutcome.SKIP_PREVIOUS_FAILED - assert isinstance(session.execution_reports[1].exc_info[1], SkippedAncestorFailed) + assert isinstance(session.execution_reports[1].exc_info[1], SkippedAncestorFailed) # type: ignore[index] def test_if_skip_decorator_is_applied_to_following_tasks(tmp_path): @@ -94,9 +94,9 @@ def task_second(path=Path("out.txt")): ... session = build(paths=tmp_path) assert session.execution_reports[0].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[0].exc_info[1], Skipped) + assert isinstance(session.execution_reports[0].exc_info[1], Skipped) # type: ignore[index] assert session.execution_reports[1].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[1].exc_info[1], Skipped) + assert isinstance(session.execution_reports[1].exc_info[1], Skipped) # type: ignore[index] @pytest.mark.parametrize( @@ -116,7 +116,7 @@ def task_first(path=Path("in.txt")): session = build(paths=tmp_path) assert session.execution_reports[0].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[0].exc_info[1], Skipped) + assert isinstance(session.execution_reports[0].exc_info[1], Skipped) # type: ignore[index] @pytest.mark.parametrize( @@ -160,16 +160,16 @@ def task_second(path=Path("out.txt")): session = build(paths=tmp_path) node = session.collection_reports[0].node - assert len(node.markers) == 1 - assert node.markers[0].name == "skipif" - assert node.markers[0].args == () - assert node.markers[0].kwargs == {"condition": True, "reason": "bla"} + assert len(node.markers) == 1 # type: ignore[union-attr] + assert node.markers[0].name == "skipif" # type: ignore[union-attr] + assert node.markers[0].args == () # type: ignore[union-attr] + assert node.markers[0].kwargs == {"condition": True, "reason": "bla"} # type: ignore[union-attr] assert session.execution_reports[0].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[0].exc_info[1], Skipped) + assert isinstance(session.execution_reports[0].exc_info[1], Skipped) # type: ignore[index] assert session.execution_reports[1].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[1].exc_info[1], Skipped) - assert session.execution_reports[0].exc_info[1].args[0] == "bla" + assert isinstance(session.execution_reports[1].exc_info[1], Skipped) # type: ignore[index] + assert session.execution_reports[0].exc_info[1].args[0] == "bla" # type: ignore[index] def test_if_skipif_decorator_is_applied_execute(tmp_path): @@ -188,10 +188,10 @@ def task_second(path=Path("out.txt")): ... session = build(paths=tmp_path) node = session.collection_reports[0].node - assert len(node.markers) == 1 - assert node.markers[0].name == "skipif" - assert node.markers[0].args == (False,) - assert node.markers[0].kwargs == {"reason": "bla"} + assert len(node.markers) == 1 # type: ignore[union-attr] + assert node.markers[0].name == "skipif" # type: ignore[union-attr] + assert node.markers[0].args == (False,) # type: ignore[union-attr] + assert node.markers[0].kwargs == {"reason": "bla"} # type: ignore[union-attr] assert session.execution_reports[0].outcome == TaskOutcome.SUCCESS assert session.execution_reports[0].exc_info is None assert session.execution_reports[1].outcome == TaskOutcome.SUCCESS @@ -216,19 +216,19 @@ def task_second(path=Path("out.txt")): session = build(paths=tmp_path) node = session.collection_reports[0].node - assert len(node.markers) == 2 - assert node.markers[0].name == "skipif" - assert node.markers[0].args == () - assert node.markers[0].kwargs == {"condition": True, "reason": "No, I am not."} - assert node.markers[1].name == "skipif" - assert node.markers[1].args == () - assert node.markers[1].kwargs == {"condition": False, "reason": "I am fine"} + assert len(node.markers) == 2 # type: ignore[union-attr] + assert node.markers[0].name == "skipif" # type: ignore[union-attr] + assert node.markers[0].args == () # type: ignore[union-attr] + assert node.markers[0].kwargs == {"condition": True, "reason": "No, I am not."} # type: ignore[union-attr] + assert node.markers[1].name == "skipif" # type: ignore[union-attr] + assert node.markers[1].args == () # type: ignore[union-attr] + assert node.markers[1].kwargs == {"condition": False, "reason": "I am fine"} # type: ignore[union-attr] assert session.execution_reports[0].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[0].exc_info[1], Skipped) + assert isinstance(session.execution_reports[0].exc_info[1], Skipped) # type: ignore[index] assert session.execution_reports[1].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[1].exc_info[1], Skipped) - assert session.execution_reports[0].exc_info[1].args[0] == "No, I am not." + assert isinstance(session.execution_reports[1].exc_info[1], Skipped) # type: ignore[index] + assert session.execution_reports[0].exc_info[1].args[0] == "No, I am not." # type: ignore[index] @pytest.mark.parametrize( @@ -243,7 +243,7 @@ def task_second(path=Path("out.txt")): ) def test_pytask_execute_task_setup(marker_name, force, expectation): session = Session.from_config({"force": force}) - task = Task(base_name="task", path=Path(), function=None) + task = Task(base_name="task", path=Path(), function=None) # type: ignore[arg-type] kwargs = {"reason": ""} if marker_name == "skip_ancestor_failed" else {} task.markers = [Mark(marker_name, (), kwargs)] diff --git a/tests/test_task.py b/tests/test_task.py index d7294b47..ee199424 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -438,7 +438,7 @@ def task_func(i=i): session = build(paths=tmp_path) assert session.exit_code == ExitCode.COLLECTION_FAILED - assert isinstance(session.collection_reports[0].exc_info[1], ValueError) + assert isinstance(session.collection_reports[0].exc_info[1], ValueError) # type: ignore[index] def test_task_receives_unknown_kwarg(runner, tmp_path): diff --git a/tests/test_task_utils.py b/tests/test_task_utils.py index d642a26c..8d7b56a7 100644 --- a/tests/test_task_utils.py +++ b/tests/test_task_utils.py @@ -67,13 +67,13 @@ def test_default_values_of_pytask_meta(): @task() def task_example(): ... - assert task_example.pytask_meta.after == [] - assert not task_example.pytask_meta.is_generator - assert task_example.pytask_meta.id_ is None - assert task_example.pytask_meta.kwargs == {} - assert task_example.pytask_meta.markers == [Mark("task", (), {})] - assert task_example.pytask_meta.name == "task_example" - assert task_example.pytask_meta.produces is None + assert task_example.pytask_meta.after == [] # type: ignore[attr-defined] + assert not task_example.pytask_meta.is_generator # type: ignore[attr-defined] + assert task_example.pytask_meta.id_ is None # type: ignore[attr-defined] + assert task_example.pytask_meta.kwargs == {} # type: ignore[attr-defined] + assert task_example.pytask_meta.markers == [Mark("task", (), {})] # type: ignore[attr-defined] + assert task_example.pytask_meta.name == "task_example" # type: ignore[attr-defined] + assert task_example.pytask_meta.produces is None # type: ignore[attr-defined] # Remove collected task. COLLECTED_TASKS.pop(Path(__file__)) diff --git a/tests/test_traceback.py b/tests/test_traceback.py index ada8ea54..aac075c6 100644 --- a/tests/test_traceback.py +++ b/tests/test_traceback.py @@ -46,14 +46,15 @@ def helper(): def test_render_traceback_with_string_traceback(): - traceback = Traceback((Exception, Exception("Help"), "String traceback.")) - rendered = render_to_string(traceback, console) + traceback = Traceback((Exception, Exception("Help"), "String traceback.")) # type: ignore[arg-type] + rendered = render_to_string(traceback, console) # type: ignore[arg-type] assert "String traceback." in rendered def test_passing_show_locals(): traceback = Traceback( - (Exception, Exception("Help"), "String traceback."), show_locals=True + (Exception, Exception("Help"), "String traceback."), + show_locals=True, # type: ignore[arg-type] ) assert traceback.show_locals is True # Also tests that the class variable has been reset. From 6f1baba92b1146b09c42fff22aeae563d27c8487 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 28 Dec 2025 17:00:15 +0100 Subject: [PATCH 17/25] Remove some ignores. --- tests/test_execute.py | 8 +++++-- tests/test_persist.py | 12 ++++++++--- tests/test_skipping.py | 48 ++++++++++++++++++++++++++++++----------- tests/test_task.py | 4 +++- tests/test_traceback.py | 4 ++-- 5 files changed, 55 insertions(+), 21 deletions(-) diff --git a/tests/test_execute.py b/tests/test_execute.py index d63b809c..6e76db2d 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -49,7 +49,9 @@ def task_example(produces=Path("out.txt")): ... assert session.exit_code == ExitCode.FAILED assert len(session.execution_reports) == 1 - assert isinstance(session.execution_reports[0].exc_info[1], NodeNotFoundError) # type: ignore[index] + exc_info = session.execution_reports[0].exc_info + assert exc_info is not None + assert isinstance(exc_info[1], NodeNotFoundError) def test_task_did_not_produce_multiple_nodes_and_all_are_shown(runner, tmp_path): @@ -116,7 +118,9 @@ def task_3(paths = [Path("deleted.txt"), Path("out_2.txt")]): report = session.execution_reports[2] assert report.outcome == TaskOutcome.FAIL - assert isinstance(report.exc_info[1], NodeNotFoundError) # type: ignore[index] + exc_info = report.exc_info + assert exc_info is not None + assert isinstance(exc_info[1], NodeNotFoundError) def test_depends_on_and_produces_can_be_used_in_task(tmp_path): diff --git a/tests/test_persist.py b/tests/test_persist.py index 93009214..477d461c 100644 --- a/tests/test_persist.py +++ b/tests/test_persist.py @@ -62,7 +62,9 @@ def task_dummy(path=Path("in.txt"), produces=Path("out.txt")): assert session.exit_code == ExitCode.OK assert len(session.execution_reports) == 1 assert session.execution_reports[0].outcome == TaskOutcome.PERSISTENCE - assert isinstance(session.execution_reports[0].exc_info[1], Persisted) # type: ignore[index] + exc_info = session.execution_reports[0].exc_info + assert exc_info is not None + assert isinstance(exc_info[1], Persisted) create_database( "sqlite:///" + tmp_path.joinpath(".pytask", "pytask.sqlite3").as_posix() @@ -81,7 +83,9 @@ def task_dummy(path=Path("in.txt"), produces=Path("out.txt")): assert session.exit_code == ExitCode.OK assert len(session.execution_reports) == 1 assert session.execution_reports[0].outcome == TaskOutcome.SKIP_UNCHANGED - assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged) # type: ignore[index] + exc_info2 = session.execution_reports[0].exc_info + assert exc_info2 is not None + assert isinstance(exc_info2[1], SkippedUnchanged) def test_migrating_a_whole_task_with_persist(tmp_path): @@ -104,7 +108,9 @@ def task_dummy(depends_on=Path("in.txt"), produces=Path("out.txt")): assert session.exit_code == ExitCode.OK assert len(session.execution_reports) == 1 assert session.execution_reports[0].outcome == TaskOutcome.PERSISTENCE - assert isinstance(session.execution_reports[0].exc_info[1], Persisted) # type: ignore[index] + exc_info = session.execution_reports[0].exc_info + assert exc_info is not None + assert isinstance(exc_info[1], Persisted) @pytest.mark.parametrize( diff --git a/tests/test_skipping.py b/tests/test_skipping.py index 1e73ee05..ae7f4b87 100644 --- a/tests/test_skipping.py +++ b/tests/test_skipping.py @@ -34,7 +34,9 @@ def task_dummy(): assert session.execution_reports[0].outcome == TaskOutcome.SUCCESS session = build(paths=tmp_path) - assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged) # type: ignore[index] + exc_info = session.execution_reports[0].exc_info + assert exc_info is not None + assert isinstance(exc_info[1], SkippedUnchanged) def test_skip_unchanged_w_dependencies_and_products(tmp_path): @@ -55,7 +57,9 @@ def task_dummy(path=Path("in.txt"), produces=Path("out.txt")): session = build(paths=tmp_path) assert session.execution_reports[0].outcome == TaskOutcome.SKIP_UNCHANGED - assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged) # type: ignore[index] + exc_info = session.execution_reports[0].exc_info + assert exc_info is not None + assert isinstance(exc_info[1], SkippedUnchanged) assert tmp_path.joinpath("out.txt").read_text() == "Original content of in.txt." @@ -73,9 +77,13 @@ def task_second(path=Path("out.txt")): ... session = build(paths=tmp_path) assert session.execution_reports[0].outcome == TaskOutcome.FAIL - assert isinstance(session.execution_reports[0].exc_info[1], Exception) # type: ignore[index] + exc_info_0 = session.execution_reports[0].exc_info + assert exc_info_0 is not None + assert isinstance(exc_info_0[1], Exception) assert session.execution_reports[1].outcome == TaskOutcome.SKIP_PREVIOUS_FAILED - assert isinstance(session.execution_reports[1].exc_info[1], SkippedAncestorFailed) # type: ignore[index] + exc_info_1 = session.execution_reports[1].exc_info + assert exc_info_1 is not None + assert isinstance(exc_info_1[1], SkippedAncestorFailed) def test_if_skip_decorator_is_applied_to_following_tasks(tmp_path): @@ -94,9 +102,13 @@ def task_second(path=Path("out.txt")): ... session = build(paths=tmp_path) assert session.execution_reports[0].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[0].exc_info[1], Skipped) # type: ignore[index] + exc_info_0 = session.execution_reports[0].exc_info + assert exc_info_0 is not None + assert isinstance(exc_info_0[1], Skipped) assert session.execution_reports[1].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[1].exc_info[1], Skipped) # type: ignore[index] + exc_info_1 = session.execution_reports[1].exc_info + assert exc_info_1 is not None + assert isinstance(exc_info_1[1], Skipped) @pytest.mark.parametrize( @@ -116,7 +128,9 @@ def task_first(path=Path("in.txt")): session = build(paths=tmp_path) assert session.execution_reports[0].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[0].exc_info[1], Skipped) # type: ignore[index] + exc_info = session.execution_reports[0].exc_info + assert exc_info is not None + assert isinstance(exc_info[1], Skipped) @pytest.mark.parametrize( @@ -166,10 +180,14 @@ def task_second(path=Path("out.txt")): assert node.markers[0].kwargs == {"condition": True, "reason": "bla"} # type: ignore[union-attr] assert session.execution_reports[0].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[0].exc_info[1], Skipped) # type: ignore[index] + exc_info_0 = session.execution_reports[0].exc_info + assert exc_info_0 is not None + assert isinstance(exc_info_0[1], Skipped) assert session.execution_reports[1].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[1].exc_info[1], Skipped) # type: ignore[index] - assert session.execution_reports[0].exc_info[1].args[0] == "bla" # type: ignore[index] + exc_info_1 = session.execution_reports[1].exc_info + assert exc_info_1 is not None + assert isinstance(exc_info_1[1], Skipped) + assert exc_info_0[1].args[0] == "bla" def test_if_skipif_decorator_is_applied_execute(tmp_path): @@ -225,10 +243,14 @@ def task_second(path=Path("out.txt")): assert node.markers[1].kwargs == {"condition": False, "reason": "I am fine"} # type: ignore[union-attr] assert session.execution_reports[0].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[0].exc_info[1], Skipped) # type: ignore[index] + exc_info_0 = session.execution_reports[0].exc_info + assert exc_info_0 is not None + assert isinstance(exc_info_0[1], Skipped) assert session.execution_reports[1].outcome == TaskOutcome.SKIP - assert isinstance(session.execution_reports[1].exc_info[1], Skipped) # type: ignore[index] - assert session.execution_reports[0].exc_info[1].args[0] == "No, I am not." # type: ignore[index] + exc_info_1 = session.execution_reports[1].exc_info + assert exc_info_1 is not None + assert isinstance(exc_info_1[1], Skipped) + assert exc_info_0[1].args[0] == "No, I am not." @pytest.mark.parametrize( diff --git a/tests/test_task.py b/tests/test_task.py index ee199424..b60a0195 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -438,7 +438,9 @@ def task_func(i=i): session = build(paths=tmp_path) assert session.exit_code == ExitCode.COLLECTION_FAILED - assert isinstance(session.collection_reports[0].exc_info[1], ValueError) # type: ignore[index] + exc_info = session.collection_reports[0].exc_info + assert exc_info is not None + assert isinstance(exc_info[1], ValueError) def test_task_receives_unknown_kwarg(runner, tmp_path): diff --git a/tests/test_traceback.py b/tests/test_traceback.py index aac075c6..30df0b73 100644 --- a/tests/test_traceback.py +++ b/tests/test_traceback.py @@ -53,8 +53,8 @@ def test_render_traceback_with_string_traceback(): def test_passing_show_locals(): traceback = Traceback( - (Exception, Exception("Help"), "String traceback."), - show_locals=True, # type: ignore[arg-type] + (Exception, Exception("Help"), "String traceback."), # type: ignore[arg-type] + show_locals=True, ) assert traceback.show_locals is True # Also tests that the class variable has been reset. From a9fc1018393c24eb42174e5f01d11f6cacd011ed Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 28 Dec 2025 17:19:26 +0100 Subject: [PATCH 18/25] Less ignores. --- tests/conftest.py | 4 ++++ tests/test_collect.py | 30 ++++++++++++++++++++---------- tests/test_dag.py | 3 ++- tests/test_dag_utils.py | 9 +++++---- tests/test_database.py | 12 +++++++++--- tests/test_mark_utils.py | 11 ++++++----- tests/test_persist.py | 8 ++++++-- tests/test_profile.py | 3 ++- tests/test_skipping.py | 11 +++++------ 9 files changed, 59 insertions(+), 32 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index aa50807d..59b07b36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,3 +157,7 @@ def enter_directory(path: Path): yield finally: os.chdir(old_cwd) + + +def noop() -> None: + """A no-op function for use in tests that need a Task with a function.""" diff --git a/tests/test_collect.py b/tests/test_collect.py index bfa34dfb..910c18ca 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -16,6 +16,7 @@ from pytask import Task from pytask import build from pytask import cli +from tests.conftest import noop @pytest.mark.parametrize( @@ -250,7 +251,7 @@ def test_find_shortest_uniquely_identifiable_names_for_tasks(tmp_path): task = Task( base_name=base_name, path=path_identifiable_by_base_name, - function=None, # type: ignore[arg-type] + function=noop, ) tasks.append(task) expected[task.name] = "t.py::" + base_name @@ -260,7 +261,7 @@ def test_find_shortest_uniquely_identifiable_names_for_tasks(tmp_path): for module in ("t.py", "m.py"): module_path = dir_identifiable_by_module_name / module - task = Task(base_name="task_a", path=module_path, function=None) # type: ignore[arg-type] + task = Task(base_name="task_a", path=module_path, function=noop) tasks.append(task) expected[task.name] = module + "::task_a" @@ -272,7 +273,7 @@ def test_find_shortest_uniquely_identifiable_names_for_tasks(tmp_path): for base_path in (dir_identifiable_by_folder_a, dir_identifiable_by_folder_b): module_path = base_path / "t.py" - task = Task(base_name="task_t", path=module_path, function=None) # type: ignore[arg-type] + task = Task(base_name="task_t", path=module_path, function=noop) tasks.append(task) expected[task.name] = base_path.name + "/t.py::task_t" @@ -294,7 +295,9 @@ def task_example(path_in = Path("in.txt"), produces = Path("out.txt")): assert session.exit_code == ExitCode.OK assert len(session.tasks) == 1 - assert session.tasks[0].depends_on["path_in"].path == tmp_path.joinpath("in.txt") # type: ignore[union-attr] + depends_on = session.tasks[0].depends_on + assert depends_on is not None + assert depends_on["path_in"].path == tmp_path.joinpath("in.txt") # type: ignore[union-attr] def test_collect_tasks_from_modules_with_the_same_name(tmp_path): @@ -309,10 +312,13 @@ def test_collect_tasks_from_modules_with_the_same_name(tmp_path): report.outcome == CollectionOutcome.SUCCESS for report in session.collection_reports ) - assert { - report.node.function.__module__ # type: ignore[union-attr] - for report in session.collection_reports - } == {"a.task_module", "b.task_module"} + modules = set() + for report in session.collection_reports: + node = report.node + assert node is not None + assert node.function is not None # type: ignore[union-attr] + modules.add(node.function.__module__) # type: ignore[union-attr] + assert modules == {"a.task_module", "b.task_module"} def test_collect_module_name(tmp_path): @@ -365,7 +371,9 @@ def task_example( session = build(paths=tmp_path) assert session.exit_code == ExitCode.OK - product = session.tasks[0].produces["path"] + produces = session.tasks[0].produces + assert produces is not None + product = produces["path"] assert product.name == "product" # type: ignore[union-attr] @@ -488,7 +496,9 @@ def task_example() -> Annotated[str, {node}]: session = build(paths=tmp_path) assert session.exit_code == ExitCode.OK assert tmp_path.joinpath("file.txt").exists() - assert session.tasks[0].produces["return"].name == tmp_path.name + "/file.txt" # type: ignore[union-attr] + produces = session.tasks[0].produces + assert produces is not None + assert produces["return"].name == tmp_path.name + "/file.txt" # type: ignore[union-attr] def test_error_when_return_annotation_cannot_be_parsed(runner, tmp_path): diff --git a/tests/test_dag.py b/tests/test_dag.py index b85a9a03..61288616 100644 --- a/tests/test_dag.py +++ b/tests/test_dag.py @@ -12,6 +12,7 @@ from pytask import Task from pytask import build from pytask import cli +from tests.conftest import noop @pytest.mark.skipif(sys.platform == "win32", reason="Hashes match only on unix.") @@ -20,7 +21,7 @@ def test_create_dag(): task = Task( base_name="task_dummy", path=root, - function=None, # type: ignore[arg-type] + function=noop, depends_on={ # type: ignore[arg-type] 0: PathNode.from_path(root / "node_1"), 1: PathNode.from_path(root / "node_2"), diff --git a/tests/test_dag_utils.py b/tests/test_dag_utils.py index 95f480fc..d11f2af2 100644 --- a/tests/test_dag_utils.py +++ b/tests/test_dag_utils.py @@ -13,6 +13,7 @@ from _pytask.dag_utils import task_and_descending_tasks from pytask import Mark from pytask import Task +from tests.conftest import noop @pytest.fixture @@ -20,8 +21,8 @@ def dag(): """Create a dag with five nodes in a line.""" dag = nx.DiGraph() for i in range(4): - task = Task(base_name=str(i), path=Path(), function=None) # type: ignore[arg-type] - next_task = Task(base_name=str(i + 1), path=Path(), function=None) # type: ignore[arg-type] + task = Task(base_name=str(i), path=Path(), function=noop) + next_task = Task(base_name=str(i + 1), path=Path(), function=noop) dag.add_node(task.signature, task=task) dag.add_node(next_task.signature, task=next_task) dag.add_edge(task.signature, next_task.signature) @@ -172,8 +173,8 @@ def test_instantiate_sorter_from_other_sorter(dag): scheduler.done(task_name) assert scheduler._nodes_done == {name_to_sig[name] for name in (".::0", ".::1")} - task = Task(base_name="5", path=Path(), function=None) # type: ignore[arg-type] - dag.add_node(task.signature, task=Task(base_name="5", path=Path(), function=None)) # type: ignore[arg-type] + task = Task(base_name="5", path=Path(), function=noop) + dag.add_node(task.signature, task=Task(base_name="5", path=Path(), function=noop)) dag.add_edge(name_to_sig[".::4"], task.signature) new_scheduler = TopologicalSorter.from_dag_and_sorter(dag, scheduler) diff --git a/tests/test_database.py b/tests/test_database.py index 045ea134..d8ab5fd1 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -39,15 +39,21 @@ def task_write(path=Path("in.txt"), produces=Path("out.txt")): with DatabaseSession() as db_session: task_id = session.tasks[0].signature out_path = tmp_path.joinpath("out.txt") - in_id = session.tasks[0].depends_on["path"].signature # type: ignore[union-attr] - out_id = session.tasks[0].produces["produces"].signature # type: ignore[union-attr] + depends_on = session.tasks[0].depends_on + produces = session.tasks[0].produces + assert depends_on is not None + assert produces is not None + in_id = depends_on["path"].signature # type: ignore[union-attr] + out_id = produces["produces"].signature # type: ignore[union-attr] for id_, path in ( (task_id, task_path), (in_id, in_path), (out_id, out_path), ): - hash_ = db_session.get(State, (task_id, id_)).hash_ # type: ignore[union-attr] + state = db_session.get(State, (task_id, id_)) + assert state is not None + hash_ = state.hash_ assert hash_ == hash_path(path, path.stat().st_mtime) diff --git a/tests/test_mark_utils.py b/tests/test_mark_utils.py index 5aa91cb9..d49129d9 100644 --- a/tests/test_mark_utils.py +++ b/tests/test_mark_utils.py @@ -12,6 +12,7 @@ from pytask import has_mark from pytask import remove_marks from pytask import set_marks +from tests.conftest import noop @pytest.mark.parametrize( @@ -29,7 +30,7 @@ ], ) def test_get_all_marks_from_task(markers, expected): - task = Task(base_name="name", path=Path(), function=None, markers=markers) # type: ignore[arg-type] + task = Task(base_name="name", path=Path(), function=noop, markers=markers) result = get_all_marks(task) assert result == expected @@ -76,7 +77,7 @@ def func(): ... ], ) def test_get_marks_from_task(markers, marker_name, expected): - task = Task(base_name="name", path=Path(), function=None, markers=markers) # type: ignore[arg-type] + task = Task(base_name="name", path=Path(), function=noop, markers=markers) result = get_marks(task, marker_name) assert result == expected @@ -125,7 +126,7 @@ def func(): ... ], ) def test_has_mark_for_task(markers, marker_name, expected): - task = Task(base_name="name", path=Path(), function=None, markers=markers) # type: ignore[arg-type] + task = Task(base_name="name", path=Path(), function=noop, markers=markers) result = has_mark(task, marker_name) assert result is expected @@ -174,7 +175,7 @@ def func(): ... def test_remove_marks_from_task( markers, marker_name, expected_markers, expected_others ): - task = Task(base_name="name", path=Path(), function=None, markers=markers) # type: ignore[arg-type] + task = Task(base_name="name", path=Path(), function=noop, markers=markers) _, result_markers = remove_marks(task, marker_name) assert task.markers == expected_others assert result_markers == expected_markers @@ -222,7 +223,7 @@ def func(): ... ], ) def test_set_marks_to_task(markers): - task = Task(base_name="name", path=Path(), function=None) # type: ignore[arg-type] + task = Task(base_name="name", path=Path(), function=noop) result = set_marks(task, markers) assert result.markers == markers diff --git a/tests/test_persist.py b/tests/test_persist.py index 477d461c..fd6caeb5 100644 --- a/tests/test_persist.py +++ b/tests/test_persist.py @@ -72,9 +72,13 @@ def task_dummy(path=Path("in.txt"), produces=Path("out.txt")): with DatabaseSession() as db_session: task_id = session.tasks[0].signature - node_id = session.tasks[0].produces["produces"].signature # type: ignore[union-attr] + produces = session.tasks[0].produces + assert produces is not None + node_id = produces["produces"].signature # type: ignore[union-attr] - hash_ = db_session.get(State, (task_id, node_id)).hash_ # type: ignore[union-attr] + state = db_session.get(State, (task_id, node_id)) + assert state is not None + hash_ = state.hash_ path = tmp_path.joinpath("out.txt") assert hash_ == hash_path(path, path.stat().st_mtime) diff --git a/tests/test_profile.py b/tests/test_profile.py index eeb8dd41..67982cf1 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -34,7 +34,8 @@ def task_example(): time.sleep(2) with DatabaseSession() as session: runtime = session.get(Runtime, task.signature) - assert runtime.duration > 2 # type: ignore[union-attr] + assert runtime is not None + assert runtime.duration > 2 def test_profile_if_no_tasks_are_collected(tmp_path, runner): diff --git a/tests/test_skipping.py b/tests/test_skipping.py index ae7f4b87..9b5ac985 100644 --- a/tests/test_skipping.py +++ b/tests/test_skipping.py @@ -17,10 +17,7 @@ from pytask import TaskOutcome from pytask import build from pytask import cli - - -class DummyClass: - pass +from tests.conftest import noop def test_skip_unchanged(tmp_path): @@ -174,6 +171,7 @@ def task_second(path=Path("out.txt")): session = build(paths=tmp_path) node = session.collection_reports[0].node + assert node is not None assert len(node.markers) == 1 # type: ignore[union-attr] assert node.markers[0].name == "skipif" # type: ignore[union-attr] assert node.markers[0].args == () # type: ignore[union-attr] @@ -205,7 +203,7 @@ def task_second(path=Path("out.txt")): ... session = build(paths=tmp_path) node = session.collection_reports[0].node - + assert node is not None assert len(node.markers) == 1 # type: ignore[union-attr] assert node.markers[0].name == "skipif" # type: ignore[union-attr] assert node.markers[0].args == (False,) # type: ignore[union-attr] @@ -234,6 +232,7 @@ def task_second(path=Path("out.txt")): session = build(paths=tmp_path) node = session.collection_reports[0].node + assert node is not None assert len(node.markers) == 2 # type: ignore[union-attr] assert node.markers[0].name == "skipif" # type: ignore[union-attr] assert node.markers[0].args == () # type: ignore[union-attr] @@ -265,7 +264,7 @@ def task_second(path=Path("out.txt")): ) def test_pytask_execute_task_setup(marker_name, force, expectation): session = Session.from_config({"force": force}) - task = Task(base_name="task", path=Path(), function=None) # type: ignore[arg-type] + task = Task(base_name="task", path=Path(), function=noop) kwargs = {"reason": ""} if marker_name == "skip_ancestor_failed" else {} task.markers = [Mark(marker_name, (), kwargs)] From 4b18ce9490cd297d1cfadb89ab309446deb2bd41 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 28 Dec 2025 17:28:00 +0100 Subject: [PATCH 19/25] rearrange deps. --- justfile | 2 +- pyproject.toml | 11 ++++------- uv.lock | 16 ++++++++-------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/justfile b/justfile index 6431b00c..bd38da90 100644 --- a/justfile +++ b/justfile @@ -16,7 +16,7 @@ test-nb: # Run type checking typing: - uv run --group typing --group test ty check src/ tests/ + uv run --group dev --group typing --group test ty check src/ tests/ # Run linting lint: diff --git a/pyproject.toml b/pyproject.toml index 31dbf877..1a052455 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ name = "Tobias Raabe" email = "raabe@posteo.de" [dependency-groups] -dev = ["pygraphviz>=1.12;platform_system=='Linux'"] +dev = ["pygraphviz>=1.12;platform_system=='Linux'", "coiled>=1.42.0"] docs = [ "furo>=2024.8.6", "ipython>=8.13.2", @@ -64,6 +64,7 @@ docs = [ ] plugin-list = ["httpx>=0.27.0", "tabulate[widechars]>=0.9.0", "tqdm>=4.66.3"] test = [ + "cloudpickle>=3.0.0", "deepdiff>=7.0.0", "nbmake>=1.5.5", "pygments>=2.18.0", @@ -72,13 +73,9 @@ test = [ "pytest-cov>=5.0.0", "pytest-xdist>=3.6.1", "syrupy>=4.5.0", - "aiohttp>=3.11.0", # For HTTPPath tests. -] -typing = [ - "ty>=0.0.5", - "coiled>=1.42.0", - "cloudpickle>=3.0.0", + "aiohttp>=3.11.0", # For HTTPPath tests. ] +typing = ["ty>=0.0.5"] [project.urls] Changelog = "https://pytask-dev.readthedocs.io/en/stable/changes.html" diff --git a/uv.lock b/uv.lock index 8430a6d6..6b07ba93 100644 --- a/uv.lock +++ b/uv.lock @@ -2671,6 +2671,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "coiled" }, { name = "pygraphviz", marker = "sys_platform == 'linux'" }, ] docs = [ @@ -2697,6 +2698,7 @@ plugin-list = [ ] test = [ { name = "aiohttp" }, + { name = "cloudpickle" }, { name = "deepdiff" }, { name = "nbmake" }, { name = "pexpect" }, @@ -2707,8 +2709,6 @@ test = [ { name = "syrupy" }, ] typing = [ - { name = "cloudpickle" }, - { name = "coiled" }, { name = "ty" }, ] @@ -2729,7 +2729,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "pygraphviz", marker = "sys_platform == 'linux'", specifier = ">=1.12" }] +dev = [ + { name = "coiled", specifier = ">=1.42.0" }, + { name = "pygraphviz", marker = "sys_platform == 'linux'", specifier = ">=1.12" }, +] docs = [ { name = "furo", specifier = ">=2024.8.6" }, { name = "ipython", specifier = ">=8.13.2" }, @@ -2752,6 +2755,7 @@ plugin-list = [ ] test = [ { name = "aiohttp", specifier = ">=3.11.0" }, + { name = "cloudpickle", specifier = ">=3.0.0" }, { name = "deepdiff", specifier = ">=7.0.0" }, { name = "nbmake", specifier = ">=1.5.5" }, { name = "pexpect", specifier = ">=4.9.0" }, @@ -2761,11 +2765,7 @@ test = [ { name = "pytest-xdist", specifier = ">=3.6.1" }, { name = "syrupy", specifier = ">=4.5.0" }, ] -typing = [ - { name = "cloudpickle", specifier = ">=3.0.0" }, - { name = "coiled", specifier = ">=1.42.0" }, - { name = "ty", specifier = ">=0.0.5" }, -] +typing = [{ name = "ty", specifier = ">=0.0.5" }] [[package]] name = "pytest" From 5a1ae34761c366b1eb3a11dd9282ff734d818f30 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 28 Dec 2025 17:35:58 +0100 Subject: [PATCH 20/25] Fix. --- tests/test_persist.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/tests/test_persist.py b/tests/test_persist.py index fd6caeb5..b2ddff3b 100644 --- a/tests/test_persist.py +++ b/tests/test_persist.py @@ -1,6 +1,7 @@ from __future__ import annotations import textwrap +from unittest.mock import Mock import pytest @@ -17,10 +18,6 @@ from tests.conftest import restore_sys_path_and_module_after_test_execution -class DummyClass: - pass - - def test_persist_marker_is_set(tmp_path): session = build(paths=tmp_path) assert "persist" in session.config["markers"] @@ -131,21 +128,14 @@ def test_pytask_execute_task_process_report(monkeypatch, exc_info, expected): lambda *x: None, # noqa: ARG005 ) - task = DummyClass() - task.name = None # type: ignore[attr-defined] - task.signature = "id" # type: ignore[attr-defined] - - session = DummyClass() - session.dag = None # type: ignore[attr-defined] - - report = DummyClass() - report.exc_info = exc_info # type: ignore[attr-defined] - report.task = task # type: ignore[attr-defined] + task = Mock(name=None, signature="id") + session = Mock(dag=None) + report = Mock(exc_info=exc_info, task=task) result = pytask_execute_task_process_report(session, report) # type: ignore[arg-type] if expected: - assert report.outcome == TaskOutcome.PERSISTENCE # type: ignore[attr-defined] + assert report.outcome == TaskOutcome.PERSISTENCE assert result is True else: assert result is None From 6515955fcca7a8add303089c60ff37b43db8beac Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 28 Dec 2025 20:07:52 +0100 Subject: [PATCH 21/25] remove unused ignores. --- pyproject.toml | 5 ++- src/_pytask/cache.py | 19 ++++---- src/_pytask/click.py | 10 ++--- src/_pytask/coiled_utils.py | 2 +- src/_pytask/collect.py | 2 +- src/_pytask/collect_command.py | 16 +++---- src/_pytask/collect_utils.py | 2 +- src/_pytask/dag.py | 6 +-- src/_pytask/data_catalog.py | 2 +- src/_pytask/execute.py | 6 +-- src/_pytask/hookspecs.py | 8 ++-- src/_pytask/path.py | 4 +- src/_pytask/provisional.py | 4 +- src/_pytask/provisional_utils.py | 6 +-- src/_pytask/task_utils.py | 14 ++++-- .../test_functional_interface.ipynb | 2 +- ...functional_interface_w_relative_path.ipynb | 2 +- tests/test_outcomes.py | 2 +- tests/test_path.py | 4 +- tests/test_persist.py | 2 +- tests/test_task_utils.py | 14 +++--- uv.lock | 44 +++++++++---------- 22 files changed, 90 insertions(+), 86 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a052455..f24b7db7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ test = [ "syrupy>=4.5.0", "aiohttp>=3.11.0", # For HTTPPath tests. ] -typing = ["ty>=0.0.5"] +typing = ["ty>=0.0.7"] [project.urls] Changelog = "https://pytask-dev.readthedocs.io/en/stable/changes.html" @@ -166,6 +166,9 @@ filterwarnings = [ "ignore:The --rsyncdir command line argument:DeprecationWarning", ] +[tool.ty.rules] +unused-ignore-comment = "error" + [tool.ty.src] exclude = ["src/_pytask/_hashlib.py"] diff --git a/src/_pytask/cache.py b/src/_pytask/cache.py index 7af8b592..517c89b8 100644 --- a/src/_pytask/cache.py +++ b/src/_pytask/cache.py @@ -19,20 +19,21 @@ if TYPE_CHECKING: from collections.abc import Callable + from typing import TypeAlias + + from ty_extensions import Intersection + + Memoized: TypeAlias = "Intersection[Callable[P, R], HasCache]" P = ParamSpec("P") R = TypeVar("R") -class MemoizedCallable(Protocol[P, R]): - """A callable that has been memoized and has a cache attribute.""" +class HasCache(Protocol): + """Protocol for objects that have a cache attribute.""" cache: Cache - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: - """Call the memoized function.""" - ... - @define class CacheInfo: @@ -46,7 +47,7 @@ class Cache: _sentinel: Any = field(factory=object) cache_info: CacheInfo = field(factory=CacheInfo) - def memoize(self, func: Callable[P, R]) -> MemoizedCallable[P, R]: + def memoize(self, func: Callable[P, R]) -> Memoized[P, R]: func_module = getattr(func, "__module__", "") func_name = getattr(func, "__name__", "") prefix = f"{func_module}.{func_name}:" @@ -68,9 +69,9 @@ def wrapped(*args: P.args, **kwargs: P.kwargs) -> R: return value - wrapped.cache = self # type: ignore[attr-defined] + wrapped.cache = self # ty: ignore[unresolved-attribute] - return wrapped # type: ignore[return-value] + return wrapped def add(self, key: str, value: Any) -> None: self._cache[key] = value diff --git a/src/_pytask/click.py b/src/_pytask/click.py index f5b643e1..36f2fa04 100644 --- a/src/_pytask/click.py +++ b/src/_pytask/click.py @@ -37,12 +37,10 @@ if importlib.metadata.version("click") < "8.2": from click.parser import split_opt else: - from click.parser import ( # type: ignore[attr-defined, no-redef, unused-ignore, unresolved-import] - _split_opt as split_opt, # ty: ignore[unresolved-import] - ) + from click.parser import _split_opt as split_opt # ty: ignore[unresolved-import] -class EnumChoice(Choice): # type: ignore[type-arg, unused-ignore] +class EnumChoice(Choice): """An enum-based choice type. The implementation is copied from https://github.com/pallets/click/pull/2210 and @@ -75,7 +73,7 @@ def convert(self, value: Any, param: Parameter | None, ctx: Context | None) -> A class _OptionHighlighter(RegexHighlighter): """A highlighter for help texts.""" - highlights: ClassVar = [ # type: ignore[misc] + highlights: ClassVar = [ r"(?P\-\w)\b", r"(?P