From 8a3b2750ed80ef4cbf1958d918b248f71c613410 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 8 Oct 2025 09:53:42 +0100 Subject: [PATCH 01/11] Remove joblib tox job --- tox.ini | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tox.ini b/tox.ini index a1e12b36..8c7b49c1 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,7 @@ envlist = clean, check, docs, - {py39,py310,py311,py312,py13}, - py13-joblib-earliest, + {py39,py310,py311,py312,py313}, [base] deps = @@ -27,7 +26,6 @@ basepython = py311: {env:TOXPYTHON:python3.11} py312: {env:TOXPYTHON:python3.12} py313: {env:TOXPYTHON:python3.13} - py313-joblib-earliest: {env:TOXPYTHON:python3.13} {clean,check,docs,report}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests @@ -41,11 +39,6 @@ deps = commands = {posargs:pytest --cov --cov-report=term-missing --benchmark-skip -vv tests} -[testenv:py313-joblib-earliest] -deps = - {[base]deps} - joblib==1.3.0 - [testenv:check] basepython = py313 deps = @@ -121,4 +114,4 @@ python = 3.10: py310, report 3.11: py311, report 3.12: py312, report - 3.13: py313, py313-joblib-earliest, report, check, docs + 3.13: py313, report, check, docs From 4666a7b8606e30840822d4b9a3fd58bc10ef596e Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 8 Oct 2025 09:47:15 +0100 Subject: [PATCH 02/11] Support Python 3.14 --- .github/workflows/main.yml | 17 +++++++++-------- .github/workflows/release.yml | 22 +++++++++++++--------- CHANGELOG.rst | 5 +++++ pyproject.toml | 1 + tox.ini | 8 +++++--- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a8e41bd..9290edf4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: python-version: [ - "3.9", "3.10", "3.11", "3.12", "3.13-dev" + "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] os: [ubuntu-latest, macos-latest, windows-latest] @@ -25,7 +25,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: toolchain: stable - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -40,7 +40,7 @@ jobs: check_rust: name: Check Rust - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 @@ -59,15 +59,15 @@ jobs: working-directory: ./rust benchmarks: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Setup python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.13" allow-prereleases: true @@ -82,8 +82,9 @@ jobs: uv pip install pytest==7.4.4 pyyaml==6.0.1 pytest-codspeed==3.2.0 Django==5.1.1 /home/runner/work/grimp/grimp - name: Run benchmarks - uses: CodSpeedHQ/action@v3 + uses: CodSpeedHQ/action@v4 with: token: ${{ secrets.CODSPEED_TOKEN }} + mode: instrumentation run: | uv run pytest tests/benchmarking/ --codspeed \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb7b983d..19667b85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,8 +33,8 @@ jobs: - runner: ubuntu-latest target: ppc64le steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: | 3.9 @@ -42,6 +42,7 @@ jobs: 3.11 3.12 3.13 + 3.14 allow-prereleases: true - name: Build wheels uses: PyO3/maturin-action@v1 @@ -70,8 +71,8 @@ jobs: - runner: ubuntu-latest target: armv7 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: | 3.9 @@ -79,6 +80,7 @@ jobs: 3.11 3.12 3.13 + 3.14 allow-prereleases: true - name: Build wheels uses: PyO3/maturin-action@v1 @@ -103,8 +105,8 @@ jobs: - runner: windows-latest target: x86 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: | 3.9 @@ -112,6 +114,7 @@ jobs: 3.11 3.12 3.13 + 3.14 allow-prereleases: true architecture: ${{ matrix.platform.target }} - name: Build wheels @@ -136,8 +139,8 @@ jobs: - runner: macos-14 target: aarch64 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: | 3.9 @@ -145,6 +148,7 @@ jobs: 3.11 3.12 3.13 + 3.14 allow-prereleases: true - name: Build wheels uses: PyO3/maturin-action@v1 @@ -161,7 +165,7 @@ jobs: sdist: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build sdist uses: PyO3/maturin-action@v1 with: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 78b3e8b5..9edab700 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog ========= +latest +------ + +* Officially support Python 3.14. + 3.11 (2025-09-01) ----------------- diff --git a/pyproject.toml b/pyproject.toml index 0d493960..c3e07ffb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Rust", "Topic :: Utilities", diff --git a/tox.ini b/tox.ini index 8c7b49c1..8b4fb848 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = clean, check, docs, - {py39,py310,py311,py312,py313}, + {py39,py310,py311,py312,py313,py314}, [base] deps = @@ -26,6 +26,7 @@ basepython = py311: {env:TOXPYTHON:python3.11} py312: {env:TOXPYTHON:python3.12} py313: {env:TOXPYTHON:python3.13} + py314: {env:TOXPYTHON:python3.14} {clean,check,docs,report}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests @@ -55,7 +56,7 @@ commands = lint-imports [testenv:benchmark] -basepython = py313 +basepython = py314 setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes @@ -71,7 +72,7 @@ commands = {posargs:pytest --benchmark-only --benchmark-autosave} [testenv:codspeed] -basepython = py313 +basepython = py314 setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes @@ -115,3 +116,4 @@ python = 3.11: py311, report 3.12: py312, report 3.13: py313, report, check, docs + 3.14: py314, report From d520d41c15c93e6e055f1a9fad7435f8ee9cead6 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 8 Oct 2025 14:42:44 +0100 Subject: [PATCH 03/11] Update readthedocs.yaml --- .readthedocs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0375648f..c5aecb0e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,9 +4,9 @@ version: 2 build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.11" + python: "3.12" sphinx: configuration: docs/conf.py From ffea2854661cf2ed6a8d9f9419ebfb510f56e6a5 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 8 Oct 2025 14:52:39 +0100 Subject: [PATCH 04/11] Reformat files for ruff We're switching from black to ruff --- docs/conf.py | 54 +++++++++---------- src/grimp/adaptors/graph.py | 2 +- src/grimp/application/ports/filesystem.py | 12 ++--- tests/adaptors/filesystem.py | 3 +- .../adaptors/graph/test_direct_imports.py | 2 +- tests/unit/adaptors/graph/test_repr.py | 3 +- tests/unit/adaptors/test_caching.py | 2 +- tests/unit/application/test_scanning.py | 5 +- tests/unit/application/test_usecases.py | 2 +- 9 files changed, 40 insertions(+), 45 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 96c11603..1c66e047 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,14 +19,14 @@ # -- Project information ----------------------------------------------------- -project = 'Grimp' -copyright = '2018, David Seddon' -author = 'David Seddon' +project = "Grimp" +copyright = "2018, David Seddon" +author = "David Seddon" # The short X.Y version -version = '' +version = "" # The full version, including alpha/beta/rc tags -release = '3.11' +release = "3.11" # -- General configuration --------------------------------------------------- @@ -38,20 +38,19 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ -] +extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -63,10 +62,10 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output ------------------------------------------------- @@ -75,9 +74,9 @@ # a list of builtin themes. # # on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +on_rtd = os.environ.get("READTHEDOCS", None) == "True" -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -88,7 +87,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -104,7 +103,7 @@ # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'Grimpdoc' +htmlhelp_basename = "Grimpdoc" # -- Options for LaTeX output ------------------------------------------------ @@ -113,15 +112,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -131,8 +127,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'Grimp.tex', 'Grimp Documentation', - 'David Seddon', 'manual'), + (master_doc, "Grimp.tex", "Grimp Documentation", "David Seddon", "manual"), ] @@ -140,10 +135,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'grimp', 'Grimp Documentation', - [author], 1) -] +man_pages = [(master_doc, "grimp", "Grimp Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -152,7 +144,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'Grimp', 'Grimp Documentation', - author, 'Grimp', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "Grimp", + "Grimp Documentation", + author, + "Grimp", + "One line description of project.", + "Miscellaneous", + ), ] diff --git a/src/grimp/adaptors/graph.py b/src/grimp/adaptors/graph.py index 5ede492d..ca7953a8 100644 --- a/src/grimp/adaptors/graph.py +++ b/src/grimp/adaptors/graph.py @@ -212,7 +212,7 @@ def _parse_layers(layers: Sequence[Layer | str | set[str]]) -> tuple[Layer, ...] def _dependencies_from_tuple( - rust_package_dependency_tuple: tuple[_RustPackageDependency, ...] + rust_package_dependency_tuple: tuple[_RustPackageDependency, ...], ) -> set[PackageDependency]: return { PackageDependency( diff --git a/src/grimp/application/ports/filesystem.py b/src/grimp/application/ports/filesystem.py index 6c1bfd9f..5e966cc7 100644 --- a/src/grimp/application/ports/filesystem.py +++ b/src/grimp/application/ports/filesystem.py @@ -103,14 +103,10 @@ class BasicFileSystem(Protocol): FileSystem interface. """ - def join(self, *components: str) -> str: - ... + def join(self, *components: str) -> str: ... - def split(self, file_name: str) -> Tuple[str, str]: - ... + def split(self, file_name: str) -> Tuple[str, str]: ... - def read(self, file_name: str) -> str: - ... + def read(self, file_name: str) -> str: ... - def exists(self, file_name: str) -> bool: - ... + def exists(self, file_name: str) -> bool: ... diff --git a/tests/adaptors/filesystem.py b/tests/adaptors/filesystem.py index b9ab1c07..b8f4f3a1 100644 --- a/tests/adaptors/filesystem.py +++ b/tests/adaptors/filesystem.py @@ -148,8 +148,7 @@ def _dedent(self, lines: List[str]) -> List[str]: """ first_line = lines[0] first_line_indent = len(first_line) - len(first_line.lstrip()) - dedented = lambda line: line[first_line_indent:] - return list(map(dedented, lines)) + return [line[first_line_indent:] for line in lines] def read(self, file_name: str) -> str: if not self.exists(file_name): diff --git a/tests/unit/adaptors/graph/test_direct_imports.py b/tests/unit/adaptors/graph/test_direct_imports.py index 2a145122..60264dd1 100644 --- a/tests/unit/adaptors/graph/test_direct_imports.py +++ b/tests/unit/adaptors/graph/test_direct_imports.py @@ -239,7 +239,7 @@ def test_returns_empty_list_when_import_but_no_available_details(self): graph = ImportGraph() importer, imported = "foo", "bar" - graph.add_import(importer=importer, imported=imported), + (graph.add_import(importer=importer, imported=imported),) assert [] == graph.get_import_details(importer=importer, imported=imported) diff --git a/tests/unit/adaptors/graph/test_repr.py b/tests/unit/adaptors/graph/test_repr.py index 0b7a81d9..da6a08c4 100644 --- a/tests/unit/adaptors/graph/test_repr.py +++ b/tests/unit/adaptors/graph/test_repr.py @@ -34,7 +34,6 @@ def test_truncated(self): re_part = "(" + "|".join(modules) + ")" assert re.match( - f"", + f"", repr(graph), ) diff --git a/tests/unit/adaptors/test_caching.py b/tests/unit/adaptors/test_caching.py index 8b733850..749f14b3 100644 --- a/tests/unit/adaptors/test_caching.py +++ b/tests/unit/adaptors/test_caching.py @@ -687,7 +687,7 @@ def test_write_to_cache_adds_marker_files(self): ) assert file_system.read(f"{some_cache_dir}/.gitignore") == ( - "# Automatically created by Grimp.\n" "*" + "# Automatically created by Grimp.\n*" ) assert file_system.read(f"{some_cache_dir}/CACHEDIR.TAG") == ( "Signature: 8a477f597d28d172789f06886806bc55\n" diff --git a/tests/unit/application/test_scanning.py b/tests/unit/application/test_scanning.py index 4b65b9c3..b4a457d3 100644 --- a/tests/unit/application/test_scanning.py +++ b/tests/unit/application/test_scanning.py @@ -82,7 +82,10 @@ def test_absolute_imports(include_external_packages, expected_result): def test_non_ascii(): blue_module = Module("mypackage.blue") blue_module_file = _module_to_module_file(blue_module) - non_ascii_modules = {Module("mypackage.ñon_ascii_变"), Module("mypackage.ñon_ascii_变.ラーメン")} + non_ascii_modules = { + Module("mypackage.ñon_ascii_变"), + Module("mypackage.ñon_ascii_变.ラーメン"), + } file_system = rust.FakeBasicFileSystem( content_map={ "/path/to/mypackage/blue.py": """ diff --git a/tests/unit/application/test_usecases.py b/tests/unit/application/test_usecases.py index 531cfe56..c38bfaab 100644 --- a/tests/unit/application/test_usecases.py +++ b/tests/unit/application/test_usecases.py @@ -28,7 +28,7 @@ def test_happy_path(self, include_external_packages): """, content_map={ "/path/to/mypackage/foo/one.py": ( - "import mypackage.foo.two.green\n" "from .. import Something" + "import mypackage.foo.two.green\nfrom .. import Something" ), "/path/to/mypackage/foo/two/green.py": "import mypackage.foo.two.blue\n" "from external.subpackage import foobar\n" From eb9cb21e1540225afdec8a4a85cb84a14c0b64f0 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 8 Oct 2025 14:58:37 +0100 Subject: [PATCH 05/11] Add precommit --- .pre-commit-config.yaml | 55 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..25d05c68 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,55 @@ +# This file contains configuration for the pre-commit (https://pre-commit.com/) tool. +fail_fast: true +repos: + - repo: local + hooks: + - id: format-python + name: Format Python + entry: just format-python + language: system + require_serial: true + pass_filenames: false + files: "^pyproject\\.toml|^src/.*|^tests/.*|^docs/conf.py$" + + - id: format-rust + name: Format Rust + entry: just format-rust + language: system + require_serial: true + pass_filenames: false + files: "^rust/.*$" + + - id: lint-python + name: Lint Python + entry: just lint-python + language: system + pass_filenames: false + files: "^pyproject\\.toml|^src/.*|^tests/.*|^docs/conf.py$" + + - id: lint-rust + name: Lint Rust + entry: just lint-rust + language: system + pass_filenames: false + files: "^rust/.*$" + + - id: run-python-tests + name: "Run Python tests" + entry: just test-python + language: system + pass_filenames: false + files: "^src/.*|^tests/.*|^rust/.*" + + - id: run-rust-tests + name: "Run Rust tests" + entry: just test-rust + language: system + pass_filenames: false + files: "^rust/.*" + + - id: build-docs + name: "Build docs" + entry: just build-docs + language: system + pass_filenames: false + files: "\\.rst$|^docs/.*" From a3484b932937ec58cf996c2fd1d4c69046f2c2b1 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 8 Oct 2025 14:59:40 +0100 Subject: [PATCH 06/11] Switch to uv --- pyproject.toml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c3e07ffb..a3bc6e44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,34 @@ readme = "README.rst" Documentation = "https://grimp.readthedocs.io/" Source-code = "https://github.com/seddonym/grimp/" +[dependency-groups] +dev = [ + "import-linter>=2.5", + "mypy>=1.18.2", + "pre-commit>=4.3.0", + "pytest>=8.4.2", + "pytest-benchmark>=5.1.0", + "pytest-stub>=1.1.0", + "pyyaml>=6.0.3", + "ruff>=0.14.0", + "syrupy>=4.9.1", + "types-pyyaml>=6.0.12.20250915", + # External packages to attempt to build the graph from. + # If the versions change, the snapshot tests will fail, and the benchmarks will change. + "Django==4.2.17", # N.B. Django 5 doesn't support Python 3.9. + "flask==3.0.3", + "requests==2.32.3", + "sqlalchemy==2.0.35", + "google-cloud-audit-log==0.3.0", +] +docs = [ + "sphinx>=7.4.7", + "sphinx-rtd-theme>=3.0.2", +] +benchmark_ci = [ + "pytest-codspeed>=3.2.0", +] + [tool.setuptools] include-package-data = true zip-safe = false @@ -53,6 +81,6 @@ grimp = ["py.typed"] where = ["src"] namespaces = false -[tool.black] +[tool.ruff] line-length = 99 -exclude = 'tests/assets/syntaxerrorpackage' +exclude = ['tests/assets'] # These are sample packages for tests to run under - we don't want ruff to mess with them. From 9f007ab4c1efbfb12330e1da0b2c868960f8be9d Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 8 Oct 2025 14:59:25 +0100 Subject: [PATCH 07/11] Add justfile --- justfile | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 justfile diff --git a/justfile b/justfile new file mode 100644 index 00000000..86c5c0d6 --- /dev/null +++ b/justfile @@ -0,0 +1,149 @@ +# List available recipes. +help: + @just --list + +# Set up Git precommit hooks for this project (recommended). +[group('setup')] +install-precommit: + @uv run pre-commit install + +# Compiles the Rust code for development. +[group('testing')] +compile: + @uv run maturin develop + +# Compiles Rust, then runs Rust and Python tests. +[group('testing')] +compile-and-test: + @just compile + @just test-rust + @just test-python + +# Runs the Rust tests. +[group('testing')] +[working-directory: 'rust'] +test-rust: + @cargo test --no-default-features + +# Runs tests under the default Python version. +[group('testing')] +test-python: + @uv run pytest --benchmark-skip + +# Runs tests under all supported Python versions, plus Rust. +[group('testing')] +test-all: test-python-3-9 test-python-3-10 test-python-3-11 test-python-3-12 test-python-3-13 test-rust + +# Runs tests under Python 3.9. +[group('testing')] +test-python-3-9: + UV_PYTHON=3.9 just test-python + +# Runs tests under Python 3.10. +[group('testing')] +test-python-3-10: + UV_PYTHON=3.10 just test-python + +# Runs tests under Python 3.11. +[group('testing')] +test-python-3-11: + UV_PYTHON=3.11 just test-python + +# Runs tests under Python 3.12. +[group('testing')] +test-python-3-12: + UV_PYTHON=3.12 just test-python + +# Runs tests under Python 3.13. +[group('testing')] +test-python-3-13: + UV_PYTHON=3.13 just test-python + +# Format the Rust code. +[group('formatting')] +[working-directory: 'rust'] +format-rust: + @cargo fmt + @echo Formatted Rust code. + +# Format the Python code. +[group('formatting')] +format-python: + @uv run ruff format + @echo Formatted Python code. + +# Format all code. +[group('formatting')] +format: + @just format-rust + @just format-python + +# Lint Python code. +[group('linting')] +lint-python: + @echo Running ruff format... + @uv run ruff format --check + @echo Running ruff check... + @uv run ruff check + @echo Running mypy... + @uv run mypy src/grimp tests + @echo Running Import Linter... + @uv run lint-imports + +# Lint Rust code using cargo fmt and clippy +[working-directory: 'rust'] +[group('linting')] +lint-rust: + @cargo fmt --check + @cargo clippy --all-targets --all-features -- -D warnings + +# Attempt to fix any clippy errors. +[working-directory: 'rust'] +[group('linting')] +autofix-rust: + @cargo clippy --all-targets --all-features --fix --allow-staged --allow-dirty + +# Run linters. +[group('linting')] +lint: + @echo Linting Python... + @just lint-python + @echo Linting Rust... + @just lint-rust + @echo + @echo '👍 {{GREEN}} Linting all good.{{NORMAL}}' + +# Build docs. +[group('docs')] +build-docs: + @uv run --group=docs sphinx-build -b html docs dist/docs --fail-on-warning --fresh-env --quiet + +# Build docs and open in browser. +[group('docs')] +build-and-open-docs: + @just build-docs + @open dist/docs/index.html + +# Run benchmarks locally using pytest-benchmark. +[group('benchmarking')] +benchmark-local: + @uv run pytest --benchmark-only --benchmark-autosave + @just show-benchmark-results + +# Show recent local benchmark results. +[group('benchmarking')] +show-benchmark-results: + @uv run --group=benchmark-local pytest-benchmark compare --group-by=fullname --sort=name --columns=mean + +# Run benchmarks using Codspeed. This only works in CI. +[group('benchmarking')] +benchmark-ci: + @uv run --group=benchmark-ci pytest --codspeed + +# Run all linters, build docs and tests. Worth running before pushing to Github. +[group('prepush')] +full-check: + @just lint + @just build-docs + @just test-all + @echo '👍 {{GREEN}} Linting, docs and tests all good.{{NORMAL}}' \ No newline at end of file From e98c5c908daa37333cb0ed5dedd93daebca164ae Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 8 Oct 2025 17:35:38 +0100 Subject: [PATCH 08/11] Update contributing guide --- CONTRIBUTING.rst | 124 ++++++++++++++++++++++++++--------------------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index b07432e5..2888dbf3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -2,8 +2,7 @@ Contributing ============ -Contributions are welcome, and they are greatly appreciated! Every -little bit helps, and credit will always be given. +We welcome contributions to Grimp. Bug reports =========== @@ -14,13 +13,6 @@ When `reporting a bug `_ please includ * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. -Documentation improvements -========================== - -Grimp could always use more documentation, whether as part of the -official docs, in docstrings, or even on the web in blog posts, -articles, and such. - Feature requests and feedback ============================= @@ -29,76 +21,100 @@ The best way to send feedback is to file an issue at https://github.com/seddonym If you are proposing a feature: * Explain in detail how it would work. -* Keep the scope as narrow as possible, to make it easier to implement. -* Remember that this is a volunteer-driven project, and that code contributions are welcome :) +* Keep the scope as narrow as possible. +* Remember that this is a volunteer-driven project. + +Submitting pull requests +======================== + +Before spending time working on a pull request, we highly recommend filing a Github issue and engaging with the project maintainer (David Seddon) to align on the direction you’re planning to take. This can save a lot of your precious time! + +This doesn’t apply to trivial pull requests such as spelling corrections. + +We recommend installing precommit, which will perform basic linting and testing whenever you commit a file. +To set this up, run ``just install-precommit``. + +Before requesting a review +-------------------------- + +- Ensure you have included tests that cover the change. In general, aim for full test coverage at the Python level. + Rust tests are optional. +- Update documentation when there's a new API, functionality etc. +- Add a note to ``CHANGELOG.rst`` about the changes. +- Add yourself to ``AUTHORS.rst``. +- Run ``just full-check`` locally. (If you're a new contributor, CI checks are not run automatically.) Development =========== -To set up `grimp` for local development: +System prerequisites +-------------------- -1. Fork `grimp `_ - (look for the "Fork" button). -2. Clone your fork locally:: +Make sure these are installed first. - git clone git@github.com:your_name_here/grimp.git +- `git `_ +- `uv `_ +- `just `_ +- Cargo (via `rustup `_) + +Setup +----- -3. Create a branch for local development:: +You don't need to activate or manage a virtual environment - this is taken care in the background of by ``uv``. - git checkout -b name-of-your-bugfix-or-feature +To set up Grimp for local development: - Now you can make your changes locally. +1. Fork `grimp `_ + (look for the "Fork" button). +2. Clone your fork locally:: -4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: + git clone git@github.com:your_name_here/grimp.git - tox +3. Change into the directory you just cloned:: -5. Commit your changes and push your branch to GitHub:: + cd grimp - git add . - git commit -m "Your detailed description of your changes." - git push origin name-of-your-bugfix-or-feature +4. Set up pre-commit. (Optional, but recommended.):: -6. Submit a pull request through the GitHub website. + just install-precommit -Rust code ---------- +You will now be able to run commands prefixed with ``just``, providing you're in the ``grimp`` directory. +To see available commands, run ``just help``. -When working with the rust code (in the ``rust/`` directory): +Working with tests +------------------ -* Run tests with ``cargo test --no-default-features``. - The ``--no-default-features`` flag is needed to due to `this PYO3 issue `_. -* Run `clippy `_ (a linter for rust) with ``cargo clippy --all-targets --all-features -- -D warnings``. - It's often possible to apply automatic fixes to clippy issues with the ``--fix`` flag e.g. ``cargo clippy --all-targets --all-features --fix --allow-staged``. +Grimp is a mixture of Python and Rust. The majority of the tests are in Python. -Pull Request Guidelines ------------------------ +If you change the Rust code, you will need to recompile it before running the Python tests. -If you need some code review or feedback while you're developing the code just make the pull request. +Typical workflow (Python changes only): -For merging, you should: +1. Make a change. +2. Run ``just test-python``. This will run the Python tests using the default Python version (e.g. whatever is specified + in a local ``.python-version`` file). -1. Include passing tests (run ``tox``) [1]_. -2. Update documentation when there's new API, functionality etc. -3. Add a note to ``CHANGELOG.rst`` about the changes. -4. Add yourself to ``AUTHORS.rst``. +Typical workflow (changes that involve Rust): -.. [1] If you don't have all the necessary python versions available locally you can rely on Github Actions, which will - run the tests for each change you add in the pull request. +1. Make a change to Rust code. +2. Run ``just compile-and-test``. This will compile the Rust code, then run Rust and Python tests in the default version. - It will be slower though ... +There are also version-specific test commands (e.g. ``just test-3-13``) - run ``just help`` to see which ones. -Tips ----- +Working with documentation +-------------------------- -To run a subset of tests:: +You can preview any documentation changes locally by running:: - tox -e envname -- pytest -k test_myfeature + just build-and-open-docs -To run all the test environments in *parallel* (you need to ``pip install detox``):: +It's helpful to include a screenshot of the new docs in the pull request description. - detox +Before you push +--------------- +It's a good idea to run ``just full-check`` before getting a review. +This will run linters, docs build and tests under every supported Python version. Benchmarking ============ @@ -117,13 +133,9 @@ Local benchmarking It's also possible to run local benchmarks, which can be helpful if you want to quickly compare performance across different versions of the code. -To benchmark a particular version of the code, run ``tox -ebenchmark``. This command creates a report that will be +To benchmark a particular version of the code, run ``just benchmark-local``. This command creates a report that will be stored in a local file (ignored by Git). -You can then see how your latest benchmark compares with earlier ones, by running: - -``pytest-benchmark compare --group-by=func --sort=name --columns=mean`` - This will display a list of all the benchmarks you've run locally, ordered from earlier to later. Profiling @@ -152,6 +164,8 @@ This will create a file called ``flamegraph.svg``, which you can open to view th Releasing to Pypi ================= +This is only possible if you're a maintainer. + 1. Choose a new version number (based on `semver `_). 2. ``git pull origin main`` 3. Update ``CHANGELOG.rst`` with the new version number. From fd788cd9f20409bae52488fc9b7ed609206cd006 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 9 Oct 2025 07:59:36 +0100 Subject: [PATCH 09/11] Add pull request template --- .github/pull_request_template.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..27668843 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ +[ ] Add tests for the change. In general, aim for full test coverage at the Python level. Rust tests are optional. +[ ] Add any appropriate documentation. +[ ] Add a summary of changes to `CHANGELOG.rst`. +[ ] Add your name to `AUTHORS.rst`. +[ ] Run `just full-check`. \ No newline at end of file From 6e52d5e4e2be3bcc85e9c4a7b11a114b02bbb197 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 9 Oct 2025 09:25:41 +0100 Subject: [PATCH 10/11] Remove tox.ini --- tox.ini | 119 -------------------------------------------------------- 1 file changed, 119 deletions(-) delete mode 100644 tox.ini diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 8b4fb848..00000000 --- a/tox.ini +++ /dev/null @@ -1,119 +0,0 @@ -[tox] -envlist = - clean, - check, - docs, - {py39,py310,py311,py312,py313,py314}, - -[base] -deps = - pytest==7.4.4 - pyyaml==6.0.1 - pytest-cov==5.0.0 - pytest-benchmark==4.0.0 - syrupy==4.9.1 - # External packages to attempt to build the graph from. - Django==4.2.17 # N.B. Django 5 doesn't support Python 3.9. - flask==3.0.3 - requests==2.32.3 - sqlalchemy==2.0.35 - google-cloud-audit-log==0.3.0 - -[testenv] -basepython = - py39: {env:TOXPYTHON:python3.9} - py310: {env:TOXPYTHON:python3.10} - py311: {env:TOXPYTHON:python3.11} - py312: {env:TOXPYTHON:python3.12} - py313: {env:TOXPYTHON:python3.13} - py314: {env:TOXPYTHON:python3.14} - {clean,check,docs,report}: {env:TOXPYTHON:python3} -setenv = - PYTHONPATH={toxinidir}/tests - PYTHONUNBUFFERED=yes -passenv = - * -usedevelop = false -deps = - {[base]deps} - joblib==1.4.2 -commands = - {posargs:pytest --cov --cov-report=term-missing --benchmark-skip -vv tests} - -[testenv:check] -basepython = py313 -deps = - black==23.9.1 - flake8==7.1.1 - import-linter==2.3 - mypy==1.11.2 - pytest-stub==1.1.0 - types-pyyaml==6.0.12.20240917 -commands = - black --check src tests - flake8 src tests - mypy src/grimp tests - lint-imports - -[testenv:benchmark] -basepython = py314 -setenv = - PYTHONPATH={toxinidir}/tests - PYTHONUNBUFFERED=yes -passenv = - * -usedevelop = false -deps = - pytest==7.4.4 - PyYAML==6.0.1 - pytest-benchmark==4.0.0 - Django==5.1.1 -commands = - {posargs:pytest --benchmark-only --benchmark-autosave} - -[testenv:codspeed] -basepython = py314 -setenv = - PYTHONPATH={toxinidir}/tests - PYTHONUNBUFFERED=yes -passenv = - * -usedevelop = false -# Note - these dependencies are duplicated in main.yml, make sure to change them there too. -# TODO: switch to UV for dependency management. -deps = - pytest==7.4.4 - pyyaml==6.0.1 - pytest-codspeed==3.2.0 - Django==5.1.1 -commands = - pytest --codspeed {posargs} - - -[testenv:docs] -deps = - -r{toxinidir}/docs/requirements.txt -commands = - sphinx-build {posargs:-E} -b html docs dist/docs - sphinx-build -b linkcheck docs dist/docs - -[testenv:report] -deps = coverage==6.4.3 -skip_install = true -commands = - coverage report - coverage html - -[testenv:clean] -commands = coverage erase -skip_install = true -deps = coverage==6.4.3 - -[gh-actions] -python = - 3.9: py39, report - 3.10: py310, report - 3.11: py311, report - 3.12: py312, report - 3.13: py313, report, check, docs - 3.14: py314, report From a3894356d59d7421d714d6b07af06733c628f99f Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 9 Oct 2025 09:17:24 +0100 Subject: [PATCH 11/11] Update Github actions workflow --- .github/workflows/main.yml | 92 +++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 51 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9290edf4..54486245 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,8 +9,31 @@ on: workflow_dispatch: jobs: - check_python: - name: Check Python ${{ matrix.python-version }}, ${{ matrix.os }} + check: + runs-on: ubuntu-24.04 + name: Lint and check docs build + steps: + - uses: actions/checkout@v4 + + - name: Install just + uses: extractions/setup-just@v3 + + - name: Install uv and set the latest supported Python version + uses: astral-sh/setup-uv@v7 + with: + python-version: 3.14 + + - name: Lint Python + run: just lint-python + + - name: Lint Rust + run: just lint-rust + + - name: Check docs build + run: just build-docs + + test: + name: Run tests for Python ${{ matrix.python-version }}, ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: @@ -22,69 +45,36 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - uses: actions/setup-python@v6 + + - name: Install just + uses: extractions/setup-just@v3 + + - name: Install uv and set the Python version + uses: astral-sh/setup-uv@v7 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -VV - python -m site - python -m pip install --upgrade pip setuptools wheel - python -m pip install --upgrade coverage[toml] tox tox-gh-actions - - - name: Run tox targets for ${{ matrix.python-version }} - run: python -m tox - - check_rust: - name: Check Rust - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - name: Print versions - run: cargo version --verbose && cargo clippy --version - - name: Check formatting - run: cargo fmt --check - working-directory: ./rust - - name: Run tests - run: cargo test --no-default-features - working-directory: ./rust - - name: Run linter (clippy) - run: cargo clippy --all-targets --all-features -- -D warnings - working-directory: ./rust + + - name: Test Python + run: just test-python + + - name: Test Rust + run: just test-rust benchmarks: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 + - name: Install just + uses: extractions/setup-just@v3 + - name: Install uv uses: astral-sh/setup-uv@v7 - - name: Setup python - uses: actions/setup-python@v6 - with: - python-version: "3.13" - allow-prereleases: true - - # Temporarily install hardcoded dependencies here. - # Codspeed doesn't work well with tox, as it runs the tox installation process as part of the benchmarking - # process, which is very slow. - - name: Install dependencies - run: | - python -VV - uv venv - uv pip install pytest==7.4.4 pyyaml==6.0.1 pytest-codspeed==3.2.0 Django==5.1.1 /home/runner/work/grimp/grimp - - name: Run benchmarks uses: CodSpeedHQ/action@v4 with: token: ${{ secrets.CODSPEED_TOKEN }} mode: instrumentation run: | - uv run pytest tests/benchmarking/ --codspeed \ No newline at end of file + just benchmark-ci \ No newline at end of file