Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,19 @@ jobs:
python-version: ${{ matrix.python }}
- run: sudo apt-get install -y doxygen
- run: pip install hatch
- run: hatch run +py=${{ matrix.python }} test:all
- run: hatch run test.py${{ matrix.python }}:all

test-sphinx:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sphinx: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.11'
- run: sudo apt-get install -y doxygen
- run: pip install hatch
- run: hatch run test.py3.11-${{ matrix.sphinx }}:all
2 changes: 1 addition & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
v1.0.0
* Require Python 3.11+ and Sphinx 9.0+
* Require Python 3.11+ and Sphinx 7.2+
* Rename the domain from csharp to cs
* Rename the method directive to function, and value to enumerator
* Support documenting C# APIs generated by Doxygen with Breathe
Expand Down
11 changes: 9 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "djungelorm", email = "djungelorm@users.noreply.github.com" }]
requires-python = ">=3.11"
dependencies = ["Sphinx>=9.0", "requests"]
dependencies = ["Sphinx>=7.2", "requests"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
Expand Down Expand Up @@ -46,11 +46,18 @@ dependencies = ["mypy", "types-docutils", "types-requests"]
check = "mypy sphinx_csharp"

[tool.hatch.envs.test]
dependencies = ["breathe", "pytest", "pytest-cov"]
dependencies = ["breathe", "defusedxml", "pytest", "pytest-cov"]

[[tool.hatch.envs.test.matrix]]
python = ["3.11", "3.12", "3.13", "3.14", "3.15"]

[[tool.hatch.envs.test.matrix]]
python = ["3.11"]
sphinx = ["7.2", "7.3", "7.4", "8.0", "8.1", "8.2"]

[tool.hatch.envs.test.overrides]
matrix.sphinx.dependencies = ["sphinx~={matrix:sphinx}.0"]

[tool.hatch.envs.test.scripts]
all = "pytest --cov=sphinx_csharp --cov-report=term-missing {args}"

Expand Down
8 changes: 6 additions & 2 deletions sphinx_csharp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from importlib.metadata import version
from typing import TYPE_CHECKING

from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata

from .csharp import CSharpDomain
from .debug import CSDebug

# Note: ExtensionMetadata was added in Sphinx 7.3.
if TYPE_CHECKING:
from sphinx.util.typing import ExtensionMetadata

def setup(app: Sphinx) -> ExtensionMetadata:

def setup(app: Sphinx) -> 'ExtensionMetadata':
""" Setup called by sphinx when the module is added to the extensions list in conf.py """

# Register domain
Expand Down
3 changes: 2 additions & 1 deletion sphinx_csharp/csharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,8 @@ def add_target_and_index(self, name: str, _: str, signode: addnodes.desc_signatu
location=(self.env.docname, self.lineno))
objects[key] = self.env.docname

if 'no-index-entry' not in self.options:
# Sphinx 7 and 8 keep noindexentry as a separate option
if 'no-index-entry' not in self.options and 'noindexentry' not in self.options:
indextext = self.get_index_text(name)
if indextext:
self.indexnode['entries'].append(('single', indextext, targetname, '', None))
Expand Down
19 changes: 15 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest
from docutils import nodes
from sphinx import addnodes
from sphinx import addnodes, version_info

pytest_plugins = ['sphinx.testing.fixtures']

Expand All @@ -27,21 +27,32 @@ def _build(rst, pages=None, parallel=0, **confoverrides):
return _build


def resolved_doctree(app):
# Sphinx 9 deprecates calls without tags, and earlier versions reject the argument
kwargs = {'tags': app.tags} if version_info >= (9,) else {}
return app.env.get_and_resolve_doctree('index', app.builder, **kwargs)


# Note: SphinxTestApp.status and SphinxTestApp.warning were added in Sphinx 7.3.
def status(app):
return app._status.getvalue()


def warnings(app):
return [line for line in app.warning.getvalue().splitlines() if line.strip()]
return [line for line in app._warning.getvalue().splitlines() if line.strip()]


def signatures(app):
""" Map each object id on the index page to its rendered signature text """
doctree = app.env.get_and_resolve_doctree('index', app.builder, tags=app.tags)
doctree = resolved_doctree(app)
return {node_id: node.astext().replace('\xa0', ' ')
for node in doctree.findall(addnodes.desc_signature)
for node_id in node['ids']}


def references(app, node_id=None):
""" Map each linked name to its target, in one signature or in the whole index page """
doctree = app.env.get_and_resolve_doctree('index', app.builder, tags=app.tags)
doctree = resolved_doctree(app)
if node_id is not None:
doctree = next(node for node in doctree.findall(addnodes.desc_signature) if node_id in node['ids'])
return {ref.astext(): ref.get('refid') or ref.get('refuri')
Expand Down
14 changes: 7 additions & 7 deletions tests/test_extrefs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest
import requests
from conftest import references, warnings
from conftest import references, status, warnings

from sphinx_csharp.debug import CSDebug
from sphinx_csharp.extrefs import ExternalRefs
Expand Down Expand Up @@ -48,9 +48,9 @@ def test_package_without_search_page(build):

def test_debug_logging(build):
app = build('.. cs:function:: int Foo (int a)\n', sphinx_csharp_debug_ext_links=True)
status = app.status.getvalue()
assert 'found external link' in status
assert 'found in extlink_cache for int' in status
log = status(app)
assert 'found external link' in log
assert 'found in extlink_cache for int' in log


class Response:
Expand All @@ -67,8 +67,8 @@ def test_link_testing(build, monkeypatch, status_code, url):
monkeypatch.setattr(requests, 'get', lambda *args, **kwargs: Response(status_code))
refs, app = links(build, 'int', sphinx_csharp_test_links=True)
assert refs == {'int': url}
assert 'external link testing is enabled' in app.status.getvalue()
assert ('invalid API link' in app.warning.getvalue()) == (status_code == 404)
assert 'external link testing is enabled' in status(app)
assert any('invalid API link' in line for line in warnings(app)) == (status_code == 404)


def test_link_testing_without_connection(build, monkeypatch):
Expand All @@ -78,6 +78,6 @@ def get(*args, **kwargs):
monkeypatch.setattr(requests, 'get', get)
refs, app = links(build, 'int', sphinx_csharp_test_links=True)
assert warnings(app) == []
assert 'external link testing is enabled' not in app.status.getvalue()
assert 'external link testing is enabled' not in status(app)
assert refs == {'int': MICROSOFT_DOCS + 'System.Int32'}
assert not ExternalRefs.test_links
3 changes: 2 additions & 1 deletion tests/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def test_basic(app):


@pytest.mark.skipif(shutil.which('doxygen') is None, reason='doxygen is not installed')
@pytest.mark.sphinx('html', testroot='breathe', copy_test_root=True, confoverrides={'nitpicky': True})
# Note: Passing srcdir copies the test root in every supported Sphinx version.
@pytest.mark.sphinx('html', testroot='breathe', srcdir='breathe', confoverrides={'nitpicky': True})
def test_breathe(app):
subprocess.run(['doxygen'], cwd=app.srcdir, check=True)
app.build()
Expand Down
13 changes: 7 additions & 6 deletions tests/test_render.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from io import StringIO

import pytest
from conftest import references, signatures, warnings
from conftest import references, resolved_doctree, signatures, status, warnings
from docutils import nodes
from sphinx import addnodes
from sphinx.errors import ExtensionError
Expand Down Expand Up @@ -118,7 +118,7 @@ def test_invalid_signature_is_shown_as_written(build, directive, sig):
app = build(f'.. cs:{directive}:: {sig}\n')
assert len(warnings(app)) == 1
assert 'signature invalid' in warnings(app)[0]
doctree = app.env.get_and_resolve_doctree('index', app.builder, tags=app.tags)
doctree = resolved_doctree(app)
assert next(doctree.findall(addnodes.desc_signature)).astext() == sig


Expand Down Expand Up @@ -255,8 +255,9 @@ def test_index_entries(app):
assert len(entries) == 10


def test_no_index_entry(build):
app = build('.. cs:class:: Shown\n\n.. cs:class:: Hidden\n :no-index-entry:\n\n'
@pytest.mark.parametrize('option', ['no-index-entry', 'noindexentry'])
def test_no_index_entry(build, option):
app = build(f'.. cs:class:: Shown\n\n.. cs:class:: Hidden\n :{option}:\n\n'
'.. cs:class:: Derived\n\n .. cs:inherits:: Shown\n')
assert warnings(app) == []
assert index_entries(app) == [('Shown (C# class)', 'class-Shown'), ('Derived (C# class)', 'class-Derived')]
Expand Down Expand Up @@ -345,10 +346,10 @@ def test_reference_without_target(build):
def test_debug_logging(build):
app = build(DEBUG_SOURCE, nitpicky=False, sphinx_csharp_debug=True)
assert warnings(app) == []
status = app.status.getvalue()
log = status(app)
for text in ['parsed func:', 'parsed var:', 'parsed prop:', 'falling back to variable: int Plain',
'parsed idxr:', 'parsed type:', 'parsed attr:', 'no C# object found for Missing']:
assert text in status
assert text in log


def test_invalid_config(make_app, tmp_path):
Expand Down
Loading