From 4e5cb0e09f2874d2fadbf29a3aabdceaf4c5f03c Mon Sep 17 00:00:00 2001 From: Jonathan B Coe Date: Sat, 31 Jan 2026 18:46:00 +0000 Subject: [PATCH 1/7] Copy devcontainer from https://github.com/jbcoe/pytorch-sandbox --- .devcontainer/.dockerfilelintrc | 2 ++ .devcontainer/Dockerfile | 26 +++++++++++++++++ .devcontainer/devcontainer.json | 52 +++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 .devcontainer/.dockerfilelintrc create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/.dockerfilelintrc b/.devcontainer/.dockerfilelintrc new file mode 100644 index 0000000..782a730 --- /dev/null +++ b/.devcontainer/.dockerfilelintrc @@ -0,0 +1,2 @@ +rules: + sudo_usage: off diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..60616a2 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,26 @@ +# Use Python:3.12-slim image as the base image as the PyTorch image +# https://hub.docker.com/r/pytorch/pytorch is too big (>3GB). +FROM python:3.12-slim + +# Install essential packages and create non-root user +RUN apt-get update && apt-get install -y --no-install-recommends git curl sudo bash-completion vim \ + && useradd -m -s /bin/bash vscode \ + && echo "vscode ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \ + && mkdir -p /workspace \ + && chown vscode:vscode /workspace \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace + +# Switch to non-root user +USER vscode + +# Set up shell completions +RUN echo 'source /usr/share/bash-completion/completions/git' >> ~/.bashrc \ + && echo 'source /etc/bash_completion' >> ~/.bashrc + +# Set up uv and Python environment +RUN curl -LsSf https://astral.sh/uv/install.sh | sh + +# Set up environment variables +ENV PATH="/home/vscode/.local/bin:${PATH}" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6f3379e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,52 @@ +{ + "name": "py_cppmodel Dev Container", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "customizations": { + "vscode": { + "extensions": [ + "charliermarsh.ruff", + "esbenp.prettier-vscode", + "github.vscode-pull-request-github", + "ms-python.mypy-type-checker", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-toolsai.jupyter-keymap", + "ms-toolsai.jupyter-renderers", + "ms-toolsai.jupyter", + "ms-toolsai.vscode-jupyter-cell-tags", + "ms-toolsai.vscode-jupyter-slideshow", + "tamasfe.even-better-toml" + ], + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml", + "editor.formatOnSave": true + }, + "evenBetterToml.schema.enabled": true, + "evenBetterToml.schema.associations": { + "pyproject.toml": "https://json.schemastore.org/pyproject.json" + } + } + } + }, + "mounts": [ + "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached", + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" + ], + "remoteUser": "vscode", + "updateRemoteUserUID": true, + "remoteEnv": { + "UV_LINK_MODE": "copy" + } +} From aa5978728cf7d6eaf463976bc48d0be1be600648 Mon Sep 17 00:00:00 2001 From: Jonathan B Coe Date: Sat, 31 Jan 2026 18:50:55 +0000 Subject: [PATCH 2/7] Simplify devcontainer and correct PythonPath --- .devcontainer/devcontainer.json | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6f3379e..679c627 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -18,24 +18,15 @@ "ms-toolsai.jupyter", "ms-toolsai.vscode-jupyter-cell-tags", "ms-toolsai.vscode-jupyter-slideshow", - "tamasfe.even-better-toml" ], "settings": { - "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.defaultInterpreterPath": "/${workspaceFolder}/.venv/bin/python", "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" } - }, - "[toml]": { - "editor.defaultFormatter": "tamasfe.even-better-toml", - "editor.formatOnSave": true - }, - "evenBetterToml.schema.enabled": true, - "evenBetterToml.schema.associations": { - "pyproject.toml": "https://json.schemastore.org/pyproject.json" } } } From d076ccf1a6328564f8248ae46c3a877da0bfcfd5 Mon Sep 17 00:00:00 2001 From: Jonathan B Coe Date: Sat, 31 Jan 2026 18:52:21 +0000 Subject: [PATCH 3/7] Cleanup from CodePilot review --- .devcontainer/Dockerfile | 2 -- .devcontainer/devcontainer.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 60616a2..d1c4445 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,3 @@ -# Use Python:3.12-slim image as the base image as the PyTorch image -# https://hub.docker.com/r/pytorch/pytorch is too big (>3GB). FROM python:3.12-slim # Install essential packages and create non-root user diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 679c627..676f004 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "name": "py_cppmodel Dev Container", + "name": "py-cppmodel Dev Container", "build": { "dockerfile": "Dockerfile", "context": ".." From 669df3687196572a48968aea0498a812073126b7 Mon Sep 17 00:00:00 2001 From: Jonathan B Coe Date: Sat, 31 Jan 2026 18:54:31 +0000 Subject: [PATCH 4/7] Cleanup from CodePilot review --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 676f004..fd24ccd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -33,7 +33,7 @@ }, "mounts": [ "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached", - "source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" + "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" ], "remoteUser": "vscode", "updateRemoteUserUID": true, From 657323fdd1f7b95e6cbe8a604804ad5bb9381866 Mon Sep 17 00:00:00 2001 From: Jonathan B Coe Date: Sat, 31 Jan 2026 19:35:35 +0000 Subject: [PATCH 5/7] Use pytest for simpler tests --- .github/workflows/python.yml | 2 +- README.md | 2 +- pyproject.toml | 5 ++ test.macos.sh | 2 +- test_parse_standard_library_includes.py | 85 ++++++++---------- test_py_cppmodel.py | 109 +++++++++++------------- uv.lock | 62 ++++++++++++++ 7 files changed, 158 insertions(+), 109 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 79c133e..506742a 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -31,4 +31,4 @@ jobs: - name: Type checks run: uv run ty check . - name: Tests - run: uv run python -m unittest discover --verbose . + run: uv run pytest diff --git a/README.md b/README.md index 85a9eb9..de04512 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ uv sync To run the tests, run: ```sh -uv run python -m unittest discover --verbose . +uv run pytest ``` To run type checking: diff --git a/pyproject.toml b/pyproject.toml index c454199..7f7fdbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,9 +20,14 @@ dev = [ "jupyter>=1.1.1", "parameterized>=0.9.0", "pre-commit>=4.5.1", + "pytest>=9.0.2", + "pytest-xdist>=3.8.0", "ty>=0.0.14", ] +[tool.pytest.ini_options] +addopts = "-n auto -v" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/test.macos.sh b/test.macos.sh index 0b271a1..8014f92 100755 --- a/test.macos.sh +++ b/test.macos.sh @@ -7,4 +7,4 @@ uv sync uv run ty check . # Unit tests -uv run python -m unittest discover --verbose . +uv run pytest diff --git a/test_parse_standard_library_includes.py b/test_parse_standard_library_includes.py index 218af70..716f7d4 100644 --- a/test_parse_standard_library_includes.py +++ b/test_parse_standard_library_includes.py @@ -1,7 +1,5 @@ -import unittest - +import pytest from clang.cindex import TranslationUnit -from parameterized import parameterized import py_cppmodel @@ -14,50 +12,41 @@ ] -def _custom_name_func(testcase_func, _, param): - return "%s_%s" % (testcase_func.__name__, parameterized.to_safe_name(param.args[0])) - - -class TestStandardLibraryIncludes(unittest.TestCase): - @parameterized.expand( - [ - "algorithm", - "any", - "array", - "deque", - "forward_list", - "functional", - "iterator", - "list", - "map", - "memory", - "numeric", - "optional", - "queue", - "set", - "stack", - "string", - "tuple", - "type_traits", - "unordered_map", - "unordered_set", - "utility", - "variant", - "vector", - ], - name_func=_custom_name_func, +@pytest.mark.parametrize( + "include", + [ + "algorithm", + "any", + "array", + "deque", + "forward_list", + "functional", + "iterator", + "list", + "map", + "memory", + "numeric", + "optional", + "queue", + "set", + "stack", + "string", + "tuple", + "type_traits", + "unordered_map", + "unordered_set", + "utility", + "variant", + "vector", + ], +) +def test_include(include): + source = f"#include <{include}>" + tu = TranslationUnit.from_source( + "t.cc", + COMPILER_ARGS, + unsaved_files=[("t.cc", source)], ) - def test_include(self, include): - source = f"#include <{include}>" - tu = TranslationUnit.from_source( - "t.cc", - COMPILER_ARGS, - unsaved_files=[("t.cc", source)], - ) - - # This should not raise an exception. - self.model = py_cppmodel.Model(tu) - -if __name__ == "__main__": - unittest.main() + # This should not raise an exception. + py_cppmodel.Model(tu) diff --git a/test_py_cppmodel.py b/test_py_cppmodel.py index 92ee415..908262b 100644 --- a/test_py_cppmodel.py +++ b/test_py_cppmodel.py @@ -1,5 +1,4 @@ -import unittest - +import pytest from clang.cindex import TranslationUnit import py_cppmodel @@ -35,59 +34,53 @@ class B { """ -class TestCppModel(unittest.TestCase): - def setUp(self): - tu = TranslationUnit.from_source( - "sample.cc", - COMPILER_ARGS, - unsaved_files=[("sample.cc", SOURCE)], - ) - self.model = py_cppmodel.Model(tu) - - def test_filename(self): - self.assertEqual(self.model.filename, "sample.cc") - - def test_functions(self): - self.assertEqual(len(self.model.functions), 2) - self.assertEqual(str(self.model.functions[0]), "") - self.assertEqual(str(self.model.functions[1]), "") - - def test_classes(self): - self.assertEqual(len(self.model.classes), 1) - self.assertEqual(str(self.model.classes[0]), "") - - self.assertEqual(len(self.model.classes[0].annotations), 1) - self.assertEqual(self.model.classes[0].annotations[0], "A") - - self.assertEqual(len(self.model.classes[0].members), 3) - self.assertEqual( - str(self.model.classes[0].members[0]), - " a>", - ) - self.assertEqual( - str(self.model.classes[0].members[1]), - " b>", - ) - self.assertEqual( - str(self.model.classes[0].members[2]), - " c>", - ) - - self.assertEqual(len(self.model.classes[0].methods), 1) - self.assertEqual(str(self.model.classes[0].methods[0]), "") - self.assertEqual(len(self.model.classes[0].methods[0].annotations), 1) - self.assertEqual(self.model.classes[0].methods[0].annotations[0], "foo") - - self.assertEqual(len(self.model.unmodelled_nodes), 2) - self.assertEqual( - str(self.model.unmodelled_nodes[0]), - ">", - ) - self.assertEqual( - str(self.model.unmodelled_nodes[1]), - " >", - ) - - -if __name__ == "__main__": - unittest.main() +@pytest.fixture +def model(): + tu = TranslationUnit.from_source( + "sample.cc", + COMPILER_ARGS, + unsaved_files=[("sample.cc", SOURCE)], + ) + return py_cppmodel.Model(tu) + + +def test_filename(model): + assert model.filename == "sample.cc" + + +def test_functions(model): + assert len(model.functions) == 2 + assert str(model.functions[0]) == "" + assert str(model.functions[1]) == "" + + +def test_classes(model): + assert len(model.classes) == 1 + assert str(model.classes[0]) == "" + + assert len(model.classes[0].annotations) == 1 + assert model.classes[0].annotations[0] == "A" + + +def test_class_members(model): + assert len(model.classes[0].members) == 3 + assert str(model.classes[0].members[0]) == " a>" + assert str(model.classes[0].members[1]) == " b>" + assert str(model.classes[0].members[2]) == " c>" + + assert len(model.classes[0].methods) == 1 + assert str(model.classes[0].methods[0]) == "" + assert len(model.classes[0].methods[0].annotations) == 1 + assert model.classes[0].methods[0].annotations[0] == "foo" + + +def test_unmodelled_nodes(model): + assert len(model.unmodelled_nodes) == 2 + assert ( + str(model.unmodelled_nodes[0]) + == ">" + ) + assert ( + str(model.unmodelled_nodes[1]) + == " >" + ) diff --git a/uv.lock b/uv.lock index bd4be30..5be05d7 100644 --- a/uv.lock +++ b/uv.lock @@ -445,6 +445,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -536,6 +545,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "ipykernel" version = "7.1.0" @@ -1239,6 +1257,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pre-commit" version = "4.5.1" @@ -1338,6 +1365,8 @@ dev = [ { name = "jupyter" }, { name = "parameterized" }, { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-xdist" }, { name = "ty" }, ] @@ -1353,6 +1382,8 @@ dev = [ { name = "jupyter", specifier = ">=1.1.1" }, { name = "parameterized", specifier = ">=0.9.0" }, { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "ty", specifier = ">=0.0.14" }, ] @@ -1374,6 +1405,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" From eaf8faa0b07b51eb793641e90830f1c952376769 Mon Sep 17 00:00:00 2001 From: Jonathan B Coe Date: Sat, 31 Jan 2026 19:39:08 +0000 Subject: [PATCH 6/7] fix hyphen in license file --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index deac713..40266d7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 The py_cppmodel Authors. All Rights Reserved. +Copyright (c) 2016 The py-cppmodel Authors. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 2183ee03aad300997c3150e5a7a879cb4da8f72d Mon Sep 17 00:00:00 2001 From: Jonathan B Coe Date: Sat, 31 Jan 2026 19:46:33 +0000 Subject: [PATCH 7/7] Fix from Copilot review --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7f7fdbb..cc20262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,6 @@ Repository = "https://github.com/jbcoe/py_cppmodel" dev = [ "ipython>=8.12.3", "jupyter>=1.1.1", - "parameterized>=0.9.0", "pre-commit>=4.5.1", "pytest>=9.0.2", "pytest-xdist>=3.8.0",