import pytest from collections import defaultdict from io import StringIO from pathlib import PosixPath from copy import deepcopy from numpydoc.numpydoc import mangle_docstrings, _clean_text_signature, update_config from numpydoc.xref import DEFAULT_LINKS from sphinx.ext.autodoc import ALL from sphinx.util import logging class MockConfig: numpydoc_use_plots = False numpydoc_use_blockquotes = True numpydoc_show_class_members = True numpydoc_show_inherited_class_members = True numpydoc_class_members_toctree = True numpydoc_xref_param_type = False numpydoc_xref_aliases = {} numpydoc_xref_aliases_complete = deepcopy(DEFAULT_LINKS) numpydoc_xref_ignore = set() templates_path = [] numpydoc_citation_re = "[a-z0-9_.-]+" numpydoc_attributes_as_param_list = True numpydoc_validation_checks = set() numpydoc_validation_exclude = set() class MockBuilder: config = MockConfig() class MockApp: config = MockConfig() builder = MockBuilder() translator = None def __init__(self): self.builder.app = self # Attrs required for logging self.verbosity = 2 self._warncount = 0 self.warningiserror = False def test_mangle_docstrings_basic(): s = """ A top section before .. autoclass:: str """ lines = s.split("\n") mangle_docstrings(MockApp(), "class", "str", str, {}, lines) assert "rpartition" in [x.strip() for x in lines] lines = s.split("\n") mangle_docstrings(MockApp(), "class", "str", str, {"members": ["upper"]}, lines) assert "rpartition" not in [x.strip() for x in lines] assert "upper" in [x.strip() for x in lines] lines = s.split("\n") mangle_docstrings(MockApp(), "class", "str", str, {"exclude-members": ALL}, lines) assert "rpartition" not in [x.strip() for x in lines] assert "upper" not in [x.strip() for x in lines] lines = s.split("\n") mangle_docstrings( MockApp(), "class", "str", str, {"exclude-members": ["upper"]}, lines ) assert "rpartition" in [x.strip() for x in lines] assert "upper" not in [x.strip() for x in lines] def test_mangle_docstrings_inherited_class_members(): # if subclass docs are rendered, this PosixPath should have Path.samefile p = """ A top section before .. autoclass:: pathlib.PosixPath """ lines = p.split("\n") app = MockApp() mangle_docstrings(app, "class", "pathlib.PosixPath", PosixPath, {}, lines) lines = [x.strip() for x in lines] assert "samefile" in lines app.config.numpydoc_show_inherited_class_members = False lines = p.split("\n") mangle_docstrings(app, "class", "pathlib.PosixPath", PosixPath, {}, lines) lines = [x.strip() for x in lines] assert "samefile" not in lines app.config.numpydoc_show_inherited_class_members = dict() lines = p.split("\n") mangle_docstrings(app, "class", "pathlib.PosixPath", PosixPath, {}, lines) lines = [x.strip() for x in lines] assert "samefile" in lines app.config.numpydoc_show_inherited_class_members = defaultdict(lambda: False) lines = p.split("\n") mangle_docstrings(app, "class", "pathlib.PosixPath", PosixPath, {}, lines) lines = [x.strip() for x in lines] 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(): """ A function whose docstring has no examples or see also section. Expect SA01 and EX01 errors if validation enabled. """ pass return _function_without_seealso_and_examples @pytest.mark.parametrize( ( "numpydoc_validation_checks", "expected_warn", "non_warnings", ), ( # Validation configured off - expect no warnings (set(), [], []), # Validation on with expected warnings ({"SA01", "EX01"}, ("SA01", "EX01"), []), # Validation on with only one activated check ({"SA01"}, ("SA01",), ("EX01",)), ), ) def test_mangle_docstring_validation_warnings( f, numpydoc_validation_checks, expected_warn, non_warnings, ): app = MockApp() # Set up config for test app.config.numpydoc_validation_checks = numpydoc_validation_checks # Update configuration update_config(app) # Set up logging status, warning = StringIO(), StringIO() logging.setup(app, status, warning) # Run mangle docstrings with the above configuration mangle_docstrings(app, "function", "f", f, None, f.__doc__.split("\n")) # Assert that all (and only) expected warnings are logged warnings = warning.getvalue() for w in expected_warn: assert w in warnings for w in non_warnings: assert w not in warnings def test_mangle_docstring_validation_exclude(): def function_with_bad_docstring(): """ This docstring will raise docstring validation warnings.""" app = MockApp() app.config.numpydoc_validation_checks = {"all"} app.config.numpydoc_validation_exclude = [r"_bad_"] # Call update_config to construct regexp from config value update_config(app) # Setup for catching warnings status, warning = StringIO(), StringIO() logging.setup(app, status, warning) # Run mangle docstrings on function_with_bad_docstring mangle_docstrings( app, "function", function_with_bad_docstring.__name__, function_with_bad_docstring, None, function_with_bad_docstring.__doc__.split("\n"), ) # Validation is skipped due to exclude pattern matching fn name, therefore # no warnings expected assert warning.getvalue() == "" def test_update_config_invalid_validation_set(): app = MockApp() # Results in {'a', 'l'} instead of {"all"} app.config.numpydoc_validation_checks = set("all") with pytest.raises(ValueError, match="Unrecognized validation code"): update_config(app) def test_update_config_exclude_str(): app = MockApp() app.config.numpydoc_validation_checks = set() app.config.numpydoc_validation_exclude = "shouldnt-be-a-str" with pytest.raises(ValueError, match=r"\['shouldnt-be-a-str'\]"): update_config(app) if __name__ == "__main__": import pytest pytest.main()