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