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 effcf01e9d9e0f3c24949dffea6c90f652fd925e Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 12:04:26 -0700 Subject: [PATCH 03/12] TST: Split docscrape tests based on sphinx dep. --- numpydoc/tests/test_docscrape.py | 702 +---------- numpydoc/tests/test_docscrape_sphinx.py | 1455 +++++++++++++++++++++++ 2 files changed, 1507 insertions(+), 650 deletions(-) create mode 100644 numpydoc/tests/test_docscrape_sphinx.py 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 new file mode 100644 index 00000000..df4c5d5c --- /dev/null +++ b/numpydoc/tests/test_docscrape_sphinx.py @@ -0,0 +1,1455 @@ +import pytest +pytest.importorskip("sphinx") + +import re +import textwrap +import warnings +from collections import namedtuple +from copy import deepcopy + +import jinja2 +from pytest import warns as assert_warns + +from numpydoc.docscrape_sphinx import ( + SphinxClassDoc, + SphinxDocString, + SphinxFunctionDoc, + get_doc_object, +) +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 + + """ + + +@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) + + +def test_signature(doc): + assert doc["Signature"].startswith("numpy.multivariate_normal(") + assert doc["Signature"].endswith("spam=None)") + + +def test_summary(doc): + assert doc["Summary"][0].startswith("Draw values") + assert doc["Summary"][-1].endswith("covariance.") + + +def test_extended_summary(doc): + assert doc["Extended Summary"][0].startswith("The multivariate normal") + + +def test_parameters(doc): + assert len(doc["Parameters"]) == 4 + names = [n for n, _, _ in doc["Parameters"]] + assert all( + a == b for a, b in zip(names, ["mean", "cov", "shape", "dtype"], strict=True) + ) + + arg, arg_type, desc = doc["Parameters"][1] + assert arg_type == "(N, N) ndarray" + assert desc[0].startswith("Covariance matrix") + assert doc["Parameters"][0][-1][-1] == " (1+2+3)/3" + + arg, arg_type, desc = doc["Parameters"][2] + assert arg == "shape" + assert arg_type == "tuple of ints" + assert desc[0].startswith("Given") + assert doc["Parameters"][0][-1][-1] == " (1+2+3)/3" + + arg, arg_type, desc = doc["Parameters"][3] + assert arg == "dtype" + assert arg_type == "data type object, optional (default : float)" + assert desc[0].startswith("The type and size") + + +def test_other_parameters(doc): + assert len(doc["Other Parameters"]) == 1 + assert [n for n, _, _ in doc["Other Parameters"]] == ["spam"] + _arg, arg_type, desc = doc["Other Parameters"][0] + assert arg_type == "parrot" + assert desc[0].startswith("A parrot off its mortal coil") + + +def test_returns(doc): + assert len(doc["Returns"]) == 3 + arg, arg_type, desc = doc["Returns"][0] + assert arg == "out" + assert arg_type == "ndarray" + assert desc[0].startswith("The drawn samples") + assert desc[-1].endswith("distribution.") + + arg, arg_type, desc = doc["Returns"][1] + assert arg == "" + assert arg_type == "list of str" + assert desc[0].startswith("This is not a real") + assert desc[-1].endswith("anonymous return values.") + + arg, arg_type, desc = doc["Returns"][2] + assert arg == "" + assert arg_type == "no_description" + assert not "".join(desc).strip() + + +def test_yields(): + section = doc_yields["Yields"] + assert len(section) == 3 + truth = [ + ("a", "int", "apples."), + ("b", "int", "bananas."), + ("", "int", "unknowns."), + ] + for (arg, arg_type, desc), (arg_, arg_type_, end) in zip( + section, truth, strict=True + ): + assert arg == arg_ + assert arg_type == arg_type_ + assert desc[0].startswith("The number of") + assert desc[0].endswith(end) + + +def test_sent(): + section = doc_sent["Receives"] + assert len(section) == 2 + truth = [("b", "int", "bananas."), ("c", "int", "oranges.")] + for (arg, arg_type, desc), (arg_, arg_type_, end) in zip( + section, truth, strict=True + ): + assert arg == arg_ + assert arg_type == arg_type_ + assert desc[0].startswith("The number of") + assert desc[0].endswith(end) + + +def test_returnyield(): + doc_text = """ +Test having returns and yields. + +Returns +------- +int + The number of apples. + +Yields +------ +a : int + The number of apples. +b : int + The number of bananas. + +""" + doc = SphinxDocString(doc_text) + assert len(doc["Returns"]) == 1 + assert len(doc["Yields"]) == 2 + + +def test_section_twice(): + doc_text = """ +Test having a section Notes twice + +Notes +----- +See the next note for more information + +Notes +----- +That should break... +""" + with pytest.raises(ValueError, match="The section Notes appears twice"): + SphinxDocString(doc_text) + + # if we have a numpydoc object, we know where the error came from + class Dummy: + """ + Dummy class. + + Notes + ----- + First note. + + Notes + ----- + Second note. + + """ + + def spam(self, a, b): + """Spam\n\nSpam spam.""" + + def ham(self, c, d): + """Cheese\n\nNo cheese.""" + + def dummy_func(arg): + """ + Dummy function. + + Notes + ----- + First note. + + Notes + ----- + Second note. + """ + + with pytest.raises(ValueError, match="Dummy class"): + SphinxClassDoc(Dummy) + + with pytest.raises(ValueError, match="dummy_func"): + SphinxFunctionDoc(dummy_func) + + +def test_notes(doc): + assert doc["Notes"][0].startswith("Instead") + assert doc["Notes"][-1].endswith("definite.") + assert len(doc["Notes"]) == 17 + + +def test_references(doc): + assert doc["References"][0].startswith("..") + assert doc["References"][-1].endswith("2001.") + + +def test_examples(doc): + assert doc["Examples"][0].startswith(">>>") + assert doc["Examples"][-1].endswith("True]") + + +def test_index(doc): + assert doc["index"]["default"] == "random" + assert len(doc["index"]) == 2 + assert len(doc["index"]["refguide"]) == 2 + + +def _strip_blank_lines(s): + "Remove leading, trailing and multiple blank lines" + s = re.sub(r"^\s*\n", "", s) + s = re.sub(r"\n\s*$", "", s) + s = re.sub(r"\n\s*\n", r"\n\n", s) + return s + + +def line_by_line_compare(a, b, n_lines=None): + a = textwrap.dedent(a) + b = textwrap.dedent(b) + a = [l.rstrip() for l in _strip_blank_lines(a).split("\n")][:n_lines] + b = [l.rstrip() for l in _strip_blank_lines(b).split("\n")][:n_lines] + assert len(a) == len(b) + for ii, (aa, bb) in enumerate(zip(a, b, strict=True)): + assert aa == bb + + +def test_no_index_in_str(): + assert "index" not in str( + SphinxDocString( + """Test idx + + """ + ) + ) + + assert "index" in str( + SphinxDocString( + """Test idx + + .. index :: random + """ + ) + ) + + assert "index" in str( + SphinxDocString( + """Test idx + + .. index :: + foo + """ + ) + ) + + +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 = SphinxDocString( + """ + Returns array of indices of the maximum values of along the given axis. + + Parameters + ---------- + a : {array_like} + Array to look in. + axis : {None, integer} + If None, the index is into the flattened array, otherwise along + the specified axis""" +) + + +def test_parameters_without_extended_description(): + assert len(doc2["Parameters"]) == 2 + + +doc4 = SphinxDocString( + """a.conj() + + Return an array with all complex-valued elements conjugated.""" +) + + +def test_empty_extended_summary(): + assert doc4["Extended Summary"] == [] + + +doc5 = SphinxDocString( + """ + a.something() + + Raises + ------ + LinAlgException + If array is singular. + + Warns + ----- + SomeWarning + If needed + """ +) + + +def test_raises(): + assert len(doc5["Raises"]) == 1 + param = doc5["Raises"][0] + assert param.name == "" + assert param.type == "LinAlgException" + assert param.desc == ["If array is singular."] + + +def test_warns(): + assert len(doc5["Warns"]) == 1 + param = doc5["Warns"][0] + assert param.name == "" + assert param.type == "SomeWarning" + assert param.desc == ["If needed"] + + +# see numpydoc/numpydoc #281 +# we want to correctly parse "See Also" both in docstrings both like +# """foo +# and +# """ +# foo +@pytest.mark.parametrize("prefix", ["", "\n "]) +def test_see_also(prefix): + doc6 = SphinxDocString( + prefix + + """z(x,theta) + + See Also + -------- + func_a, func_b, func_c + func_d : some equivalent func + foo.func_e : some other func over + multiple lines + func_f, func_g, :meth:`func_h`, func_j, + func_k + func_f1, func_g1, :meth:`func_h1`, func_j1 + func_f2, func_g2, :meth:`func_h2`, func_j2 : description of multiple + :obj:`baz.obj_q` + :obj:`~baz.obj_r` + :class:`class_j`: fubar + foobar + """ + ) + + assert len(doc6["See Also"]) == 10 + for funcs, desc in doc6["See Also"]: + for func, role in funcs: + if func in ( + "func_a", + "func_b", + "func_c", + "func_f", + "func_g", + "func_h", + "func_j", + "func_k", + "baz.obj_q", + "func_f1", + "func_g1", + "func_h1", + "func_j1", + "~baz.obj_r", + ): + assert not desc, str([func, desc]) + elif func in ("func_f2", "func_g2", "func_h2", "func_j2"): + assert desc, str([func, desc]) + else: + assert desc, str([func, desc]) + + if func == "func_h": + assert role == "meth" + elif func in ("baz.obj_q", "~baz.obj_r"): + assert role == "obj" + elif func == "class_j": + assert role == "class" + elif func in ("func_h1", "func_h2"): + assert role == "meth" + else: + assert role is None, str([func, role]) + + if func == "func_d": + assert desc == ["some equivalent func"] + elif func == "foo.func_e": + assert desc == ["some other func over", "multiple lines"] + elif func == "class_j": + assert desc == ["fubar", "foobar"] + elif func in ("func_f2", "func_g2", "func_h2", "func_j2"): + assert desc == ["description of multiple"], str( + [desc, ["description of multiple"]] + ) + + +def test_see_also_parse_error(): + text = """ + z(x,theta) + + See Also + -------- + :func:`~foo` + """ + with pytest.raises(ValueError, match="See Also entry ':func:`~foo`'"): + SphinxDocString(text) + + +def test_see_also_trailing_comma_warning(): + warnings.filterwarnings("error") + with assert_warns( + Warning, + match="Unexpected comma or period after function list at index 43 of line .*", + ): + SphinxDocString( + """ + z(x,theta) + + See Also + -------- + func_f2, func_g2, :meth:`func_h2`, func_j2, : description of multiple + :class:`class_j`: fubar + foobar + """ + ) + + +def test_unknown_section(): + doc_text = """ +Test having an unknown section + +Mope +---- +This should be ignored and warned about +""" + + class BadSection: + """Class with bad section. + + Nope + ---- + This class has a nope section. + """ + + # 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 = SphinxDocString( + """ + + Doc starts on second line. + + """ +) + + +def test_empty_first_line(): + assert doc7["Summary"][0].startswith("Doc starts") + + +doc8 = SphinxDocString( + """ + + Parameters with colon and no types: + + Parameters + ---------- + + data : + some stuff, technically invalid + """ +) + + +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 = SphinxDocString( + """ + Returns + ------- + str or :class:`NumpyDocString` + """ + ) + expected = "str or :class:`NumpyDocString`" # not "str or : class:... + assert docstring["Returns"][0].type == expected + assert expected in str(docstring) + + +def test_trailing_colon(): + assert doc8["Parameters"][0].name == "data" + + +def test_unicode(): + doc = SphinxDocString( + """ + öäöäöäöäöåååå + + öäöäöäööäååå + + Parameters + ---------- + ååå : äää + ööö + + Returns + ------- + ååå : ööö + äää + + """ + ) + assert isinstance(doc["Summary"][0], str) + 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: + """ + Dummy class. + + """ + + def spam(self, a, b): + """Spam\n\nSpam spam.""" + + def ham(self, c, d): + """Cheese\n\nNo cheese.""" + + @property + def spammity(self): + """Spammity index""" + return 0.95 + + class Ignorable: + """local class, to be ignored""" + + 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): + """ + Subclass of Dummy class. + + """ + + def ham(self, c, d): + """Cheese\n\nNo cheese.\nOverloaded Dummy.ham""" + + def bar(self, a, b): + """Bar\n\nNo bar""" + + 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)) + + assert ".. autosummary::" in str(doc), str(doc) + + +def test_duplicate_signature(): + # Duplicate function signatures occur e.g. in ufuncs, when the + # automatic mechanism adds one, and a more detailed comes from the + # docstring itself. + + doc = SphinxDocString( + """ + z(x1, x2) + + z(a, theta) + """ + ) + + 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 + 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) + + class SpecialProperty: + def __init__(self, axis=0, doc=""): + self.axis = axis + self.__doc__ = doc + + def __get__(self, obj, type): + if obj is None: + # Only instances have actual _data, not classes + return self + else: + return obj._data.axes[self.axis] + + def __set__(self, obj, value): + obj._set_axis(self.axis, value) + + class Dummy: + attr = SpecialProperty(doc="test attribute") + + doc = get_doc_object(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 SphinxDocString._error_location doesn't fail when self._obj + does not have a __name__ attr. + + See gh-362 + """ + from collections.abc import Callable + + # Create a Callable that doesn't have a __name__ attribute + class Foo: + def __call__(self): + pass + + foo = Foo() # foo is a Callable, but no a function instance + assert isinstance(foo, Callable) + + # 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): + sds._error_location(msg=msg) + + +def test_class_docstring_cached_property(): + """Ensure that properties marked with the `cached_property` decorator + are listed in the Methods section. See gh-432.""" + from functools import cached_property + + class Foo: + _x = [1, 2, 3] + + @cached_property + def val(self): + return self._x + + class_docstring = get_doc_object(Foo) + assert len(class_docstring["Attributes"]) == 1 + assert class_docstring["Attributes"][0].name == "val" + + +def test_namedtuple_no_duplicate_attributes(): + """ + Ensure that attributes of namedtuples are not duplicated in the docstring. + + See gh-257 + """ + from collections import namedtuple + + foo = namedtuple("Foo", ("bar", "baz")) + + # Create the SphinxClassDoc object via get_doc_object + sds = get_doc_object(foo) + assert sds["Attributes"] == [] + + +def test_namedtuple_class_docstring(): + """Ensure that class docstring is preserved when inheriting from namedtuple. + + See gh-257 + """ + from collections import namedtuple + + foo = namedtuple("Foo", ("bar", "baz")) + + 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"] + + # Example dataclass where constructor params are documented explicit. + # Parameter names/descriptions should be included in the docstring, but + # should not result in a duplicated `Attributes` section + class MyFooWithParams(foo): + """ + MyFoo's class docstring + + Parameters + ---------- + bar : str + The bar attribute + baz : str + The baz attribute + """ + + 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" + + +if __name__ == "__main__": + import pytest + + pytest.main() From a380d08de3770ac859e03394f33d5f8bd2053d3d Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 12:10:40 -0700 Subject: [PATCH 04/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 4d3b301ba0d83151df254a75a4d7f152b7660963 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 12:15:51 -0700 Subject: [PATCH 05/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 530d58090ba3e841cb08ed87bb43945f2b9a6c50 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 14:06:43 -0700 Subject: [PATCH 06/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 4bebfdd990389d15b8ef8eb594ec44fe9d3bdcac Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 14:58:02 -0700 Subject: [PATCH 07/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 8283bd117e6a59bcf01e2e3d3ee51c7e2f7ba081 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Tue, 14 Oct 2025 10:48:48 -0700 Subject: [PATCH 08/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 f27d2890ba4f1b910ca5b914387034c6e57a4940 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Fri, 17 Oct 2025 11:33:43 -0700 Subject: [PATCH 09/12] Update numpydoc/cli.py Co-authored-by: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> --- numpydoc/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/cli.py b/numpydoc/cli.py index 0cbf7eff..5fb217bd 100644 --- a/numpydoc/cli.py +++ b/numpydoc/cli.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import List -from numpydoc.docscrape import get_doc_object +from .docscrape import get_doc_object from .hooks import utils, validate_docstrings from .validate import ERROR_MSGS, Validator, validate From 7708ca05b353c93ebb7f8548825488019a03f863 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Fri, 17 Oct 2025 13:55:14 -0700 Subject: [PATCH 10/12] TST: Fix hook tests for windows. * replace path separators on windows * Add drive to path for windows --- numpydoc/tests/hooks/test_utils.py | 6 +++++- numpydoc/tests/hooks/test_validate_hook.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/numpydoc/tests/hooks/test_utils.py b/numpydoc/tests/hooks/test_utils.py index 5db63762..918b9045 100644 --- a/numpydoc/tests/hooks/test_utils.py +++ b/numpydoc/tests/hooks/test_utils.py @@ -23,7 +23,11 @@ def test_find_project_root(tmp_path, request, reason_file, files, expected_reaso (tmp_path / reason_file).touch() if files: - expected_dir = Path("/") if expected_reason == "file system root" else tmp_path + if expected_reason == "file system root": + expected_dir = Path(tmp_path.drive + tmp_path.root) + else: + expected_dir = tmp_path + for file in files: (tmp_path / file).touch() else: diff --git a/numpydoc/tests/hooks/test_validate_hook.py b/numpydoc/tests/hooks/test_validate_hook.py index b235313b..ffa0e473 100644 --- a/numpydoc/tests/hooks/test_validate_hook.py +++ b/numpydoc/tests/hooks/test_validate_hook.py @@ -2,6 +2,7 @@ import inspect from pathlib import Path +import sys import pytest @@ -63,6 +64,8 @@ def test_validate_hook(example_module, config, capsys): numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring """ ) + if sys.platform == "win32": + expected = expected.replace("/", "\\") return_code = run_hook([example_module], config=config) assert return_code == 1 @@ -90,6 +93,8 @@ def test_validate_hook_with_ignore(example_module, capsys): numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring """ ) + if sys.platform == "win32": + expected = expected.replace("/", "\\") return_code = run_hook([example_module], ignore=["ES01", "SA01", "EX01"]) @@ -133,6 +138,8 @@ def test_validate_hook_with_toml_config(example_module, tmp_path, capsys): numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring """ ) + if sys.platform == "win32": + expected = expected.replace("/", "\\") return_code = run_hook([example_module], config=tmp_path) assert return_code == 1 @@ -168,6 +175,8 @@ def test_validate_hook_with_setup_cfg(example_module, tmp_path, capsys): numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring """ ) + if sys.platform == "win32": + expected = expected.replace("/", "\\") return_code = run_hook([example_module], config=tmp_path) assert return_code == 1 @@ -209,6 +218,8 @@ def test_validate_hook_exclude_option_pyproject(example_module, tmp_path, capsys numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring """ ) + if sys.platform == "win32": + expected = expected.replace("/", "\\") return_code = run_hook([example_module], config=tmp_path) assert return_code == 1 @@ -242,6 +253,8 @@ def test_validate_hook_exclude_option_setup_cfg(example_module, tmp_path, capsys numpydoc/tests/hooks/example_module.py:17: PR07 Parameter "*args" has no description """ ) + if sys.platform == "win32": + expected = expected.replace("/", "\\") return_code = run_hook([example_module], config=tmp_path) assert return_code == 1 From b31376ad10490d2c62e3e8b65be4199cab457d58 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Fri, 17 Oct 2025 14:09:02 -0700 Subject: [PATCH 11/12] Lint. --- numpydoc/__init__.py | 2 -- numpydoc/numpydoc.py | 2 +- numpydoc/tests/hooks/test_validate_hook.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(+), 5 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/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/hooks/test_validate_hook.py b/numpydoc/tests/hooks/test_validate_hook.py index ffa0e473..c9acad3f 100644 --- a/numpydoc/tests/hooks/test_validate_hook.py +++ b/numpydoc/tests/hooks/test_validate_hook.py @@ -1,8 +1,8 @@ """Test the numpydoc validate pre-commit hook.""" import inspect -from pathlib import Path import sys +from pathlib import Path import pytest 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") From 09cfe96fc290d33a4f4a69e10fd97536aa388a81 Mon Sep 17 00:00:00 2001 From: Ross Barnowski Date: Sat, 18 Oct 2025 10:55:53 -0700 Subject: [PATCH 12/12] Update pyproject.toml Co-authored-by: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ad9ec8c1..33a15c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,10 +41,12 @@ file = 'LICENSE.txt' Homepage = 'https://numpydoc.readthedocs.io' Source = 'https://github.com/numpy/numpydoc/' -[dependency-groups] +[project.optional-dependencies] default = [ 'sphinx>=6', ] + +[dependency-groups] dev = [ 'pre-commit>=3.3', "tomli; python_version < '3.11'",