Skip to content

Commit a37edf1

Browse files
committed
test(project): every const subpackage is re-exported and documented (#2656)
- Read pcapkit.const's subpackages with pkgutil and require each to be star-imported by pcapkit/const/__init__.py, to list every name of its own __all__ in pcapkit.const.__all__, and to be in the toctree of its index page. - Hold pcapkit.vendor's index page to the toctree rule. Its re-export is held by CrawlerReachabilityTests, so it is not repeated. - Plain vendor modules (__main__, default) are excused with asserted reasons. - KNOWN_UNWIRED records pcapkit.const.linux (#2661), which __init__ does not re-export and whose name is unprefixed. Project pin: textual, imports nothing.
1 parent 32935fc commit a37edf1

1 file changed

Lines changed: 305 additions & 0 deletions

File tree

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# -*- coding: utf-8 -*-
2+
"""Every ``pcapkit.const`` subpackage is re-exported and documented (#2656).
3+
4+
A subpackage whose ``from pcapkit.const.<name> import *`` and ``__all__`` entries leave
5+
:file:`pcapkit/const/__init__.py`, or whose line leaves the toctree in
6+
:file:`docs/source/pcapkit/const/index.rst`, fails here. The subpackages are read from
7+
the tree with :func:`pkgutil.iter_modules`, so one added to the tree is held to both
8+
rules without an edit here, as *Round-Trip Invariants* in
9+
:file:`docs/source/contributing/conventions/test-coverage.rst` asks of a sweep.
10+
11+
* :class:`ReaderTests` -- the toctree, ``__all__`` and star-import readers the sweeps use.
12+
* :class:`ConstPackageTests` -- each subpackage is star-imported by
13+
:mod:`pcapkit.const`, lists every name of its own ``__all__`` in
14+
:data:`pcapkit.const.__all__`, and is in the toctree of the index page.
15+
* :class:`VendorPackageTests` -- the toctree rule for :mod:`pcapkit.vendor`, which
16+
mirrors :mod:`pcapkit.const` (*Const-Vendor Pairing* in
17+
:file:`docs/source/contributing/conventions/const-vendor-pairing.rst`). Its
18+
re-export is held by ``CrawlerReachabilityTests`` in
19+
:file:`tests/vendor/test_crawler_reachability_unit.py`, so it is not repeated.
20+
21+
The check is textual and imports nothing.
22+
23+
"""
24+
25+
from __future__ import annotations
26+
27+
import ast
28+
import pathlib
29+
import pkgutil
30+
import re
31+
import tempfile
32+
import unittest
33+
34+
#: Test tier (#1958): reads the package trees and the docs index pages, and imports nothing.
35+
__tier__ = 'LATEST'
36+
37+
ROOT = pathlib.Path(__file__).resolve().parents[2]
38+
39+
#: The docs pages of :mod:`pcapkit`'s packages.
40+
DOCS = ROOT / 'docs' / 'source' / 'pcapkit'
41+
42+
#: Modules directly under a checked package that are no registry package, so owe no
43+
#: re-export and no toctree line, keyed by package and then module. Each reason is
44+
#: asserted by a test of its own, so an entry cannot outlive it.
45+
NOT_A_REGISTRY = {
46+
'vendor': {
47+
'__main__': 'the pcapkit-vendor command line: it defines main, and the index page '
48+
'documents it under a module directive of its own',
49+
'default': 'the Vendor base class every crawler subclasses: pcapkit.vendor imports it '
50+
'by name and keeps it out of __all__',
51+
},
52+
}
53+
54+
#: Registry packages of :mod:`pcapkit.const` that :file:`pcapkit/const/__init__.py` does not
55+
#: re-export, a recorded defect (#2661). Each reason names what is missing. Wiring a package
56+
#: deletes its entry, which ``test_unwired_entries_hold`` enforces.
57+
KNOWN_UNWIRED = {
58+
'linux': 'no "from pcapkit.const.linux import *" in pcapkit/const/__init__.py, and the name '
59+
'is unprefixed: pcapkit.const.linux.__all__ lists ARPHRD, where the house prefix '
60+
'gives Linux_ARPHRD, as pcapkit.vendor.linux.__all__ lists it',
61+
}
62+
63+
#: A ``.. toctree::`` directive, capturing its indentation.
64+
TOCTREE = re.compile(r'^(\s*)\.\. toctree::\s*$')
65+
66+
#: The target of a ``Title <target>`` toctree entry.
67+
TITLED = re.compile(r'<([^>]+)>\s*$')
68+
69+
70+
def members(package: 'str') -> 'dict[str, bool]':
71+
"""Every module directly under :mod:`pcapkit.<package>`, mapped to whether it is a package."""
72+
return {info.name: info.ispkg
73+
for info in pkgutil.iter_modules([str(ROOT / 'pcapkit' / package)])}
74+
75+
76+
def subpackages(package: 'str') -> 'list[str]':
77+
"""The packages directly under :mod:`pcapkit.<package>`, sorted."""
78+
return sorted(name for name, ispkg in members(package).items() if ispkg)
79+
80+
81+
def parse(path: 'pathlib.Path') -> 'ast.Module':
82+
"""The syntax tree of the module at ``path``."""
83+
return ast.parse(path.read_text(encoding='utf-8'), filename=str(path))
84+
85+
86+
def literal_all(path: 'pathlib.Path') -> 'list[str]':
87+
"""The names in the literal ``__all__`` of the module at ``path``.
88+
89+
Raises:
90+
AssertionError: The module has no ``__all__``, or one that is not a literal list of
91+
names, which this module cannot read.
92+
93+
"""
94+
for node in parse(path).body:
95+
if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == '__all__'
96+
for target in node.targets):
97+
try:
98+
names = ast.literal_eval(node.value)
99+
except ValueError:
100+
names = None
101+
if not isinstance(names, (list, tuple)) or not all(isinstance(name, str) for name in names):
102+
raise AssertionError(f'{path} spells __all__ as something other than a literal list of names')
103+
return list(names)
104+
raise AssertionError(f'{path} defines no __all__')
105+
106+
107+
def star_imports(path: 'pathlib.Path') -> 'set[str]':
108+
"""The modules the module at ``path`` star-imports, at any depth."""
109+
return {node.module for node in ast.walk(parse(path))
110+
if isinstance(node, ast.ImportFrom) and node.level == 0 and node.module is not None
111+
and any(alias.name == '*' for alias in node.names)}
112+
113+
114+
def toctree_entries(path: 'pathlib.Path') -> 'set[str]':
115+
"""The documents listed by every toctree in the page at ``path``, as written.
116+
117+
A ``Title <target>`` entry counts as its target. Options and comments are skipped.
118+
119+
"""
120+
entries = set() # type: set[str]
121+
indent = None # type: int | None
122+
for line in path.read_text(encoding='utf-8').splitlines():
123+
directive = TOCTREE.match(line)
124+
if directive:
125+
indent = len(directive.group(1))
126+
continue
127+
if indent is None or not line.strip():
128+
continue
129+
if len(line) - len(line.lstrip()) <= indent:
130+
indent = None
131+
continue
132+
text = line.strip()
133+
if text.startswith((':', '..')):
134+
continue
135+
titled = TITLED.search(text)
136+
entries.add(titled.group(1) if titled else text)
137+
return entries
138+
139+
140+
def index_page(package: 'str') -> 'pathlib.Path':
141+
"""The page that documents :mod:`pcapkit.<package>`."""
142+
return DOCS / package / 'index.rst'
143+
144+
145+
def unlisted(package: 'str') -> 'list[str]':
146+
"""The subpackages of :mod:`pcapkit.<package>` that no toctree of its index page lists."""
147+
listed = toctree_entries(index_page(package))
148+
return [name for name in subpackages(package) if name not in listed]
149+
150+
151+
def unexcused(package: 'str') -> 'list[str]':
152+
"""The plain modules directly under :mod:`pcapkit.<package>` that ``NOT_A_REGISTRY`` omits."""
153+
plain = {name for name, ispkg in members(package).items() if not ispkg}
154+
return sorted(plain - set(NOT_A_REGISTRY.get(package, ())))
155+
156+
157+
def stale_excuses(package: 'str') -> 'list[str]':
158+
"""The ``NOT_A_REGISTRY`` entries of ``package`` that are not plain modules under it."""
159+
plain = {name for name, ispkg in members(package).items() if not ispkg}
160+
return sorted(set(NOT_A_REGISTRY.get(package, ())) - plain)
161+
162+
163+
class ReaderTests(unittest.TestCase):
164+
"""The readers find what the sweeps rely on, so a green sweep is not a reader that saw nothing."""
165+
166+
def write(self, text: 'str') -> 'pathlib.Path':
167+
"""A scratch file holding ``text``."""
168+
directory = tempfile.TemporaryDirectory()
169+
self.addCleanup(directory.cleanup)
170+
path = pathlib.Path(directory.name) / 'page'
171+
path.write_text(text, encoding='utf-8')
172+
return path
173+
174+
def test_toctree_entries_read_every_form(self) -> 'None':
175+
"""Plain, titled and nested entries count; options, comments and prose do not."""
176+
path = self.write('Title\n=====\n\n'
177+
'.. toctree::\n :maxdepth: 2\n :caption: Layers\n\n'
178+
' plain\n Shown <titled>\n .. commented\n\n'
179+
'prose after the block\n\n'
180+
'.. only:: html\n\n .. toctree::\n\n nested\n\n'
181+
'.. note::\n\n not_in_a_toctree\n')
182+
self.assertEqual(toctree_entries(path), {'plain', 'titled', 'nested'})
183+
184+
def test_literal_all_reads_a_list_and_refuses_the_rest(self) -> 'None':
185+
"""A missing, computed or non-string ``__all__`` raises instead of reading as empty."""
186+
self.assertEqual(literal_all(self.write("__all__ = ['a', 'b']\n")), ['a', 'b'])
187+
for text in ('x = 1\n', '__all__ = sorted(names)\n', '__all__ = [1]\n'):
188+
with self.subTest(text=text), self.assertRaises(AssertionError):
189+
literal_all(self.write(text))
190+
191+
def test_star_imports_read_nested_absolute_ones(self) -> 'None':
192+
"""A star import under ``if``/``else`` counts; a named or relative one does not."""
193+
path = self.write('from a.b import *\nif x:\n from c import *\nfrom d import e\nfrom . import *\n')
194+
self.assertEqual(star_imports(path), {'a.b', 'c'})
195+
196+
197+
class ConstPackageTests(unittest.TestCase):
198+
"""Each :mod:`pcapkit.const` subpackage is re-exported and listed in its index page."""
199+
200+
init = ROOT / 'pcapkit' / 'const' / '__init__.py'
201+
202+
def test_the_walk_found_subpackages_and_a_toctree(self) -> 'None':
203+
"""An empty walk or toctree would let every loop below pass without checking a member."""
204+
self.assertTrue(subpackages('const'))
205+
self.assertTrue(toctree_entries(index_page('const')))
206+
207+
def test_every_subpackage_is_star_imported(self) -> 'None':
208+
"""Its ``from pcapkit.const.<name> import *`` is what puts the names on the package."""
209+
stars = star_imports(self.init)
210+
missing = [name for name in subpackages('const')
211+
if name not in KNOWN_UNWIRED and f'pcapkit.const.{name}' not in stars]
212+
self.assertEqual(missing, [], 'add a "from pcapkit.const.<name> import *" for each')
213+
214+
def test_every_subpackage_name_is_in_all(self) -> 'None':
215+
"""Each name of its own ``__all__`` is in :data:`pcapkit.const.__all__`."""
216+
exported = set(literal_all(self.init))
217+
missing = {} # type: dict[str, list[str]]
218+
for name in subpackages('const'):
219+
if name in KNOWN_UNWIRED:
220+
continue
221+
names = literal_all(self.init.parent / name / '__init__.py')
222+
self.assertTrue(names, f'pcapkit.const.{name}.__all__ is empty')
223+
lacking = [item for item in names if item not in exported]
224+
if lacking:
225+
missing[name] = lacking
226+
self.assertEqual(missing, {}, 'add each name to __all__ in pcapkit/const/__init__.py')
227+
228+
def test_every_subpackage_is_in_the_toctree(self) -> 'None':
229+
"""A package with no toctree line is orphaned from the reference."""
230+
self.assertEqual(unlisted('const'), [],
231+
f'add each to a toctree of {index_page("const").relative_to(ROOT)}')
232+
233+
def test_every_plain_module_is_excused(self) -> 'None':
234+
"""A module that is no package owes neither a re-export nor a line, and says why."""
235+
self.assertEqual(unexcused('const'), [],
236+
'make each a package, or add it to NOT_A_REGISTRY with the reason')
237+
self.assertEqual(stale_excuses('const'), [])
238+
239+
def test_unwired_entries_hold(self) -> 'None':
240+
"""An entry names a real package that is neither star-imported nor listed in ``__all__``."""
241+
stars = star_imports(self.init)
242+
exported = set(literal_all(self.init))
243+
for name in KNOWN_UNWIRED:
244+
with self.subTest(package=name):
245+
self.assertTrue(name in subpackages('const'), 'names no subpackage of pcapkit.const')
246+
self.assertFalse(f'pcapkit.const.{name}' in stars, 'is star-imported; delete the entry')
247+
listed = [item for item in literal_all(self.init.parent / name / '__init__.py')
248+
if item in exported]
249+
self.assertEqual(listed, [], 'is listed in __all__; delete the entry')
250+
251+
252+
class VendorPackageTests(unittest.TestCase):
253+
"""The :mod:`pcapkit.vendor` index page lists every crawler package."""
254+
255+
vendor = ROOT / 'pcapkit' / 'vendor'
256+
257+
def test_the_walk_found_subpackages_and_a_toctree(self) -> 'None':
258+
"""An empty walk or toctree would let the loop below pass without checking a member."""
259+
self.assertTrue(subpackages('vendor'))
260+
self.assertTrue(toctree_entries(index_page('vendor')))
261+
262+
def test_every_subpackage_is_in_the_toctree(self) -> 'None':
263+
"""A crawler package with no toctree line is orphaned from the reference."""
264+
self.assertEqual(unlisted('vendor'), [],
265+
f'add each to a toctree of {index_page("vendor").relative_to(ROOT)}')
266+
267+
def test_every_plain_module_is_excused(self) -> 'None':
268+
"""A module that is no package owes no toctree line, and says why."""
269+
self.assertEqual(unexcused('vendor'), [],
270+
'make each a package, or add it to NOT_A_REGISTRY with the reason')
271+
self.assertEqual(stale_excuses('vendor'), [])
272+
273+
def command_line_is_no_registry(self) -> 'None':
274+
"""``__main__`` defines ``main``, and the index page documents it by a module directive."""
275+
defined = {node.name for node in parse(self.vendor / '__main__.py').body
276+
if isinstance(node, ast.FunctionDef)}
277+
self.assertIn('main', defined)
278+
self.assertIn('.. module:: pcapkit.vendor.__main__',
279+
index_page('vendor').read_text(encoding='utf-8'))
280+
281+
def base_class_is_no_registry(self) -> 'None':
282+
"""``default`` defines ``Vendor``, which the package imports by name and does not export."""
283+
defined = {node.name for node in parse(self.vendor / 'default.py').body
284+
if isinstance(node, ast.ClassDef)}
285+
self.assertIn('Vendor', defined)
286+
init = self.vendor / '__init__.py'
287+
imported = [alias.name for node in ast.walk(parse(init))
288+
if isinstance(node, ast.ImportFrom) and node.module == 'pcapkit.vendor.default'
289+
for alias in node.names]
290+
self.assertEqual(imported, ['Vendor'])
291+
self.assertNotIn('Vendor', literal_all(init))
292+
293+
def test_every_excuse_holds_its_reason(self) -> 'None':
294+
"""Each ``NOT_A_REGISTRY`` entry has an assertion of its reason, and no assertion is orphaned."""
295+
asserted = {'__main__': self.command_line_is_no_registry,
296+
'default': self.base_class_is_no_registry}
297+
self.assertEqual(sorted(asserted), sorted(NOT_A_REGISTRY['vendor']),
298+
'assert the reason of each excuse, and only of those')
299+
for name, check in asserted.items():
300+
with self.subTest(module=name):
301+
check()
302+
303+
304+
if __name__ == '__main__':
305+
unittest.main()

0 commit comments

Comments
 (0)