From 8a51c0a508fabc0936420dcd9d88f3bca4111d83 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 10:37:28 -0700 Subject: [PATCH 01/12] DEP: Mv sphinx dep to optional. --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b8911d85..ad9ec8c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,6 @@ classifiers = [ 'Topic :: Documentation', ] dependencies = [ - 'sphinx>=6', "tomli>=1.1.0;python_version<'3.11'", ] @@ -43,6 +42,9 @@ Homepage = 'https://numpydoc.readthedocs.io' Source = 'https://github.com/numpy/numpydoc/' [dependency-groups] +default = [ + 'sphinx>=6', +] dev = [ 'pre-commit>=3.3', "tomli; python_version < '3.11'", From fb070f95fbc35ac19c3882e0facd1e7ea137b281 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 10:41:46 -0700 Subject: [PATCH 02/12] MAINT: Make app setup optional in __init__. --- numpydoc/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/numpydoc/__init__.py b/numpydoc/__init__.py index 5458b67f..ce164700 100644 --- a/numpydoc/__init__.py +++ b/numpydoc/__init__.py @@ -6,7 +6,16 @@ from ._version import __version__ -def setup(app, *args, **kwargs): - from .numpydoc import setup +# NOTE: Determine whether sphinx is installed with an explicit import. +# If so, define the setup function for registering the numpydoc extension; +# otherwise skip this step. +try: + import sphinx - return setup(app, *args, **kwargs) + + def setup(app, *args, **kwargs): + from .numpydoc import setup + + return setup(app, *args, **kwargs) +except ModuleNotFoundError: + pass From 03c75938a9a926da63d45902c78cf771e23ab097 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Thu, 13 Nov 2025 15:38:42 -0800 Subject: [PATCH 03/12] Split history numpydoc/tests/test_docscrape.py to numpydoc/tests/test_docscrape_sphinx.py --- numpydoc/tests/{test_docscrape.py => test_docscrape_sphinx.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename numpydoc/tests/{test_docscrape.py => test_docscrape_sphinx.py} (100%) diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape_sphinx.py similarity index 100% rename from numpydoc/tests/test_docscrape.py rename to numpydoc/tests/test_docscrape_sphinx.py From c9683e4ee68983eeccd906bc6ee3aa3cf234fc96 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Thu, 13 Nov 2025 15:38:42 -0800 Subject: [PATCH 04/12] Split history numpydoc/tests/test_docscrape.py to numpydoc/tests/test_docscrape_sphinx.py --- numpydoc/tests/test_docscrape.py => temp | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename numpydoc/tests/test_docscrape.py => temp (100%) diff --git a/numpydoc/tests/test_docscrape.py b/temp similarity index 100% rename from numpydoc/tests/test_docscrape.py rename to temp From e61875f0a5845b2bb78c4c9a83d107c12df7f7c9 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Thu, 13 Nov 2025 15:38:42 -0800 Subject: [PATCH 05/12] Split history numpydoc/tests/test_docscrape.py to numpydoc/tests/test_docscrape_sphinx.py --- temp => numpydoc/tests/test_docscrape.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename temp => numpydoc/tests/test_docscrape.py (100%) diff --git a/temp b/numpydoc/tests/test_docscrape.py similarity index 100% rename from temp rename to numpydoc/tests/test_docscrape.py From 3c4ffafe622d26f36d3138de6f885924c3ec7708 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 12:04:26 -0700 Subject: [PATCH 06/12] TST: Split docscrape tests based on sphinx dep. --- numpydoc/tests/test_docscrape.py | 702 ++---------------------- numpydoc/tests/test_docscrape_sphinx.py | 395 ++----------- 2 files changed, 113 insertions(+), 984 deletions(-) diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index cf7c0de9..6c81f952 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -4,19 +4,10 @@ from collections import namedtuple from copy import deepcopy -import jinja2 import pytest from pytest import warns as assert_warns -from numpydoc.docscrape import ClassDoc, FunctionDoc, NumpyDocString -from numpydoc.docscrape_sphinx import ( - SphinxClassDoc, - SphinxDocString, - SphinxFunctionDoc, - get_doc_object, -) -from numpydoc.numpydoc import update_config -from numpydoc.xref import DEFAULT_LINKS +from numpydoc.docscrape import ClassDoc, FunctionDoc, NumpyDocString, get_doc_object doc_txt = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) @@ -338,10 +329,10 @@ def dummy_func(arg): """ with pytest.raises(ValueError, match="Dummy class"): - SphinxClassDoc(Dummy) + ClassDoc(Dummy) with pytest.raises(ValueError, match="dummy_func"): - SphinxFunctionDoc(dummy_func) + FunctionDoc(dummy_func) def test_notes(doc): @@ -570,153 +561,6 @@ def test_no_index_in_str(): ) -def test_sphinx_str(): - sphinx_doc = SphinxDocString(doc_txt) - line_by_line_compare( - str(sphinx_doc), - """ -.. index:: random - single: random;distributions, random;gauss - -Draw values from a multivariate normal distribution with specified -mean and covariance. - -The multivariate normal or Gaussian distribution is a generalisation -of the one-dimensional normal distribution to higher dimensions. - -:Parameters: - - **mean** : (N,) ndarray - Mean of the N-dimensional distribution. - - .. math:: - - (1+2+3)/3 - - **cov** : (N, N) ndarray - Covariance matrix of the distribution. - - **shape** : tuple of ints - Given a shape of, for example, (m,n,k), m*n*k samples are - generated, and packed in an m-by-n-by-k arrangement. Because - each sample is N-dimensional, the output shape is (m,n,k,N). - - **dtype** : data type object, optional (default : float) - The type and size of the data to be returned. - -:Returns: - - **out** : ndarray - The drawn samples, arranged according to `shape`. If the - shape given is (m,n,...), then the shape of `out` is - (m,n,...,N). - - In other words, each entry ``out[i,j,...,:]`` is an N-dimensional - value drawn from the distribution. - - list of str - This is not a real return value. It exists to test - anonymous return values. - - no_description - .. - -:Other Parameters: - - **spam** : parrot - A parrot off its mortal coil. - -:Raises: - - RuntimeError - Some error - -:Warns: - - RuntimeWarning - Some warning - -.. warning:: - - Certain warnings apply. - -.. seealso:: - - :obj:`some`, :obj:`other`, :obj:`funcs` - .. - :obj:`otherfunc` - relationship - :py:meth:`spyder.widgets.mixins.GetHelpMixin.show_object_info` - .. - -.. rubric:: Notes - -Instead of specifying the full covariance matrix, popular -approximations include: - - - Spherical covariance (`cov` is a multiple of the identity matrix) - - Diagonal covariance (`cov` has non-negative elements only on the diagonal) - -This geometrical property can be seen in two dimensions by plotting -generated data-points: - ->>> mean = [0,0] ->>> cov = [[1,0],[0,100]] # diagonal covariance, points lie on x or y-axis - ->>> x,y = multivariate_normal(mean,cov,5000).T ->>> plt.plot(x,y,'x'); plt.axis('equal'); plt.show() - -Note that the covariance matrix must be symmetric and non-negative -definite. - -.. rubric:: References - -.. [1] A. Papoulis, "Probability, Random Variables, and Stochastic - Processes," 3rd ed., McGraw-Hill Companies, 1991 -.. [2] R.O. Duda, P.E. Hart, and D.G. Stork, "Pattern Classification," - 2nd ed., Wiley, 2001. - -.. only:: latex - - [1]_, [2]_ - -.. rubric:: Examples - ->>> mean = (1,2) ->>> cov = [[1,0],[1,0]] ->>> x = multivariate_normal(mean,cov,(3,3)) ->>> print(x.shape) -(3, 3, 2) - -The following is probably true, given that 0.6 is roughly twice the -standard deviation: - ->>> print(list((x[0, 0, :] - mean) < 0.6)) -[True, True] -""", - ) - - -def test_sphinx_yields_str(): - sphinx_doc = SphinxDocString(doc_yields_txt) - line_by_line_compare( - str(sphinx_doc), - """Test generator - -:Yields: - - **a** : int - The number of apples. - - **b** : int - The number of bananas. - - int - The number of unknowns. -""", - ) - - doc2 = NumpyDocString( """ Returns array of indices of the maximum values of along the given axis. @@ -945,13 +789,6 @@ class BadSection: NumpyDocString(doc_text) assert len(record) == 1 - # SphinxClassDoc has _obj.__name__ == "BadSection". Test that this is - # included in the message - msg_match = "Unknown section Nope in the docstring of BadSection" - with pytest.warns(UserWarning, match=msg_match) as record: - SphinxClassDoc(BadSection) - assert len(record) == 1 - doc7 = NumpyDocString( """ @@ -999,18 +836,8 @@ def test_trailing_colon(): assert doc8["Parameters"][0].name == "data" -def test_no_summary(): - str( - SphinxDocString( - """ - Parameters - ----------""" - ) - ) - - def test_unicode(): - doc = SphinxDocString( + doc = NumpyDocString( """ öäöäöäöäöåååå @@ -1032,48 +859,6 @@ def test_unicode(): assert doc["Summary"][0] == "öäöäöäöäöåååå" -def test_plot_examples(): - cfg = dict(use_plots=True) - - doc = SphinxDocString( - """ - Examples - -------- - >>> import matplotlib.pyplot as plt - >>> plt.plot([1,2,3],[4,5,6]) - >>> plt.show() - """, - config=cfg, - ) - assert "plot::" in str(doc), str(doc) - - doc = SphinxDocString( - """ - Examples - -------- - >>> from matplotlib import pyplot as plt - >>> plt.plot([1,2,3],[4,5,6]) - >>> plt.show() - """, - config=cfg, - ) - assert "plot::" in str(doc), str(doc) - - doc = SphinxDocString( - """ - Examples - -------- - .. plot:: - - import matplotlib.pyplot as plt - plt.plot([1,2,3],[4,5,6]) - plt.show() - """, - config=cfg, - ) - assert str(doc).count("plot::") == 1, str(doc) - - def test_class_members(): class Dummy: """ @@ -1095,24 +880,20 @@ def spammity(self): class Ignorable: """local class, to be ignored""" - for cls in (ClassDoc, SphinxClassDoc): - doc = cls(Dummy, config=dict(show_class_members=False)) - assert "Methods" not in str(doc), (cls, str(doc)) - assert "spam" not in str(doc), (cls, str(doc)) - assert "ham" not in str(doc), (cls, str(doc)) - assert "spammity" not in str(doc), (cls, str(doc)) - assert "Spammity index" not in str(doc), (cls, str(doc)) - - doc = cls(Dummy, config=dict(show_class_members=True)) - assert "Methods" in str(doc), (cls, str(doc)) - assert "spam" in str(doc), (cls, str(doc)) - assert "ham" in str(doc), (cls, str(doc)) - assert "spammity" in str(doc), (cls, str(doc)) - - if cls is SphinxClassDoc: - assert ".. autosummary::" in str(doc), str(doc) - else: - assert "Spammity index" in str(doc), str(doc) + doc = ClassDoc(Dummy, config=dict(show_class_members=False)) + assert "Methods" not in str(doc), (ClassDoc, str(doc)) + assert "spam" not in str(doc), (ClassDoc, str(doc)) + assert "ham" not in str(doc), (ClassDoc, str(doc)) + assert "spammity" not in str(doc), (ClassDoc, str(doc)) + assert "Spammity index" not in str(doc), (ClassDoc, str(doc)) + + doc = ClassDoc(Dummy, config=dict(show_class_members=True)) + assert "Methods" in str(doc), (ClassDoc, str(doc)) + assert "spam" in str(doc), (ClassDoc, str(doc)) + assert "ham" in str(doc), (ClassDoc, str(doc)) + assert "spammity" in str(doc), (ClassDoc, str(doc)) + + assert "Spammity index" in str(doc), str(doc) class SubDummy(Dummy): """ @@ -1126,36 +907,29 @@ def ham(self, c, d): def bar(self, a, b): """Bar\n\nNo bar""" - for cls in (ClassDoc, SphinxClassDoc): - doc = cls( - SubDummy, - config=dict(show_class_members=True, show_inherited_class_members=False), - ) - assert "Methods" in str(doc), (cls, str(doc)) - assert "spam" not in str(doc), (cls, str(doc)) - assert "ham" in str(doc), (cls, str(doc)) - assert "bar" in str(doc), (cls, str(doc)) - assert "spammity" not in str(doc), (cls, str(doc)) - - if cls is SphinxClassDoc: - assert ".. autosummary::" in str(doc), str(doc) - else: - assert "Spammity index" not in str(doc), str(doc) - - doc = cls( - SubDummy, - config=dict(show_class_members=True, show_inherited_class_members=True), - ) - assert "Methods" in str(doc), (cls, str(doc)) - assert "spam" in str(doc), (cls, str(doc)) - assert "ham" in str(doc), (cls, str(doc)) - assert "bar" in str(doc), (cls, str(doc)) - assert "spammity" in str(doc), (cls, str(doc)) + doc = ClassDoc( + SubDummy, + config=dict(show_class_members=True, show_inherited_class_members=False), + ) + assert "Methods" in str(doc), (ClassDoc, str(doc)) + assert "spam" not in str(doc), (ClassDoc, str(doc)) + assert "ham" in str(doc), (ClassDoc, str(doc)) + assert "bar" in str(doc), (ClassDoc, str(doc)) + assert "spammity" not in str(doc), (ClassDoc, str(doc)) + + assert "Spammity index" not in str(doc), str(doc) + + doc = ClassDoc( + SubDummy, + config=dict(show_class_members=True, show_inherited_class_members=True), + ) + assert "Methods" in str(doc), (ClassDoc, str(doc)) + assert "spam" in str(doc), (ClassDoc, str(doc)) + assert "ham" in str(doc), (ClassDoc, str(doc)) + assert "bar" in str(doc), (ClassDoc, str(doc)) + assert "spammity" in str(doc), (ClassDoc, str(doc)) - if cls is SphinxClassDoc: - assert ".. autosummary::" in str(doc), str(doc) - else: - assert "Spammity index" in str(doc), str(doc) + assert "Spammity index" in str(doc), str(doc) def test_duplicate_signature(): @@ -1226,226 +1000,6 @@ def test_duplicate_signature(): """ -def test_class_members_doc(): - doc = ClassDoc(None, class_doc_txt) - line_by_line_compare( - str(doc), - """ - Foo - - Parameters - ---------- - f : callable ``f(t, y, *f_args)`` - Aaa. - jac : callable ``jac(t, y, *jac_args)`` - Bbb. - - Attributes - ---------- - t : float - Current time. - y : ndarray - Current variable values. - - * hello - * world - an_attribute : float - The docstring is printed instead - no_docstring : str - But a description - no_docstring2 : str - multiline_sentence - midword_period - no_period - - Methods - ------- - a - b - c - - Other Parameters - ---------------- - another parameter : str - This parameter is less important. - - Notes - ----- - Some notes about the class. - - Examples - -------- - For usage examples, see `ode`. - - """, - ) - - -def test_class_members_doc_sphinx(): - class Foo: - @property - def an_attribute(self): - """Test attribute""" - return - - @property - def no_docstring(self): - return None - - @property - def no_docstring2(self): - return None - - @property - def multiline_sentence(self): - """This is a - sentence. It spans multiple lines.""" - return - - @property - def midword_period(self): - """The sentence for numpy.org.""" - return - - @property - def no_period(self): - """This does not have a period - so we truncate its summary to the first linebreak - - Apparently. - """ - return - - doc = SphinxClassDoc(Foo, class_doc_txt) - line_by_line_compare( - str(doc), - """ - Foo - - :Parameters: - - **f** : callable ``f(t, y, *f_args)`` - Aaa. - - **jac** : callable ``jac(t, y, *jac_args)`` - Bbb. - - :Attributes: - - **t** : float - Current time. - - **y** : ndarray - Current variable values. - - * hello - * world - - :obj:`an_attribute ` : float - Test attribute - - **no_docstring** : str - But a description - - **no_docstring2** : str - .. - - :obj:`multiline_sentence ` - This is a sentence. - - :obj:`midword_period ` - The sentence for numpy.org. - - :obj:`no_period ` - This does not have a period - - .. rubric:: Methods - - ===== ========== - **a** - **b** - **c** - ===== ========== - - :Other Parameters: - - **another parameter** : str - This parameter is less important. - - .. rubric:: Notes - - Some notes about the class. - - .. rubric:: Examples - - For usage examples, see `ode`. - - """, - ) - - -def test_class_attributes_as_member_list(): - class Foo: - """ - Class docstring. - - Attributes - ---------- - an_attribute - Another description that is not used. - - """ - - @property - def an_attribute(self): - """Test attribute""" - return - - attr_doc = """:Attributes: - - :obj:`an_attribute ` - Test attribute""" - - assert attr_doc in str(SphinxClassDoc(Foo)) - assert "Another description" not in str(SphinxClassDoc(Foo)) - - attr_doc2 = """.. rubric:: Attributes - -.. autosummary:: - :toctree: - - an_attribute""" - - cfg = dict(attributes_as_param_list=False) - assert attr_doc2 in str(SphinxClassDoc(Foo, config=cfg)) - assert "Another description" not in str(SphinxClassDoc(Foo, config=cfg)) - - -def test_templated_sections(): - doc = SphinxClassDoc( - None, - class_doc_txt, - config={"template": jinja2.Template("{{examples}}\n{{parameters}}")}, - ) - line_by_line_compare( - str(doc), - """ - .. rubric:: Examples - - For usage examples, see `ode`. - - :Parameters: - - **f** : callable ``f(t, y, *f_args)`` - Aaa. - - **jac** : callable ``jac(t, y, *jac_args)`` - Bbb. - - """, - ) - - def test_nonstandard_property(): # test discovery of a property that does not satisfy isinstace(.., property) @@ -1471,158 +1025,6 @@ class Dummy: assert "test attribute" in str(doc) -def test_args_and_kwargs(): - cfg = dict() - doc = SphinxDocString( - """ - Parameters - ---------- - param1 : int - First parameter - *args : tuple - Arguments - **kwargs : dict - Keyword arguments - """, - config=cfg, - ) - line_by_line_compare( - str(doc), - r""" -:Parameters: - - **param1** : int - First parameter - - **\*args** : tuple - Arguments - - **\*\*kwargs** : dict - Keyword arguments - """, - ) - - -def test_autoclass(): - cfg = dict(show_class_members=True, show_inherited_class_members=True) - doc = SphinxClassDoc( - str, - """ -A top section before - -.. autoclass:: str - """, - config=cfg, - ) - line_by_line_compare( - str(doc), - r""" -A top section before - -.. autoclass:: str - -.. rubric:: Methods - - - """, - 5, - ) - - -xref_doc_txt = """ -Test xref in Parameters, Other Parameters and Returns - -Parameters ----------- -p1 : int - Integer value - -p2 : float, optional - Integer value - -Other Parameters ----------------- -p3 : list[int] - List of integers -p4 : :class:`pandas.DataFrame` - A dataframe -p5 : sequence of `int` - A sequence - -Returns -------- -out : array - Numerical return value -""" - - -xref_doc_txt_expected = r""" -Test xref in Parameters, Other Parameters and Returns - - -:Parameters: - - **p1** : :class:`python:int` - Integer value - - **p2** : :class:`python:float`, optional - Integer value - -:Returns: - - **out** : :obj:`array ` - Numerical return value - - -:Other Parameters: - - **p3** : :class:`python:list`\[:class:`python:int`] - List of integers - - **p4** : :class:`pandas.DataFrame` - A dataframe - - **p5** : :obj:`python:sequence` of `int` - A sequence -""" - - -def test_xref(): - xref_aliases = { - "sequence": ":obj:`python:sequence`", - } - - class Config: - def __init__(self, a, b): - self.numpydoc_xref_aliases = a - self.numpydoc_xref_aliases_complete = b - # numpydoc.update_config fails if this config option not present - self.numpydoc_validation_checks = set() - self.numpydoc_validation_exclude = set() - self.numpydoc_validation_exclude_files = set() - self.numpydoc_validation_overrides = dict() - - xref_aliases_complete = deepcopy(DEFAULT_LINKS) - for key, val in xref_aliases.items(): - xref_aliases_complete[key] = val - config = Config(xref_aliases, xref_aliases_complete) - app = namedtuple("config", "config")(config) - update_config(app) - - xref_ignore = {"of", "default", "optional"} - - doc = SphinxDocString( - xref_doc_txt, - config=dict( - xref_param_type=True, - xref_aliases=xref_aliases_complete, - xref_ignore=xref_ignore, - ), - ) - - line_by_line_compare(str(doc), xref_doc_txt_expected) - - def test__error_location_no_name_attr(): """ Ensure that NumpyDocString._error_location doesn't fail when self._obj @@ -1675,9 +1077,9 @@ def test_namedtuple_no_duplicate_attributes(): foo = namedtuple("Foo", ("bar", "baz")) - # Create the SphinxClassDoc object via get_doc_object - sds = get_doc_object(foo) - assert sds["Attributes"] == [] + # Create the ClassDoc object via get_doc_object + nds = get_doc_object(foo) + assert nds["Attributes"] == [] def test_namedtuple_class_docstring(): @@ -1692,9 +1094,9 @@ def test_namedtuple_class_docstring(): class MyFoo(foo): """MyFoo's class docstring""" - # Create the SphinxClassDoc object via get_doc_object - sds = get_doc_object(MyFoo) - assert sds["Summary"] == ["MyFoo's class docstring"] + # Create the ClassDoc object via get_doc_object + nds = get_doc_object(MyFoo) + assert nds["Summary"] == ["MyFoo's class docstring"] # Example dataclass where constructor params are documented explicit. # Parameter names/descriptions should be included in the docstring, but @@ -1714,12 +1116,12 @@ class MyFooWithParams(foo): bar: str baz: str - sds = get_doc_object(MyFooWithParams) - assert "MyFoo's class docstring" in sds["Summary"] - assert len(sds["Attributes"]) == 0 - assert len(sds["Parameters"]) == 2 - assert sds["Parameters"][0].desc[0] == "The bar attribute" - assert sds["Parameters"][1].desc[0] == "The baz attribute" + nds = get_doc_object(MyFooWithParams) + assert "MyFoo's class docstring" in nds["Summary"] + assert len(nds["Attributes"]) == 0 + assert len(nds["Parameters"]) == 2 + assert nds["Parameters"][0].desc[0] == "The bar attribute" + assert nds["Parameters"][1].desc[0] == "The baz attribute" if __name__ == "__main__": diff --git a/numpydoc/tests/test_docscrape_sphinx.py b/numpydoc/tests/test_docscrape_sphinx.py index cf7c0de9..df4c5d5c 100644 --- a/numpydoc/tests/test_docscrape_sphinx.py +++ b/numpydoc/tests/test_docscrape_sphinx.py @@ -1,3 +1,6 @@ +import pytest +pytest.importorskip("sphinx") + import re import textwrap import warnings @@ -5,10 +8,8 @@ from copy import deepcopy import jinja2 -import pytest from pytest import warns as assert_warns -from numpydoc.docscrape import ClassDoc, FunctionDoc, NumpyDocString from numpydoc.docscrape_sphinx import ( SphinxClassDoc, SphinxDocString, @@ -133,7 +134,7 @@ @pytest.fixture(params=["", "\n "], ids=["flush", "newline_indented"]) def doc(request): - return NumpyDocString(request.param + doc_txt) + return SphinxDocString(request.param + doc_txt) doc_yields_txt = """ @@ -148,7 +149,7 @@ def doc(request): int The number of unknowns. """ -doc_yields = NumpyDocString(doc_yields_txt) +doc_yields = SphinxDocString(doc_yields_txt) doc_sent_txt = """ @@ -167,7 +168,7 @@ def doc(request): The number of oranges. """ -doc_sent = NumpyDocString(doc_sent_txt) +doc_sent = SphinxDocString(doc_sent_txt) def test_signature(doc): @@ -283,7 +284,7 @@ def test_returnyield(): The number of bananas. """ - doc = NumpyDocString(doc_text) + doc = SphinxDocString(doc_text) assert len(doc["Returns"]) == 1 assert len(doc["Yields"]) == 2 @@ -301,7 +302,7 @@ def test_section_twice(): That should break... """ with pytest.raises(ValueError, match="The section Notes appears twice"): - NumpyDocString(doc_text) + SphinxDocString(doc_text) # if we have a numpydoc object, we know where the error came from class Dummy: @@ -384,166 +385,9 @@ def line_by_line_compare(a, b, n_lines=None): assert aa == bb -def test_str(doc): - # doc_txt has the order of Notes and See Also sections flipped. - # This should be handled automatically, and so, one thing this test does - # is to make sure that See Also precedes Notes in the output. - line_by_line_compare( - str(doc), - """numpy.multivariate_normal(mean, cov, shape=None, spam=None) - -Draw values from a multivariate normal distribution with specified -mean and covariance. - -The multivariate normal or Gaussian distribution is a generalisation -of the one-dimensional normal distribution to higher dimensions. - -Parameters ----------- -mean : (N,) ndarray - Mean of the N-dimensional distribution. - - .. math:: - - (1+2+3)/3 -cov : (N, N) ndarray - Covariance matrix of the distribution. -shape : tuple of ints - Given a shape of, for example, (m,n,k), m*n*k samples are - generated, and packed in an m-by-n-by-k arrangement. Because - each sample is N-dimensional, the output shape is (m,n,k,N). -dtype : data type object, optional (default : float) - The type and size of the data to be returned. - -Returns -------- -out : ndarray - The drawn samples, arranged according to `shape`. If the - shape given is (m,n,...), then the shape of `out` is - (m,n,...,N). - - In other words, each entry ``out[i,j,...,:]`` is an N-dimensional - value drawn from the distribution. -list of str - This is not a real return value. It exists to test - anonymous return values. -no_description - -Other Parameters ----------------- -spam : parrot - A parrot off its mortal coil. - -Raises ------- -RuntimeError - Some error - -Warns ------ -RuntimeWarning - Some warning - -Warnings --------- -Certain warnings apply. - -See Also --------- - -`some`_, `other`_, `funcs`_ - .. -`otherfunc`_ - relationship -:py:meth:`spyder.widgets.mixins.GetHelpMixin.show_object_info` - .. - -Notes ------ -Instead of specifying the full covariance matrix, popular -approximations include: - - - Spherical covariance (`cov` is a multiple of the identity matrix) - - Diagonal covariance (`cov` has non-negative elements only on the diagonal) - -This geometrical property can be seen in two dimensions by plotting -generated data-points: - ->>> mean = [0,0] ->>> cov = [[1,0],[0,100]] # diagonal covariance, points lie on x or y-axis - ->>> x,y = multivariate_normal(mean,cov,5000).T ->>> plt.plot(x,y,'x'); plt.axis('equal'); plt.show() - -Note that the covariance matrix must be symmetric and non-negative -definite. - -References ----------- -.. [1] A. Papoulis, "Probability, Random Variables, and Stochastic - Processes," 3rd ed., McGraw-Hill Companies, 1991 -.. [2] R.O. Duda, P.E. Hart, and D.G. Stork, "Pattern Classification," - 2nd ed., Wiley, 2001. - -Examples --------- ->>> mean = (1,2) ->>> cov = [[1,0],[1,0]] ->>> x = multivariate_normal(mean,cov,(3,3)) ->>> print(x.shape) -(3, 3, 2) - -The following is probably true, given that 0.6 is roughly twice the -standard deviation: - ->>> print(list((x[0, 0, :] - mean) < 0.6)) -[True, True] - -.. index:: random - :refguide: random;distributions, random;gauss""", - ) - - -def test_yield_str(): - line_by_line_compare( - str(doc_yields), - """Test generator - -Yields ------- -a : int - The number of apples. -b : int - The number of bananas. -int - The number of unknowns. -""", - ) - - -def test_receives_str(): - line_by_line_compare( - str(doc_sent), - """Test generator - -Yields ------- -a : int - The number of apples. - -Receives --------- -b : int - The number of bananas. -c : int - The number of oranges. -""", - ) - - def test_no_index_in_str(): assert "index" not in str( - NumpyDocString( + SphinxDocString( """Test idx """ @@ -551,7 +395,7 @@ def test_no_index_in_str(): ) assert "index" in str( - NumpyDocString( + SphinxDocString( """Test idx .. index :: random @@ -560,7 +404,7 @@ def test_no_index_in_str(): ) assert "index" in str( - NumpyDocString( + SphinxDocString( """Test idx .. index :: @@ -717,7 +561,7 @@ def test_sphinx_yields_str(): ) -doc2 = NumpyDocString( +doc2 = SphinxDocString( """ Returns array of indices of the maximum values of along the given axis. @@ -735,27 +579,7 @@ def test_parameters_without_extended_description(): assert len(doc2["Parameters"]) == 2 -doc3 = NumpyDocString( - """ - my_signature(*params, **kwds) - - Return this and that. - """ -) - - -def test_escape_stars(): - signature = str(doc3).split("\n")[0] - assert signature == r"my_signature(\*params, \*\*kwds)" - - def my_func(a, b, **kwargs): - pass - - fdoc = FunctionDoc(func=my_func) - assert fdoc["Signature"] == "" - - -doc4 = NumpyDocString( +doc4 = SphinxDocString( """a.conj() Return an array with all complex-valued elements conjugated.""" @@ -766,7 +590,7 @@ def test_empty_extended_summary(): assert doc4["Extended Summary"] == [] -doc5 = NumpyDocString( +doc5 = SphinxDocString( """ a.something() @@ -807,7 +631,7 @@ def test_warns(): # foo @pytest.mark.parametrize("prefix", ["", "\n "]) def test_see_also(prefix): - doc6 = NumpyDocString( + doc6 = SphinxDocString( prefix + """z(x,theta) @@ -885,24 +709,7 @@ def test_see_also_parse_error(): :func:`~foo` """ with pytest.raises(ValueError, match="See Also entry ':func:`~foo`'"): - NumpyDocString(text) - - -def test_see_also_print(): - class Dummy: - """ - See Also - -------- - func_a, func_b - func_c : some relationship - goes here - func_d - """ - - s = str(FunctionDoc(Dummy, role="func")) - assert ":func:`func_a`, :func:`func_b`" in s - assert " some relationship" in s - assert ":func:`func_d`" in s + SphinxDocString(text) def test_see_also_trailing_comma_warning(): @@ -911,7 +718,7 @@ def test_see_also_trailing_comma_warning(): Warning, match="Unexpected comma or period after function list at index 43 of line .*", ): - NumpyDocString( + SphinxDocString( """ z(x,theta) @@ -941,10 +748,6 @@ class BadSection: This class has a nope section. """ - with pytest.warns(UserWarning, match="Unknown section Mope") as record: - NumpyDocString(doc_text) - assert len(record) == 1 - # SphinxClassDoc has _obj.__name__ == "BadSection". Test that this is # included in the message msg_match = "Unknown section Nope in the docstring of BadSection" @@ -953,7 +756,7 @@ class BadSection: assert len(record) == 1 -doc7 = NumpyDocString( +doc7 = SphinxDocString( """ Doc starts on second line. @@ -966,7 +769,7 @@ def test_empty_first_line(): assert doc7["Summary"][0].startswith("Doc starts") -doc8 = NumpyDocString( +doc8 = SphinxDocString( """ Parameters with colon and no types: @@ -983,7 +786,7 @@ def test_empty_first_line(): def test_returns_with_roles_no_names(): """Make sure colons that are part of sphinx roles are not misinterpreted as type separator in returns section. See gh-428.""" - docstring = NumpyDocString( + docstring = SphinxDocString( """ Returns ------- @@ -999,16 +802,6 @@ def test_trailing_colon(): assert doc8["Parameters"][0].name == "data" -def test_no_summary(): - str( - SphinxDocString( - """ - Parameters - ----------""" - ) - ) - - def test_unicode(): doc = SphinxDocString( """ @@ -1095,24 +888,20 @@ def spammity(self): class Ignorable: """local class, to be ignored""" - for cls in (ClassDoc, SphinxClassDoc): - doc = cls(Dummy, config=dict(show_class_members=False)) - assert "Methods" not in str(doc), (cls, str(doc)) - assert "spam" not in str(doc), (cls, str(doc)) - assert "ham" not in str(doc), (cls, str(doc)) - assert "spammity" not in str(doc), (cls, str(doc)) - assert "Spammity index" not in str(doc), (cls, str(doc)) - - doc = cls(Dummy, config=dict(show_class_members=True)) - assert "Methods" in str(doc), (cls, str(doc)) - assert "spam" in str(doc), (cls, str(doc)) - assert "ham" in str(doc), (cls, str(doc)) - assert "spammity" in str(doc), (cls, str(doc)) - - if cls is SphinxClassDoc: - assert ".. autosummary::" in str(doc), str(doc) - else: - assert "Spammity index" in str(doc), str(doc) + doc = SphinxClassDoc(Dummy, config=dict(show_class_members=False)) + assert "Methods" not in str(doc), (SphinxClassDoc, str(doc)) + assert "spam" not in str(doc), (SphinxClassDoc, str(doc)) + assert "ham" not in str(doc), (SphinxClassDoc, str(doc)) + assert "spammity" not in str(doc), (SphinxClassDoc, str(doc)) + assert "Spammity index" not in str(doc), (SphinxClassDoc, str(doc)) + + doc = SphinxClassDoc(Dummy, config=dict(show_class_members=True)) + assert "Methods" in str(doc), (SphinxClassDoc, str(doc)) + assert "spam" in str(doc), (SphinxClassDoc, str(doc)) + assert "ham" in str(doc), (SphinxClassDoc, str(doc)) + assert "spammity" in str(doc), (SphinxClassDoc, str(doc)) + + assert ".. autosummary::" in str(doc), str(doc) class SubDummy(Dummy): """ @@ -1126,36 +915,29 @@ def ham(self, c, d): def bar(self, a, b): """Bar\n\nNo bar""" - for cls in (ClassDoc, SphinxClassDoc): - doc = cls( - SubDummy, - config=dict(show_class_members=True, show_inherited_class_members=False), - ) - assert "Methods" in str(doc), (cls, str(doc)) - assert "spam" not in str(doc), (cls, str(doc)) - assert "ham" in str(doc), (cls, str(doc)) - assert "bar" in str(doc), (cls, str(doc)) - assert "spammity" not in str(doc), (cls, str(doc)) - - if cls is SphinxClassDoc: - assert ".. autosummary::" in str(doc), str(doc) - else: - assert "Spammity index" not in str(doc), str(doc) - - doc = cls( - SubDummy, - config=dict(show_class_members=True, show_inherited_class_members=True), - ) - assert "Methods" in str(doc), (cls, str(doc)) - assert "spam" in str(doc), (cls, str(doc)) - assert "ham" in str(doc), (cls, str(doc)) - assert "bar" in str(doc), (cls, str(doc)) - assert "spammity" in str(doc), (cls, str(doc)) + doc = SphinxClassDoc( + SubDummy, + config=dict(show_class_members=True, show_inherited_class_members=False), + ) + assert "Methods" in str(doc), (SphinxClassDoc, str(doc)) + assert "spam" not in str(doc), (SphinxClassDoc, str(doc)) + assert "ham" in str(doc), (SphinxClassDoc, str(doc)) + assert "bar" in str(doc), (SphinxClassDoc, str(doc)) + assert "spammity" not in str(doc), (SphinxClassDoc, str(doc)) + + assert ".. autosummary::" in str(doc), str(doc) + + doc = SphinxClassDoc( + SubDummy, + config=dict(show_class_members=True, show_inherited_class_members=True), + ) + assert "Methods" in str(doc), (SphinxClassDoc, str(doc)) + assert "spam" in str(doc), (SphinxClassDoc, str(doc)) + assert "ham" in str(doc), (SphinxClassDoc, str(doc)) + assert "bar" in str(doc), (SphinxClassDoc, str(doc)) + assert "spammity" in str(doc), (SphinxClassDoc, str(doc)) - if cls is SphinxClassDoc: - assert ".. autosummary::" in str(doc), str(doc) - else: - assert "Spammity index" in str(doc), str(doc) + assert ".. autosummary::" in str(doc), str(doc) def test_duplicate_signature(): @@ -1163,7 +945,7 @@ def test_duplicate_signature(): # automatic mechanism adds one, and a more detailed comes from the # docstring itself. - doc = NumpyDocString( + doc = SphinxDocString( """ z(x1, x2) @@ -1226,61 +1008,6 @@ def test_duplicate_signature(): """ -def test_class_members_doc(): - doc = ClassDoc(None, class_doc_txt) - line_by_line_compare( - str(doc), - """ - Foo - - Parameters - ---------- - f : callable ``f(t, y, *f_args)`` - Aaa. - jac : callable ``jac(t, y, *jac_args)`` - Bbb. - - Attributes - ---------- - t : float - Current time. - y : ndarray - Current variable values. - - * hello - * world - an_attribute : float - The docstring is printed instead - no_docstring : str - But a description - no_docstring2 : str - multiline_sentence - midword_period - no_period - - Methods - ------- - a - b - c - - Other Parameters - ---------------- - another parameter : str - This parameter is less important. - - Notes - ----- - Some notes about the class. - - Examples - -------- - For usage examples, see `ode`. - - """, - ) - - def test_class_members_doc_sphinx(): class Foo: @property @@ -1625,7 +1352,7 @@ def __init__(self, a, b): def test__error_location_no_name_attr(): """ - Ensure that NumpyDocString._error_location doesn't fail when self._obj + Ensure that SphinxDocString._error_location doesn't fail when self._obj does not have a __name__ attr. See gh-362 @@ -1640,12 +1367,12 @@ def __call__(self): foo = Foo() # foo is a Callable, but no a function instance assert isinstance(foo, Callable) - # Create an NumpyDocString instance to call the _error_location method - nds = get_doc_object(foo) + # Create an SphinxDocString instance to call the _error_location method + sds = get_doc_object(foo) msg = "Potentially wrong underline length.*Foo.*" with pytest.raises(ValueError, match=msg): - nds._error_location(msg=msg) + sds._error_location(msg=msg) def test_class_docstring_cached_property(): From 4e38009f2765b2dfaf301d263c2e38f87dd3133e Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 12:10:40 -0700 Subject: [PATCH 07/12] TST: Reuse test inputs in test_docscrape_sphinx.py. --- numpydoc/tests/test_docscrape_sphinx.py | 195 +----------------------- 1 file changed, 1 insertion(+), 194 deletions(-) diff --git a/numpydoc/tests/test_docscrape_sphinx.py b/numpydoc/tests/test_docscrape_sphinx.py index df4c5d5c..77e056a0 100644 --- a/numpydoc/tests/test_docscrape_sphinx.py +++ b/numpydoc/tests/test_docscrape_sphinx.py @@ -19,155 +19,14 @@ from numpydoc.numpydoc import update_config from numpydoc.xref import DEFAULT_LINKS -doc_txt = """\ - numpy.multivariate_normal(mean, cov, shape=None, spam=None) - - Draw values from a multivariate normal distribution with specified - mean and covariance. - - The multivariate normal or Gaussian distribution is a generalisation - of the one-dimensional normal distribution to higher dimensions. - - Parameters - ---------- - mean : (N,) ndarray - Mean of the N-dimensional distribution. - - .. math:: - - (1+2+3)/3 - - cov : (N, N) ndarray - Covariance matrix of the distribution. - shape : tuple of ints - Given a shape of, for example, (m,n,k), m*n*k samples are - generated, and packed in an m-by-n-by-k arrangement. Because - each sample is N-dimensional, the output shape is (m,n,k,N). - dtype : data type object, optional (default : float) - The type and size of the data to be returned. - - Returns - ------- - out : ndarray - The drawn samples, arranged according to `shape`. If the - shape given is (m,n,...), then the shape of `out` is - (m,n,...,N). - - In other words, each entry ``out[i,j,...,:]`` is an N-dimensional - value drawn from the distribution. - list of str - This is not a real return value. It exists to test - anonymous return values. - no_description - - Other Parameters - ---------------- - spam : parrot - A parrot off its mortal coil. - - Raises - ------ - RuntimeError - Some error - - Warns - ----- - RuntimeWarning - Some warning - - Warnings - -------- - Certain warnings apply. - - Notes - ----- - Instead of specifying the full covariance matrix, popular - approximations include: - - - Spherical covariance (`cov` is a multiple of the identity matrix) - - Diagonal covariance (`cov` has non-negative elements only on the diagonal) - - This geometrical property can be seen in two dimensions by plotting - generated data-points: - - >>> mean = [0,0] - >>> cov = [[1,0],[0,100]] # diagonal covariance, points lie on x or y-axis - - >>> x,y = multivariate_normal(mean,cov,5000).T - >>> plt.plot(x,y,'x'); plt.axis('equal'); plt.show() - - Note that the covariance matrix must be symmetric and non-negative - definite. - - References - ---------- - .. [1] A. Papoulis, "Probability, Random Variables, and Stochastic - Processes," 3rd ed., McGraw-Hill Companies, 1991 - .. [2] R.O. Duda, P.E. Hart, and D.G. Stork, "Pattern Classification," - 2nd ed., Wiley, 2001. - - See Also - -------- - some, other, funcs - otherfunc : relationship - :py:meth:`spyder.widgets.mixins.GetHelpMixin.show_object_info` - - Examples - -------- - >>> mean = (1,2) - >>> cov = [[1,0],[1,0]] - >>> x = multivariate_normal(mean,cov,(3,3)) - >>> print(x.shape) - (3, 3, 2) - - The following is probably true, given that 0.6 is roughly twice the - standard deviation: - - >>> print(list((x[0, 0, :] - mean) < 0.6)) - [True, True] - - .. index:: random - :refguide: random;distributions, random;gauss - - """ - +from test_docscrape import doc_txt, doc_yields_txt, doc_sent_txt, class_doc_txt @pytest.fixture(params=["", "\n "], ids=["flush", "newline_indented"]) def doc(request): return SphinxDocString(request.param + doc_txt) -doc_yields_txt = """ -Test generator - -Yields ------- -a : int - The number of apples. -b : int - The number of bananas. -int - The number of unknowns. -""" doc_yields = SphinxDocString(doc_yields_txt) - - -doc_sent_txt = """ -Test generator - -Yields ------- -a : int - The number of apples. - -Receives --------- -b : int - The number of bananas. -c : int - The number of oranges. - -""" doc_sent = SphinxDocString(doc_sent_txt) @@ -956,58 +815,6 @@ def test_duplicate_signature(): assert doc["Signature"].strip() == "z(a, theta)" -class_doc_txt = """ - Foo - - Parameters - ---------- - f : callable ``f(t, y, *f_args)`` - Aaa. - jac : callable ``jac(t, y, *jac_args)`` - - Bbb. - - Attributes - ---------- - t : float - Current time. - y : ndarray - Current variable values. - - * hello - * world - an_attribute : float - The docstring is printed instead - no_docstring : str - But a description - no_docstring2 : str - multiline_sentence - midword_period - no_period - - Methods - ------- - a - b - c - - Other Parameters - ---------------- - - another parameter : str - This parameter is less important. - - Notes - ----- - - Some notes about the class. - - Examples - -------- - For usage examples, see `ode`. -""" - - def test_class_members_doc_sphinx(): class Foo: @property From cef7fd9a46f689f7739383c2b76daffbd97664a2 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 12:15:51 -0700 Subject: [PATCH 08/12] TST: Skip test_full if no sphinx installed. --- numpydoc/tests/test_full.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/numpydoc/tests/test_full.py b/numpydoc/tests/test_full.py index 9ab241fa..6963bddf 100644 --- a/numpydoc/tests/test_full.py +++ b/numpydoc/tests/test_full.py @@ -1,8 +1,10 @@ +import pytest +pytest.importorskip("sphinx") + import os.path as op import re import shutil -import pytest from docutils import __version__ as docutils_version from packaging import version from sphinx.application import Sphinx From f8f23102364f3a7c8bf52ce43424d59c5dfb6e6d Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 14:06:43 -0700 Subject: [PATCH 09/12] MAINT: split _clean_text_signature out to _utils. All other functions defined in numpydoc.py module depend on sphinx and therefore can be skipped for testing purposes when sphinx is not installed. importorskips the numpydoc tests but maintains the testing of _clean_text_signature in new test_utils.py module. --- numpydoc/_utils.py | 13 +++++++++++ numpydoc/numpydoc.py | 13 +---------- numpydoc/tests/test_numpydoc.py | 40 ++++----------------------------- numpydoc/tests/test_utils.py | 35 +++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 48 deletions(-) create mode 100644 numpydoc/_utils.py create mode 100644 numpydoc/tests/test_utils.py diff --git a/numpydoc/_utils.py b/numpydoc/_utils.py new file mode 100644 index 00000000..cb3d9f95 --- /dev/null +++ b/numpydoc/_utils.py @@ -0,0 +1,13 @@ +import re + + +def _clean_text_signature(sig): + if sig is None: + return None + start_pattern = re.compile(r"^[^(]*\(") + start, end = start_pattern.search(sig).span() + start_sig = sig[start:end] + sig = sig[end:-1] + sig = re.sub(r"^\$(self|module|type)(,\s|$)", "", sig, count=1) + sig = re.sub(r"(^|(?<=,\s))/,\s\*", "*", sig, count=1) + return start_sig + sig + ")" diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py index 2b9757d7..37a716da 100644 --- a/numpydoc/numpydoc.py +++ b/numpydoc/numpydoc.py @@ -37,6 +37,7 @@ from .docscrape_sphinx import get_doc_object from .validate import get_validation_checks, validate from .xref import DEFAULT_LINKS +from ._utils import _clean_text_signature logger = logging.getLogger(__name__) @@ -301,18 +302,6 @@ def mangle_signature(app: SphinxApp, what, name, obj, options, sig, retann): return sig, "" -def _clean_text_signature(sig): - if sig is None: - return None - start_pattern = re.compile(r"^[^(]*\(") - start, end = start_pattern.search(sig).span() - start_sig = sig[start:end] - sig = sig[end:-1] - sig = re.sub(r"^\$(self|module|type)(,\s|$)", "", sig, count=1) - sig = re.sub(r"(^|(?<=,\s))/,\s\*", "*", sig, count=1) - return start_sig + sig + ")" - - def setup(app: SphinxApp, get_doc_object_=get_doc_object): if not hasattr(app, "add_config_value"): return None # probably called by nose, better bail out diff --git a/numpydoc/tests/test_numpydoc.py b/numpydoc/tests/test_numpydoc.py index f879d7fb..2149e2ba 100644 --- a/numpydoc/tests/test_numpydoc.py +++ b/numpydoc/tests/test_numpydoc.py @@ -1,15 +1,17 @@ +import pytest +pytest.importorskip("sphinx") +pytest.importorskip("docutils") + from collections import defaultdict from copy import deepcopy from io import StringIO from pathlib import PosixPath -import pytest from docutils import nodes from sphinx.ext.autodoc import ALL from sphinx.util import logging from numpydoc.numpydoc import ( - _clean_text_signature, clean_backrefs, mangle_docstrings, update_config, @@ -111,40 +113,6 @@ def test_mangle_docstrings_inherited_class_members(): assert "samefile" not in lines -def test_clean_text_signature(): - assert _clean_text_signature(None) is None - assert _clean_text_signature("func($self)") == "func()" - assert ( - _clean_text_signature("func($self, *args, **kwargs)") == "func(*args, **kwargs)" - ) - assert _clean_text_signature("($self)") == "()" - assert _clean_text_signature("()") == "()" - assert _clean_text_signature("func()") == "func()" - assert ( - _clean_text_signature("func($self, /, *args, **kwargs)") - == "func(*args, **kwargs)" - ) - assert ( - _clean_text_signature("func($self, other, /, *args, **kwargs)") - == "func(other, *args, **kwargs)" - ) - assert _clean_text_signature("($module)") == "()" - assert _clean_text_signature("func($type)") == "func()" - assert ( - _clean_text_signature('func($self, foo="hello world")') - == 'func(foo="hello world")' - ) - assert ( - _clean_text_signature("func($self, foo='hello world')") - == "func(foo='hello world')" - ) - assert _clean_text_signature('func(foo="hello world")') == 'func(foo="hello world")' - assert _clean_text_signature('func(foo="$self")') == 'func(foo="$self")' - assert _clean_text_signature('func($self, foo="$self")') == 'func(foo="$self")' - assert _clean_text_signature("func(self, other)") == "func(self, other)" - assert _clean_text_signature("func($self, *args)") == "func(*args)" - - @pytest.fixture def f(): def _function_without_seealso_and_examples(): diff --git a/numpydoc/tests/test_utils.py b/numpydoc/tests/test_utils.py new file mode 100644 index 00000000..80cfe897 --- /dev/null +++ b/numpydoc/tests/test_utils.py @@ -0,0 +1,35 @@ +from numpydoc._utils import _clean_text_signature + + +def test_clean_text_signature(): + assert _clean_text_signature(None) is None + assert _clean_text_signature("func($self)") == "func()" + assert ( + _clean_text_signature("func($self, *args, **kwargs)") == "func(*args, **kwargs)" + ) + assert _clean_text_signature("($self)") == "()" + assert _clean_text_signature("()") == "()" + assert _clean_text_signature("func()") == "func()" + assert ( + _clean_text_signature("func($self, /, *args, **kwargs)") + == "func(*args, **kwargs)" + ) + assert ( + _clean_text_signature("func($self, other, /, *args, **kwargs)") + == "func(other, *args, **kwargs)" + ) + assert _clean_text_signature("($module)") == "()" + assert _clean_text_signature("func($type)") == "func()" + assert ( + _clean_text_signature('func($self, foo="hello world")') + == 'func(foo="hello world")' + ) + assert ( + _clean_text_signature("func($self, foo='hello world')") + == "func(foo='hello world')" + ) + assert _clean_text_signature('func(foo="hello world")') == 'func(foo="hello world")' + assert _clean_text_signature('func(foo="$self")') == 'func(foo="$self")' + assert _clean_text_signature('func($self, foo="$self")') == 'func(foo="$self")' + assert _clean_text_signature("func(self, other)") == "func(self, other)" + assert _clean_text_signature("func($self, *args)") == "func(*args)" From c3f1676b0a53b553b5a8050fd79c87f32a0d5552 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 14:58:02 -0700 Subject: [PATCH 10/12] TST: Configure test collection per sphinx availability When sphinx is not installed, ignore the numpydoc.py and docscrape_sphinx.py modules during test collection as they depend on sphinx. --- conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..4b322d1c --- /dev/null +++ b/conftest.py @@ -0,0 +1,15 @@ +# Configure test collection to skip doctests for modules that depend on sphinx +# when sphinx is not available in the environment. + +try: + import sphinx + + has_sphinx = True +except ImportError: + has_sphinx = False + + +collect_ignore = [] + +if not has_sphinx: + collect_ignore += ["numpydoc/numpydoc.py", "numpydoc/docscrape_sphinx.py"] From da65208147d2340287ca1f102b3ddaf66a90ac32 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 10:48:48 -0700 Subject: [PATCH 11/12] DEP: Break numpydoc.cli dependence on sphinx. The only thing here that depends on sphinx is the fn which uses docscrape_sphinx's version of get_doc_object. If we switch to docscrape's version of get_doc_object, then the resulting rendering is derived from NumpyDocString instead of SphinxDocString. Alternatively - this could be left as-is and the test_render tests in could be importorskipped. --- numpydoc/cli.py | 2 +- numpydoc/docscrape.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/numpydoc/cli.py b/numpydoc/cli.py index 30daa3f5..0cbf7eff 100644 --- a/numpydoc/cli.py +++ b/numpydoc/cli.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import List -from .docscrape_sphinx import get_doc_object +from numpydoc.docscrape import get_doc_object from .hooks import utils, validate_docstrings from .validate import ERROR_MSGS, Validator, validate diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index 26a08259..f468b27a 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -574,7 +574,7 @@ def dedent_lines(lines): class FunctionDoc(NumpyDocString): - def __init__(self, func, role="func", doc=None, config=None): + def __init__(self, func, role=None, doc=None, config=None): self._f = func self._role = role # e.g. "func" or "meth" From 2d14fa0f1dccca3d33edb290098573c1ca10ff57 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Thu, 13 Nov 2025 16:00:17 -0800 Subject: [PATCH 12/12] Make linter happy. --- numpydoc/__init__.py | 2 -- numpydoc/cli.py | 1 + numpydoc/numpydoc.py | 2 +- numpydoc/tests/test_docscrape_sphinx.py | 3 ++- numpydoc/tests/test_full.py | 1 + numpydoc/tests/test_numpydoc.py | 1 + 6 files changed, 6 insertions(+), 4 deletions(-) diff --git a/numpydoc/__init__.py b/numpydoc/__init__.py index ce164700..632b7063 100644 --- a/numpydoc/__init__.py +++ b/numpydoc/__init__.py @@ -5,14 +5,12 @@ from ._version import __version__ - # NOTE: Determine whether sphinx is installed with an explicit import. # If so, define the setup function for registering the numpydoc extension; # otherwise skip this step. try: import sphinx - def setup(app, *args, **kwargs): from .numpydoc import setup diff --git a/numpydoc/cli.py b/numpydoc/cli.py index 0cbf7eff..d5ce2721 100644 --- a/numpydoc/cli.py +++ b/numpydoc/cli.py @@ -7,6 +7,7 @@ from typing import List from numpydoc.docscrape import get_doc_object + from .hooks import utils, validate_docstrings from .validate import ERROR_MSGS, Validator, validate diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py index 37a716da..70a5ed77 100644 --- a/numpydoc/numpydoc.py +++ b/numpydoc/numpydoc.py @@ -34,10 +34,10 @@ from sphinx.util import logging from . import __version__ +from ._utils import _clean_text_signature from .docscrape_sphinx import get_doc_object from .validate import get_validation_checks, validate from .xref import DEFAULT_LINKS -from ._utils import _clean_text_signature logger = logging.getLogger(__name__) diff --git a/numpydoc/tests/test_docscrape_sphinx.py b/numpydoc/tests/test_docscrape_sphinx.py index 77e056a0..357e325e 100644 --- a/numpydoc/tests/test_docscrape_sphinx.py +++ b/numpydoc/tests/test_docscrape_sphinx.py @@ -1,4 +1,5 @@ import pytest + pytest.importorskip("sphinx") import re @@ -9,6 +10,7 @@ import jinja2 from pytest import warns as assert_warns +from test_docscrape import class_doc_txt, doc_sent_txt, doc_txt, doc_yields_txt from numpydoc.docscrape_sphinx import ( SphinxClassDoc, @@ -19,7 +21,6 @@ from numpydoc.numpydoc import update_config from numpydoc.xref import DEFAULT_LINKS -from test_docscrape import doc_txt, doc_yields_txt, doc_sent_txt, class_doc_txt @pytest.fixture(params=["", "\n "], ids=["flush", "newline_indented"]) def doc(request): diff --git a/numpydoc/tests/test_full.py b/numpydoc/tests/test_full.py index 6963bddf..90c431a2 100644 --- a/numpydoc/tests/test_full.py +++ b/numpydoc/tests/test_full.py @@ -1,4 +1,5 @@ import pytest + pytest.importorskip("sphinx") import os.path as op diff --git a/numpydoc/tests/test_numpydoc.py b/numpydoc/tests/test_numpydoc.py index 2149e2ba..5d7179a4 100644 --- a/numpydoc/tests/test_numpydoc.py +++ b/numpydoc/tests/test_numpydoc.py @@ -1,4 +1,5 @@ import pytest + pytest.importorskip("sphinx") pytest.importorskip("docutils")