Skip to content

Commit 61f8c2a

Browse files
committed
test: make Constants attribute docstring doctests actually run
Constants class attributes document their default with a bare string literal after the assignment (e.g. patronymic_name_order = False / """..."""), Sphinx's attribute-docstring convention. That string is never a real __doc__, so doctest.DocTestFinder (and pytest's --doctest-modules) never discovers the .. doctest:: examples inside it -- they can silently go stale, which already happened once for middle_name_as_last. Add tests/test_config_attribute_docstrings.py, which parses the source with ast to recover those literals (the same info Sphinx's static analysis reads) and runs any doctest examples through doctest.DocTestParser/DocTestRunner, so pytest -q now exercises them. Discovery also caught a second dormant docstring (empty_attribute_default) with its own bugs: a missing space after >>> and an expectation of literal None output where the REPL prints nothing for None.
1 parent 5ea10c6 commit 61f8c2a

2 files changed

Lines changed: 81 additions & 2 deletions

File tree

nameparser/config/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,9 @@ class Constants:
323323
>>> from nameparser.config import CONSTANTS
324324
>>> CONSTANTS.empty_attribute_default = None
325325
>>> name = HumanName("John Doe")
326-
>>> name.title
326+
>>> print(name.title)
327327
None
328-
>>>name.first
328+
>>> name.first
329329
'John'
330330
331331
"""
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Exercise the doctest examples embedded in Constants attribute docstrings.
2+
3+
nameparser.config.Constants documents several scalar attributes (e.g.
4+
patronymic_name_order, middle_name_as_last) with a bare string literal placed
5+
right after the class-attribute assignment -- Sphinx's "attribute docstring"
6+
convention, picked up by autodoc's source-level analysis for the built docs.
7+
8+
That convention is invisible to Python at runtime: only module/class/function/
9+
method docstrings become a real __doc__, so a bare string following
10+
`attr = value` is evaluated and discarded. doctest.DocTestFinder walks __doc__
11+
attributes, so pytest's --doctest-modules (see pyproject.toml addopts) never
12+
finds the `.. doctest::` examples inside them -- they can go stale silently.
13+
14+
This module parses the source with `ast` to recover those literals (the same
15+
information Sphinx's static analysis relies on) and runs any doctest examples
16+
found inside them explicitly, so a stale example fails the suite.
17+
"""
18+
import ast
19+
import doctest
20+
import io
21+
from pathlib import Path
22+
23+
import pytest
24+
25+
import nameparser.config as config_module
26+
from nameparser import HumanName
27+
from nameparser.config import CONSTANTS, Constants
28+
29+
CONFIG_SOURCE_PATH = Path(config_module.__file__)
30+
31+
32+
def _constants_attribute_docstrings() -> dict[str, str]:
33+
"""Map attribute name -> bare-string-literal docstring, Constants class body only."""
34+
tree = ast.parse(CONFIG_SOURCE_PATH.read_text(), filename=str(CONFIG_SOURCE_PATH))
35+
(class_node,) = (
36+
node for node in ast.walk(tree)
37+
if isinstance(node, ast.ClassDef) and node.name == 'Constants'
38+
)
39+
docstrings = {}
40+
body = class_node.body
41+
for stmt, following in zip(body, body[1:]):
42+
if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1
43+
and isinstance(stmt.targets[0], ast.Name)):
44+
continue
45+
if isinstance(following, ast.Expr) and isinstance(following.value, ast.Constant) \
46+
and isinstance(following.value.value, str):
47+
docstrings[stmt.targets[0].id] = following.value.value
48+
return docstrings
49+
50+
51+
ATTRIBUTE_DOCSTRINGS = _constants_attribute_docstrings()
52+
53+
DOCTEST_GLOBS = {'HumanName': HumanName, 'CONSTANTS': CONSTANTS, 'Constants': Constants}
54+
55+
56+
def _attrs_with_doctest_examples() -> list[str]:
57+
parser = doctest.DocTestParser()
58+
return sorted(
59+
attr for attr, docstring in ATTRIBUTE_DOCSTRINGS.items()
60+
if parser.get_examples(docstring)
61+
)
62+
63+
64+
@pytest.mark.parametrize("attr", _attrs_with_doctest_examples())
65+
def test_constants_attribute_docstring_examples(attr: str) -> None:
66+
docstring = ATTRIBUTE_DOCSTRINGS[attr]
67+
test = doctest.DocTestParser().get_doctest(
68+
docstring, DOCTEST_GLOBS, attr, str(CONFIG_SOURCE_PATH), 0,
69+
)
70+
output = io.StringIO()
71+
failures, _ = doctest.DocTestRunner().run(test, out=output.write)
72+
assert failures == 0, output.getvalue()
73+
74+
75+
def test_found_expected_attributes_with_doctest_examples() -> None:
76+
"""Guard the discovery mechanism itself: if this drops to zero, the AST
77+
walk above stopped matching Constants's attribute-docstring pattern and
78+
every parametrized case above is silently skipped rather than run."""
79+
assert _attrs_with_doctest_examples()

0 commit comments

Comments
 (0)