diff --git a/.github/actions/setup_rye/action.yml b/.github/actions/setup_rye/action.yml
deleted file mode 100644
index 83ea135f..00000000
--- a/.github/actions/setup_rye/action.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-name: Set up rye
-runs:
- using: 'composite'
- steps:
- # now install rye and sync the dependencies
- - uses: eifinger/setup-rye@v4
- with:
- version: "0.42.0"
- enable-cache: false
- - run: rye sync
- shell: ${{ runner.os == 'Windows' && 'powershell' || 'bash' }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 814fd9d5..b53e8bc7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -37,26 +37,51 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: ./.github/actions/setup_rye
- - run: rye run mypy:all
+ - uses: actions/setup-python@v5
+ with:
+ python-version-file: ".python-version"
+ - run: python -m pip install tox tox-uv
+ - run: tox -e mypy
pytest:
- name: Pytest (${{ matrix.os }})
+ name: "Pytest: ${{ matrix.tox-env }} (${{ matrix.os }})"
strategy:
fail-fast: false
matrix:
include:
+ # minimal versions
- os: ubuntu-latest
+ python: "3.12"
+ tox-env: py312-sphinx7-needs5
+ # maximal versions
+ - os: ubuntu-latest
+ python: "3.14"
+ tox-env: py314-sphinx8-needs8
+ # minimal version on Windows
+ - os: windows-latest
+ python: "3.14"
+ tox-env: py312-sphinx7-needs5
+ # maximal versions on other OSes
- os: ubuntu-24.04-arm
+ python: "3.14"
+ tox-env: py314-sphinx8-needs8
- os: windows-latest
+ python: "3.14"
+ tox-env: py314-sphinx8-needs8
- os: macos-latest
+ python: "3.14"
+ tox-env: py314-sphinx8-needs8
- runs-on: ["${{ matrix.os }}"]
+ runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- - uses: ./.github/actions/setup_rye
- - run: rye test -a
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "${{ matrix.python }}"
+ allow-prereleases: true
+ - run: python -m pip install tox tox-uv
+ - run: tox -e "${{ matrix.tox-env }}"
docs:
name: Documentation build
@@ -64,9 +89,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: ./.github/actions/setup_rye
- - name: Run documentation build
- run: rye run docs
+ - uses: actions/setup-python@v5
+ with:
+ python-version-file: ".python-version"
+ - run: python -m pip install tox tox-uv
+ - run: tox -e docs-clean
all_good:
# This job does nothing and is only used for the branch protection
diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
index 554913f1..c01eeb7b 100644
--- a/.github/workflows/coverage.yaml
+++ b/.github/workflows/coverage.yaml
@@ -12,12 +12,14 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
+ with:
+ python-version-file: ".python-version"
- - name: Setup rye
- uses: ./.github/actions/setup_rye
+ - name: Install tox
+ run: python -m pip install tox tox-uv
- name: Run tests
- run: rye test -a -- --cov --cov-branch --cov-report=xml
+ run: tox -e py312-sphinx8-needs5 -- --cov --cov-branch --cov-report=xml
- name: Upload results to Codecov
uses: codecov/codecov-action@v5
diff --git a/.github/workflows/gh_pages.yml b/.github/workflows/gh_pages.yml
index 3f1ab4e6..b09a7928 100644
--- a/.github/workflows/gh_pages.yml
+++ b/.github/workflows/gh_pages.yml
@@ -31,10 +31,12 @@ jobs:
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- - uses: eifinger/setup-rye@v4
- - run: rye sync
+ - uses: actions/setup-python@v5
+ with:
+ python-version-file: ".python-version"
+ - run: python -m pip install tox tox-uv
- name: Run documentation build
- run: rye run docs
+ run: tox -e docs-clean
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
diff --git a/.gitignore b/.gitignore
index 2f77aa73..841eb1b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+.DS_Store
+
# python generated files
.ruff_cache
.pytest_cache
@@ -17,7 +19,7 @@ requirements-dev.lock
# Sphinx build output
**/_build
-# rye is the primary tool, uv is only used for on-the-fly setups
+# uv lock file
uv.lock
# coverage files
@@ -26,3 +28,5 @@ coverage.xml
invalid_objs.json
.tox
+
+output/
diff --git a/AGENTS.md b/AGENTS.md
index ce038999..ef2607c3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -67,24 +67,27 @@ All commands should be run via [`tox`](https://tox.wiki) for consistency. The pr
### Testing
+Test environments follow the pattern `py{VERSION}-sphinx{MAJOR}-needs{MAJOR}`,
+e.g. `py312-sphinx8-needs5`. Use `tox -a` to list all available combinations.
+
```bash
# Run default test environment
tox
-# Run tests for specific Python/Sphinx combination
-tox -e py312-sphinx8
+# Run tests for a specific Python/Sphinx/sphinx-needs combination
+tox -e py312-sphinx8-needs5
# Run a specific test file
-tox -e py312-sphinx8 -- tests/test_analyse.py
+tox -e py312-sphinx8-needs5 -- tests/test_analyse.py
# Run a specific test function
-tox -e py312-sphinx8 -- tests/test_analyse.py::test_function_name
+tox -e py312-sphinx8-needs5 -- tests/test_analyse.py::test_function_name
# Run with coverage
-tox -e py312-sphinx8 -- --cov=sphinx_codelinks
+tox -e py312-sphinx8-needs5 -- --cov=sphinx_codelinks
# Update snapshot test fixtures
-tox -e py312-sphinx8 -- --snapshot-update
+tox -e py312-sphinx8-needs5 -- --snapshot-update
```
### Documentation
@@ -385,10 +388,10 @@ The CLI uses Typer for command definitions:
## Debugging
-- Use `--pdb` with pytest to drop into debugger on failures: `tox -e py312-sphinx8 -- --pdb`
-- Use `-v` for verbose test output: `tox -e py312-sphinx8 -- -v`
+- Use `--pdb` with pytest to drop into debugger on failures: `tox -e py312-sphinx8-needs5 -- --pdb`
+- Use `-v` for verbose test output: `tox -e py312-sphinx8-needs5 -- -v`
- Build docs with `-T` flag for full tracebacks: `tox -e docs-clean -- -T`
-- Set logging level in tests: `tox -e py312-sphinx8 -- --log-cli-level=DEBUG`
+- Set logging level in tests: `tox -e py312-sphinx8-needs5 -- --log-cli-level=DEBUG`
- Use `debug.py` module functions for development debugging
## Common Patterns
diff --git a/docs/source/development/change_log.rst b/docs/source/development/change_log.rst
index be24aa03..e9f20e27 100644
--- a/docs/source/development/change_log.rst
+++ b/docs/source/development/change_log.rst
@@ -3,6 +3,11 @@
Changelog
=========
+Upcoming
+--------
+
+- ⬆️ Support and test sphinx-needs v5-8
+
.. _`release:1.2.0`:
1.2.0
diff --git a/docs/source/development/contributing.rst b/docs/source/development/contributing.rst
index 4ca31a9f..445ec3bf 100644
--- a/docs/source/development/contributing.rst
+++ b/docs/source/development/contributing.rst
@@ -22,13 +22,14 @@ Your PR should conform with the following rules:
Install Dependencies
--------------------
-``CodeLinks`` uses `rye `_ to manage the repository.
+``CodeLinks`` uses `tox `_ (with `tox-uv `_) to manage development tasks.
-For development, use the following command to install Python dependencies into the virtual environment.
+Install tox with pip or uv:
.. code-block:: bash
- rye sync
+ pip install tox tox-uv
+ uv tool install tox --with tox-uv
Formatting, Linting and Typing
------------------------------
@@ -44,7 +45,7 @@ The CI also checks typing. Use the following command locally to see if your code
.. code-block:: bash
- rye run mypy:all
+ tox -e mypy
Build docs
----------
@@ -53,7 +54,7 @@ To build the documentation stored in ``docs``, run:
.. code-block:: bash
- rye run docs
+ tox -e docs-clean
Test Cases
----------
@@ -62,7 +63,7 @@ To run test cases locally:
.. code-block:: bash
- rye test -a
+ tox -e py312-sphinx8
Note some tests use `syrupy `__ to perform snapshot testing.
These snapshots can be updated by running:
diff --git a/pyproject.toml b/pyproject.toml
index cdfbe5d2..c854d7ff 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,7 +17,7 @@ dependencies = [
"click < 8.2", # click 8.2.* produces empty errors if no args are given
"jsonschema",
"sphinx>=7.4,<9",
- "sphinx-needs>=4.2.0", # unconstrained versions, to be pinned by user or Sphinx
+ "sphinx-needs>=5,<9",
"jinja2",
"pygments",
"docutils", # constrained by user or Sphinx
@@ -66,64 +66,9 @@ mypy = [
ruff = ["ruff>=0.8.0"]
build = ["simple-build>=0.0.2", "shiv>=1.0.8"]
-[tool.rye]
-managed = true
-dev-dependencies = [
- "types-docutils",
- "types-Pygments",
- "syrupy>=4.9.1",
- "furo>=2024.5.6",
- "moto ~= 5.0",
- "mypy>=1.12.1",
- "myst-parser>=4.0.0",
- "pydantic ~= 2.9",
- "pip-licenses>=5.0.0",
- "psutil>=7.0.0",
- "pytest-cov>=5.0.0",
- "pytest>=8.2.2",
- "simple-build>=0.0.2",
- "sphinx-design>=0.6.1",
- "types-psutil>=7.0.0.20250218",
- "uv>=0.5.5",
- "pytest-docker>=3.1.2",
- "shiv>=1.0.8",
- "insta-science>=0.2.1",
- "types-jsonschema>=4.23.0.20241208",
- "toml>=0.10.2",
- "sphinx-code-tabs>=0.5.5",
- "sphinxcontrib-typer>=0.5.1",
- "sphinxcontrib-video>=0.4.1",
-]
-
[project.scripts]
codelinks = "sphinx_codelinks.cmd:app"
-[tool.rye.scripts]
-# linting and formatting
-"mypy:all" = "mypy ."
-"rye:lint" = "rye lint"
-"rye:format" = "rye format"
-"check" = { chain = ["rye:format", "rye:lint", "mypy:all"] }
-# docs html
-"docs:rm" = "rm -rf docs/_build/html"
-"docs" = "sphinx-build -nW --keep-going -b html -T -c docs docs/source docs/_build/html"
-"docs:clean" = { chain = ["docs:rm", "docs"] }
-# needextend demo
-"analyse" = "codelinks analyse tests/data/configs/minimum_config.toml"
-"analyse:rm" = "rm -rf output/marked_content.json"
-"write" = "codelinks write rst output/marked_content.json --outpath tests/data/needextend_demo/needextend.rst"
-"write:rm" = "rm -rf tests/data/needextend_demo/needextend.rst"
-"demo:rm" = "rm -rf tests/data/needextend_demo/_build"
-"demo:build" = "sphinx-build -nW --keep-going -b html -T -c tests/data/needextend_demo tests/data/needextend_demo tests/data/needextend_demo/_build/html"
-"demo:clean" = { chain = [
- "demo:rm",
- "analyse:rm",
- "write:rm",
- "analyse",
- "write",
- "demo:build",
-] }
-
[tool.ruff.lint]
extend-select = [
# "ANN",
@@ -162,15 +107,6 @@ force-sort-within-sections = true
"PLR2004", # magic-value-comparison - valueable for tests
"S101", # assert - needed for tests
]
-"**/build_hooks_*/**" = [
- "S607", # start-process-with-partial-path - pyarmor call in rye context
- "S603", # subprocess-without-shell-equals-true - pyarmor call
-]
-"scripts/*.py" = [
- "T201", # print - used for output
- "S607", # start-process-with-partial-path - pyarmor call in rye context
- "S603", # subprocess-without-shell-equals-true - build scripts
-]
"src/sphinx_codelinks/sphinx_extension/debug.py" = [
"T201", # print - used for output
"UP047", # on-pep695-generic-function - it's generic
diff --git a/tests/data/needextend_demo/README.md b/tests/data/needextend_demo/README.md
index 40706357..3b53f537 100644
--- a/tests/data/needextend_demo/README.md
+++ b/tests/data/needextend_demo/README.md
@@ -3,5 +3,5 @@
Run the following command in project root to have demo generated
```bash
-rye run demo:clean
+tox -e demo
```
diff --git a/tests/data/needextend_demo/conf.py b/tests/data/needextend_demo/conf.py
index 2cef3d08..71c153d4 100644
--- a/tests/data/needextend_demo/conf.py
+++ b/tests/data/needextend_demo/conf.py
@@ -26,7 +26,7 @@
extensions = ["sphinx_needs", "sphinx_codelinks"]
# for needextend to work with remote url
-needs_extra_options = ["remote_url"]
+needs_fields = {"remote_url": {"default": ""}}
needs_string_links = {
"custom_name": {
diff --git a/tests/test_cmd.py b/tests/test_cmd.py
index debc481f..edc62a32 100644
--- a/tests/test_cmd.py
+++ b/tests/test_cmd.py
@@ -1,6 +1,7 @@
# @Test suite for CLI commands including analyse, discover, and write, TEST_CLI_1, test, [IMPL_CLI_ANALYZE, IMPL_CLI_DISCOVER, IMPL_CLI_WRITE]
import json
from pathlib import Path
+import re
import pytest
import toml
@@ -46,6 +47,17 @@
runner = CliRunner()
+def _normalize_output(text: str) -> str:
+ """Normalize rich panel output by collapsing box-drawing chars and whitespace.
+
+ Typer wraps error messages in rich panels whose line breaks depend on terminal
+ width, which can cause substring assertions to fail.
+ """
+ # Remove box-drawing characters (─│╭╮╯╰) and collapse resulting whitespace
+ text = re.sub(r"[─│╭╮╯╰]", " ", text)
+ return re.sub(r"\s+", " ", text).strip()
+
+
@pytest.mark.parametrize(
("config_path"),
[
@@ -215,8 +227,9 @@ def test_analyse_config_negative(
]
result = runner.invoke(app, options)
assert result.exit_code != 0
+ normalized = _normalize_output(result.stdout)
for line in output_lines:
- assert line in result.stdout
+ assert line in normalized
@pytest.mark.parametrize(
@@ -248,8 +261,9 @@ def test_analyse_project_negative(projects, output_lines, tmp_path: Path) -> Non
options.extend(projects_config)
result = runner.invoke(app, options)
assert result.exit_code != 0
+ normalized = _normalize_output(result.stdout)
for line in output_lines:
- assert line in result.stdout
+ assert line in normalized
def test_write_rst_invalid_json(tmp_path: Path) -> None:
@@ -318,8 +332,9 @@ def test_write_rst_negative(json_objs: list[dict], output_lines, tmp_path) -> No
result = runner.invoke(app, options)
assert result.exit_code != 0
+ normalized = _normalize_output(result.stdout)
for line in output_lines:
- assert line in result.stdout
+ assert line in normalized
def test_analyse_with_relative_git_root(tmp_path: Path) -> None:
diff --git a/tox.ini b/tox.ini
index b1aef422..97c5b545 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,16 +7,20 @@
# `tox -r`
[tox]
-envlist = py312-sphinx8
+envlist = py312-sphinx8-needs5
[testenv]
usedevelop = true
-[testenv:py{312,313,314}-sphinx{7,8,9}]
+[testenv:py{312,313,314}-sphinx{7,8,9}-needs{5,6,7,8}]
deps =
sphinx7: sphinx>=7.4,<8
sphinx8: sphinx>=8,<9
sphinx9: sphinx>=9,<10
+ needs5: sphinx-needs>=5,<6
+ needs6: sphinx-needs>=6,<7
+ needs7: sphinx-needs>=7,<8
+ needs8: sphinx-needs>=8,<9
dependency_groups = testing
commands = pytest {posargs}
@@ -54,3 +58,15 @@ commands = ruff check {posargs:--fix}
skip_install = true
dependency_groups = ruff
commands = ruff format {posargs}
+
+[testenv:demo]
+description = Run the needextend demo (analyse, write, build)
+dependency_groups = docs
+allowlist_externals = rm
+commands =
+ rm -rf tests/data/needextend_demo/_build
+ rm -rf output/marked_content.json
+ rm -rf tests/data/needextend_demo/needextend.rst
+ codelinks analyse tests/data/configs/minimum_config.toml
+ codelinks write rst output/marked_content.json --outpath tests/data/needextend_demo/needextend.rst
+ sphinx-build -nW --keep-going -b html -T -c tests/data/needextend_demo tests/data/needextend_demo tests/data/needextend_demo/_build/html