diff --git a/docs/source/contributing/conventions/process.rst b/docs/source/contributing/conventions/process.rst index 1d65f0ed5..d3c744743 100644 --- a/docs/source/contributing/conventions/process.rst +++ b/docs/source/contributing/conventions/process.rst @@ -100,13 +100,12 @@ commands down instead of a figure that will be stale by the next merge: The grouping scheme was settled on :issue:`918`: **a section per top-level module, with** ``Added``/``Changed``/``Fixed`` **nested inside each** -- module granularity, not -per-file and not per-subpackage. The file carries **10** module-level sections holding -298 entries, and no entry carries an inline kind label:: +per-file and not per-subpackage. No entry in the file carries an inline kind label:: $ grep -cE '^\* \*\*(Added|Changed|Fixed)\*\*' docs/source/changelog/1.5.0.rst 0 -Nine of those ten name a module; the tenth, *Project infrastructure*, is for what +Each section names a module except *Project infrastructure*, which is for what belongs to none. The map is one-to-one with the package list below -- every package, :mod:`pcapkit.interface` included, has a section of its own: diff --git a/docs/source/contributing/conventions/registry-protocol.rst b/docs/source/contributing/conventions/registry-protocol.rst index e35639a17..7b25d50ac 100644 --- a/docs/source/contributing/conventions/registry-protocol.rst +++ b/docs/source/contributing/conventions/registry-protocol.rst @@ -104,9 +104,9 @@ Three things about it are easy to get wrong: Re-parenting every non-registry enumeration onto :class:`~pcapkit.corekit.enum.EnumLookup` was **phase 2** of - :issue:`877`, and it is **complete**: the phase landed for 24 of the 24 - non-registry enumerations. **Zero enumerations remain outside the hierarchy**, - measured by a runtime walk over both the :mod:`enum` and ``aenum`` flavours. + :issue:`877`, and it is **complete**: the phase landed for every non-registry + enumeration. **No enumeration remains outside the hierarchy**, measured by a + runtime walk over both the :mod:`enum` and ``aenum`` flavours. Failed-Lookup Exceptions ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -280,7 +280,7 @@ and it is the trap for whoever writes the next override: the base branches on branched on ``isinstance(key, int)`` and fell through to the *name* path for anything else. ``get(None)`` therefore raised a quiet :exc:`~pcapkit.utilities.exceptions.EnumKeyError` on those two and a loud -:exc:`~pcapkit.utilities.exceptions.EnumValueError` on the other four of the six +:exc:`~pcapkit.utilities.exceptions.EnumValueError` on the other locally-defined helpers in those two modules, and no prose anywhere said so. Re-implementing the dispatch is how an override acquires a divergence nobody wrote down; delegating to it is how it does not. @@ -366,26 +366,24 @@ The Audit, per Class registry added later has something to check itself against. The owner set its scope: audit every registry first, and decide case-sensitivity per registry from that. -The population it covers, with the counting convention spelled out because the -figures move: **127** :class:`~pcapkit.corekit.enum.EnumRegistry` subclasses, every -one of them under :mod:`pcapkit.const`, across 124 files -- 117 :class:`int`-valued -(of which 5 are flag registries) and 10 ``aenum.StrEnum``-valued. Plus **24** -non-registry enumerations counted by a runtime walk over both the :mod:`enum` and -``aenum`` flavours and including nested classes: 17 top level (3 of them under -:mod:`pcapkit.const` itself) and 7 nested, the nested ones being -``FrameType.Flags`` in :mod:`pcapkit.protocols.schema.application.httpv2` plus its -6 concrete per-frame subclasses. 151 enumerations in total. - -**The** :class:`int`\ **-valued tier, all 117, is case-sensitive, and the criterion is -vacuous on it rather than merely unmet.** A registry whose values are numbers has +The population it covers is every :class:`~pcapkit.corekit.enum.EnumRegistry` +subclass, every one of them under :mod:`pcapkit.const` and each either +:class:`int`-valued (the flag registries among them) or ``aenum.StrEnum``-valued, +plus every non-registry enumeration a runtime walk over both the :mod:`enum` and +``aenum`` flavours finds, nested classes included -- such as ``FrameType.Flags`` in +:mod:`pcapkit.protocols.schema.application.httpv2` and its concrete per-frame +subclasses. + +**The** :class:`int`\ **-valued tier is case-sensitive throughout, and the criterion +is vacuous on it rather than merely unmet.** A registry whose values are numbers has nothing for case to apply to; the only way a string reaches :meth:`~pcapkit.corekit.enum.EnumLookup.get` on one is as a member *name*, and a name is the Python identifier :meth:`pcapkit.vendor.default.Vendor.safe_name` derives from the registry's own name column -- it preserves the registrar's casing exactly, but it is not itself a value any specification states a comparison rule for. Measured across -all 151 enumerations: exactly **one** would collide if names were folded -- -:class:`~pcapkit.const.hip.parameter.Parameter`, on ``R1_Counter`` against -``R1_COUNTER`` -- and **no** enumeration anywhere has two ``str`` *values* that +every enumeration: **only** :class:`~pcapkit.const.hip.parameter.Parameter` would +collide if names were folded, on ``R1_Counter`` against ``R1_COUNTER`` -- and +**no** enumeration anywhere has two ``str`` *values* that collide when folded. So folding names is not merely unjustified, it is unsafe in a measured case; folding values is safe but unjustified except where the table below says otherwise. @@ -466,16 +464,16 @@ That leaves the classes with something to decide: ``a`` 18, ``p`` 16, ``s/p`` 3, blank 1; ``o`` 28, ``m`` 27, ``h`` 7, ``m [1]`` 2). Both tallies cover all 64 rows. - **open** -- see below - * - The 6 :mod:`~pcapkit.protocols.internet.mh` and + * - The :mod:`~pcapkit.protocols.internet.mh` and :mod:`~pcapkit.protocols.application.ngap` helper enumerations - IANA Mobility Header registries, 3GPP TS 38.413 - ``FastBindingAcknowledgmentStatus``, ``IPv6AddressPrefixCode``, ``LMAAddressCode``, ``LocalizedRoutingStatus``, ``Criticality`` and ``PDUKind``. - **Five** of the six carry numeric codes, so the criterion is vacuous on them + **All but** ``PDUKind`` carry numeric codes, so the criterion is vacuous on them exactly as for the :class:`int` tier above; ``PDUKind``'s are :class:`str` ASN.1 identifiers from 3GPP TS 38.413, and ASN.1 identifiers are case-significant by construction, so it lands the same way for a different reason. - **None** of the six defines a ``get`` of its own. Two did when this + **None** of them defines a ``get`` of its own. Two did when this audit was taken -- ``FastBindingAcknowledgmentStatus`` and ``IPv6AddressPrefixCode``, for signature reasons (no ``default``, and an :class:`int`/:class:`str` dispatch) diff --git a/docs/source/contributing/conventions/sentinel-convention.rst b/docs/source/contributing/conventions/sentinel-convention.rst index 1f0f0fce5..d6fc2c96e 100644 --- a/docs/source/contributing/conventions/sentinel-convention.rst +++ b/docs/source/contributing/conventions/sentinel-convention.rst @@ -14,7 +14,7 @@ with no rule naming any of them wrong. GitHub issue :issue:`937` closed that gap owner ruled for SCREAMING_SNAKE and accepted the resulting breaking change outright, with no backport. So the object is named in SCREAMING_SNAKE and the type-naming rule above derives from it mechanically -- title-case each underscore-separated word and append -``Type``, no per-sentinel exception needed. The four in the tree follow it: +``Type``, no per-sentinel exception needed. The sentinels in the tree follow it: .. list-table:: :header-rows: 1 diff --git a/docs/source/contributing/releasing.rst b/docs/source/contributing/releasing.rst index 86505ba8a..c02b99066 100644 --- a/docs/source/contributing/releasing.rst +++ b/docs/source/contributing/releasing.rst @@ -331,13 +331,14 @@ checks *is* the one it is about to create: * ``tag`` checks whether ``conda-+0``, the tag *it* creates, already exists. -* ``pypi`` compares PyPI's file count for the version against the 7 expected - -- the 6 supported legs' wheels plus the sdist kept only on the 3.15 leg; the +* ``pypi`` compares PyPI's file count for the version against the expected one + -- a wheel per supported leg plus the sdist kept only on the 3.15 leg; the KTLO legs' wheels (`#1633 `__) are not counted. This job is already idempotent (``skip-existing: true`` on both upload steps), so the check exists only to save the cost of the build matrix, not for correctness. -* ``conda`` compares Anaconda's aggregate count against the 10 expected (2 OS - x 5 Python versions) as the same kind of job-level cost saver, and *also* +* ``conda`` compares Anaconda's aggregate count against the expected one (a + file per OS and Python version of its matrix) as the same kind of job-level + cost saver, and *also* checks, per matrix leg, whether that leg's own platform/Python distribution is already there -- this second check is the actual correctness fix, because ``anaconda/actions/upload-package`` has no ``skip-existing`` equivalent, only diff --git a/docs/source/contributing/workflows.rst b/docs/source/contributing/workflows.rst index 90d90ec3c..39799eda8 100644 --- a/docs/source/contributing/workflows.rst +++ b/docs/source/contributing/workflows.rst @@ -3,7 +3,7 @@ GitHub Actions Workflows .. important:: - The thirteen workflows under :file:`.github/workflows/` trigger each other, and + The workflows under :file:`.github/workflows/` trigger each other, and the chain that results is not visible from any single file -- reading one ``on:`` block never shows what a *different* workflow's completion goes on to start. This page is the repository-wide graph. The release *pipeline* @@ -295,15 +295,13 @@ repository target the same reusable workflow, with the same input: * ``.github/workflows/deploy-pages.yml:55`` -- job ``unit-tests`` calls ``./.github/workflows/unit-tests.yml`` with ``gate-only: true``. -``gate-only: true`` selects **three** of ``unit-tests.yml``'s **fourteen** jobs, -not one: ``gate`` (the full suite, on ``latest``), ``changelog`` -(checks ``CHANGELOG.md`` against its source entry) and ``python-versions``, -which reads ``gate``'s version (see `Python Versions`_ below). It skips the other -**eleven** -- the eight matrix jobs, ``test`` (five Python legs), ``test-ktlo`` -(four), ``integration`` (five), ``integration-ktlo`` (four), -``engine-tests`` (one job for each of five engines, each looping over six -Python versions), ``pypcap-parity`` (one), ``pypcap-parity-ktlo`` (one) and -``unittest-ordering`` (eleven matrix cells), each of which has already run once for this commit +``gate-only: true`` selects not only ``unit-tests.yml``'s ``gate`` (the full +suite, on ``latest``) but also ``changelog`` (checks ``CHANGELOG.md`` against its +source entry) and ``python-versions``, which reads ``gate``'s version (see +`Python Versions`_ below). It skips the rest -- the matrix jobs, ``test``, +``test-ktlo``, ``integration``, ``integration-ktlo``, ``engine-tests`` (a job per +engine, each looping over its Python versions), ``pypcap-parity``, +``pypcap-parity-ktlo`` and ``unittest-ordering``, each of which has already run once for this commit from Unit Tests' own ``push`` trigger (and, all but ``unittest-ordering``, which runs on ``main`` pushes only, from its ``pull_request`` trigger), so running any of them again per caller would @@ -603,7 +601,7 @@ in the supported tier, so its cells block on every run. Required Status Checks ---------------------- -Ruleset ``23497679`` on ``main`` requires six contexts, with +Ruleset ``23497679`` on ``main`` requires the contexts below, with ``strict_required_status_checks_policy: true`` (a branch must be up to date with ``main`` before it can merge, not merely green). Each was found by grepping every workflow file for its name: @@ -640,14 +638,14 @@ between the tiers below means editing the ruleset in the same change. ``Required checks passed`` is defined by exactly one job (``unit-tests.yml:1937``). ``Compat Python`` is defined by one, the ``compatibility`` job in ``python-compatibility.yml`` (``:60``), whose matrix -(``:74``) comes from :file:`.github/python-versions.toml`: the five required legs -are its supported tier (``python-versions.toml:20-24``), and its four KTLO legs +(``:74``) comes from :file:`.github/python-versions.toml`: the required legs +are its supported tier (``python-versions.toml:20-24``), and its KTLO legs (``python-versions.toml:16-19``) have names ending in ``(KTLO)``, so no required context matches one. A comment at ``unit-tests.yml:1849`` mentions the string without defining it. ``Required checks passed`` is an aggregate, not a single check run. The ruleset -requires it and the five ``Compat Python 3.11``-``3.15`` legs, nothing else, as +requires it and the ``Compat Python 3.11``-``3.15`` legs, nothing else, as ``unit-tests.yml``'s own comment (``unit-tests.yml:1848-1858``) records. It ``needs:`` the gating jobs of ``unit-tests.yml`` -- ``test``, ``integration``, ``engine-tests`` and ``pypcap-parity``, plus ``changes`` and ``project-tests`` @@ -721,8 +719,8 @@ are they in ``pypcap-parity``, which also runs on ``ubuntu-latest``; none of the was measured on that job. The **experimental** tier is free-threaded CPython and GraalPy, spelt as -setup-python reads them (``3.14t``, ``graalpy-25.4``). Each runs one leg of -``experimental`` (two), the one matrix job of :file:`.github/workflows/experimental.yml` +setup-python reads them (``3.14t``, ``graalpy-25.4``). Each runs a leg of +``experimental``, the matrix job of :file:`.github/workflows/experimental.yml` ("Experimental Tests"), named ``Python (experimental)``, and nothing else: no ``integration``, ``Compat``, wheel or conda leg, so never a required context. They were legs of ``test`` until `#1732 @@ -856,8 +854,8 @@ through ``fromJSON``, and each single-interpreter job reads ``latest``. off ``latest``, with its reason in the comment above it, such as the conda jobs' ``!= '3.15'``. ``tests/project/test_python_versions_1660.py`` fails on a pin the file does not list, on an exemption with no comment, and on a -classifier, doc or comment that no longer matches the file -- the coverage -leg's version, the release checks' counts. Its tests, and the other tests that +classifier, doc or comment that no longer matches the file -- a tier range, +the coverage leg's version. Its tests, and the other tests that read the file, skip on the KTLO interpreters, which have no ``tomllib``. Two edits it names rather than makes: a new ``pypi`` KTLO leg needs its own ``ktlo_*`` output for ``release_status``, and a pre-release leg needs diff --git a/tests/corekit/test_sentinel_exports_unit.py b/tests/corekit/test_sentinel_exports_unit.py index b0146bd74..3c75d73d6 100644 --- a/tests/corekit/test_sentinel_exports_unit.py +++ b/tests/corekit/test_sentinel_exports_unit.py @@ -376,14 +376,17 @@ def test_every_sentinel_object_is_screaming_snake(self) -> 'None': self.assertRegex(name, pattern) def test_conventions_doc_lists_every_sentinel_in_the_tree(self) -> 'None': - """The doc said "three" and listed three; ``_Absent`` was the fourth. - - Asserting the names rather than only the count, because a count corrected - without the row -- or a row added without the count -- is the same defect - in a different place. The "Defined in" column now names - :data:`CANONICAL_MODULE` for every row, since GitHub issue #911's housing - move gave all four the same defining module -- checked once, outside the - loop, rather than once per row against a value that no longer varies. + """The doc once stated a count and listed that many; ``_Absent`` was missing. + + Asserting the names, because a row missing from the table is the defect, and + the page now states no count for a row to disagree with (#1103's ruling, + #1811). The table's instance column is read back against every singleton + :data:`CANONICAL_MODULE` defines, both ways, so a sentinel added there without + a row, or a row left for one removed, fails here. The "Defined in" column + names :data:`CANONICAL_MODULE` for every row, since GitHub issue #911's + housing move gave every sentinel the same defining module -- checked once, + outside the loop, rather than once per row against a value that no longer + varies. """ section = _sentinel_section() @@ -394,8 +397,13 @@ def test_conventions_doc_lists_every_sentinel_in_the_tree(self) -> 'None': self.assertIn(f'``{type_.__name__}``', section) self.assertIn(CANONICAL_MODULE, section) - self.assertIn('four in the tree follow it', section) - self.assertNotIn('three in the tree follow it', section) + canonical = importlib.import_module(CANONICAL_MODULE) + defined = {name for name, value in vars(canonical).items() + if not isinstance(value, type) and type(value).__module__ == CANONICAL_MODULE} + # The list-table's body: the block after its options, up to the next blank line. + table = section.split('.. list-table::', 1)[1].split('\n\n')[1] + rows = set(re.findall(r'(?m)^ \* - ``(\w+)``$', table)) + self.assertEqual(rows, defined) self.assertNotIn('The three sentinels deliberately differ', section) def test_conventions_doc_records_that_only_the_object_is_exported(self) -> 'None': diff --git a/tests/project/test_conventions_doc_claims.py b/tests/project/test_conventions_doc_claims.py index d51d06202..ded55a56e 100644 --- a/tests/project/test_conventions_doc_claims.py +++ b/tests/project/test_conventions_doc_claims.py @@ -23,10 +23,12 @@ exists, lives in exactly its own file, is listed in the index's toctree, and the top-level :file:`docs/source/index.rst` points at the new index page rather than the retired bare document path. -* :class:`PhaseTwoRemainderTests` -- the three counts the page states about - `#877 `__'s phase 2, measured - rather than remembered. The page said the phase *"has not happened yet"* for as long - as it did because nothing contradicted it once #921 landed. +* :class:`PhaseTwoRemainderTests` -- what the page says about + `#877 `__'s phase 2 and the + population its audit covers, measured rather than remembered. The page said the + phase *"has not happened yet"* for as long as it did because nothing contradicted + it once #921 landed. The page states no counts (#1103's ruling, #1811), so what is + checked is each claim about the whole population. * :class:`ExtensionHeaderClassificationTests` -- the page's bases-per-header table against the declarations themselves. ``tests/protocols/internet/test_ipv6_ext_unit.py`` already pins the *code* to the ruling; nothing pinned the *page* to the code, which is @@ -128,16 +130,6 @@ #: shared page to slice between two of them. PAGES = {anchor: CONVENTIONS_DIR / f'{anchor}.rst' for anchor in ANCHORS} -#: Number words as the page spells them, so a count can be read back out of the prose. -#: The page states its figures in words rather than digits, which is house style there. -#: ``'zero'`` joined the set with GitHub issue #930, once phase 2 finished and the -#: outside-the-hierarchy count it counts dropped to none. -NUMBER_WORDS = { - 'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, - 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, -} - - def _page(anchor: 'str') -> 'str': """``anchor``'s own page, whole. @@ -404,7 +396,7 @@ def test_the_index_preamble_does_not_claim_to_be_a_ruling_page_itself(self) -> ' class PhaseTwoRemainderTests(unittest.TestCase): - """The page's counts for GitHub issue #877's phase 2, measured.""" + """The page's claims about GitHub issue #877's phase 2, measured.""" def setUp(self) -> 'None': from pcapkit.corekit.enum import EnumLookup, EnumRegistry @@ -424,22 +416,23 @@ def test_the_walk_found_something_to_count(self) -> 'None': 'the enumeration walk collapsed; the counts below would ' 'pass vacuously') - def test_the_page_states_the_measured_number_outside_the_hierarchy(self) -> 'None': - """*"Zero enumerations remain outside the hierarchy"* -- against a runtime walk. + def test_the_page_says_phase_two_is_complete_and_the_walk_agrees(self) -> 'None': + """*"No enumeration remains outside the hierarchy"* -- against a runtime walk. - GitHub issue #930 finished phase 2, so the page's own wording moved from - *"are still outside"* -- true while seven remained -- to *"remain outside"*, - which reads correctly now that none do. + GitHub issue #930 finished phase 2, so the page says it landed for every + non-registry enumeration. That replaces two figure checks, on how many + remained outside and how many of the total had landed (#1811): both said + the same thing as the walk finding none outside, which is what fails here + once an enumeration is added outside :class:`~pcapkit.corekit.enum.EnumLookup`. """ - stated = re.search(r'\*\*(\w+) enumerations remain outside the hierarchy\*\*', - self.note) - self.assertIsNotNone(stated, 'the page no longer states how many enumerations ' - 'are outside EnumLookup; the wording this test ' - 'reads has changed') - assert stated is not None # for type checkers; asserted above - self.assertEqual(NUMBER_WORDS[stated.group(1).lower()], len(self.outside), - f'the page says {stated.group(1)!r} but the tree has ' - f'{len(self.outside)}: {sorted(self.outside.values())}') + for claim in ('the phase landed for every non-registry enumeration', + '**No enumeration remains outside the hierarchy**'): + with self.subTest(claim=claim): + self.assertIn(claim, self.note, + 'the page no longer says phase 2 is complete; the wording ' + 'this test reads has changed') + self.assertEqual(sorted(self.outside.values()), [], + 'enumerations outside EnumLookup, which the page says none are') def test_the_page_names_every_enumeration_outside_the_hierarchy(self) -> 'None': """The count alone would pass on a wrong list of the right length. @@ -447,7 +440,7 @@ def test_the_page_names_every_enumeration_outside_the_hierarchy(self) -> 'None': Vacuous while :attr:`self.outside` is empty -- GitHub issue #930 emptied it, and an empty dict gives the loop below nothing to iterate, so this method cannot fail no matter what the page says. - ``test_the_page_states_the_measured_number_outside_the_hierarchy`` is + ``test_the_page_says_phase_two_is_complete_and_the_walk_agrees`` is what actually pins the empty state; this one is dormant rather than deleted, for whenever a future regression makes :attr:`self.outside` non-empty again, at which point it resumes checking that the page names @@ -460,39 +453,15 @@ def test_the_page_names_every_enumeration_outside_the_hierarchy(self) -> 'None': with self.subTest(enumeration=name): self.assertIn(name.rsplit('.', maxsplit=1)[-1], self.note) - def test_the_page_states_the_measured_phase_two_progress(self) -> 'None': - """*"landed for 24 of the 24 non-registry enumerations"*, both figures. - - 17 of 24 until GitHub issue #930 finished phase 2's remaining seven -- - ``CommandType``, ``ConformanceRequirement``, ``ESPStatus`` and the four - Mobility Header helpers -- so both figures now read the same. - """ - stated = re.search(r'landed for (\d+) of the (\d+) non-registry enumerations', - self.note) - self.assertIsNotNone(stated, 'the page no longer states phase 2 progress in ' - 'the shape this test reads') - assert stated is not None # for type checkers; asserted above - done, total = int(stated.group(1)), int(stated.group(2)) - self.assertEqual(total, len(self.non_registry), - 'the page\'s non-registry enumeration count is stale') - self.assertEqual(done, len(self.non_registry) - len(self.outside), - 'the page\'s re-parented count is stale') - - def test_the_audit_population_figures_are_the_measured_ones(self) -> 'None': + def test_the_audit_population_claims_hold_for_the_whole_population(self) -> 'None': """*The Audit, per Class*'s own population, against the same runtime walk. - Six figures the section opens with -- the registry count, that every one of - them is under :mod:`pcapkit.const`, the file count, the :class:`int`-valued - and flag splits, the ``aenum.StrEnum`` count and the grand total -- were - prose-only until GitHub issue #949 wired them to the walk. The section says - itself that *the figures move*, which is precisely the case that needs a - measurement rather than a reader's diligence: #930 moved the non-registry - figures once already, and only those had a test. - - Each figure is read out of the page by its own regex and compared, so a stale - one fails naming both numbers. Wording is not pinned; the regexes are - deliberately loose about the prose between the figures and would survive a - rephrasing that kept the claims. + The section opened with figures for the registries, their files, the + :class:`int`-valued, flag and ``aenum.StrEnum`` splits and the grand total, + which GitHub issue #949 wired to the walk and #1811 removed with the figures + themselves (#1103's ruling: a count drifts). What the section still claims of + the whole population is checked here: every registry is :class:`int`- or + ``str``-valued, and every one of them is under :mod:`pcapkit.const`. """ from pcapkit.corekit.enum import EnumRegistry @@ -500,19 +469,16 @@ def test_the_audit_population_figures_are_the_measured_ones(self) -> 'None': registries = {cls: name for cls, name in self.enumerations.items() if issubclass(cls, EnumRegistry) and cls is not EnumRegistry} # ``issubclass(cls, int)`` rather than a check on ``_member_type_``: the flag - # registries are ``IntFlag`` subclasses and have to count inside the int tier, - # which is what the page's parenthetical "(of which N are flag registries)" - # says -- a disjoint reading would make the two figures fail to add up. + # registries are ``IntFlag`` subclasses and belong inside the int tier, which + # is where the page's parenthetical "(the flag registries among them)" puts + # them. int_valued = {c: n for c, n in registries.items() if issubclass(c, int)} str_valued = {c: n for c, n in registries.items() if issubclass(c, str)} - flags = {c: n for c, n in registries.items() - if issubclass(c, (enum.Flag, aenum.Flag))} - files = {cls.__module__ for cls in registries} # Guards the rest from passing on a collapsed walk, as - # ``test_the_walk_found_something_to_count`` does for the counts above. + # ``test_the_walk_found_something_to_count`` does for the checks above. self.assertGreater(len(registries), 100, - 'the registry half of the walk collapsed; the figures below ' + 'the registry half of the walk collapsed; the checks below ' 'would pass vacuously') neither = sorted(name for cls, name in registries.items() if not issubclass(cls, (int, str))) @@ -520,32 +486,8 @@ def test_the_audit_population_figures_are_the_measured_ones(self) -> 'None': 'a registry is neither int- nor str-valued, so the page\'s ' f'two-tier split no longer partitions the population: {neither}') - for pattern, measured, what in ( - (r'\*\*(\d+)\*\* :class:`~pcapkit\.corekit\.enum\.EnumRegistry` subclasses', - len(registries), 'EnumRegistry subclasses'), - (r'across (\d+) files', len(files), 'files holding a registry'), - (r'(\d+) :class:`int`\\?-valued', len(int_valued), 'int-valued registries'), - (r'of which (\d+) are flag registries', len(flags), 'flag registries'), - (r'(\d+) ``aenum\.StrEnum``\\?-valued', len(str_valued), - 'str-valued registries'), - (r'\*\*(\d+)\*\* non-registry enumerations', len(self.non_registry), - 'non-registry enumerations'), - (r'(\d+) enumerations in total', len(self.enumerations), - 'enumerations in total'), - ): - with self.subTest(figure=what): - stated = re.search(pattern, self.note) - self.assertIsNotNone( - stated, f'the page no longer states how many {what} there are in ' - 'the shape this test reads; re-derive the figure rather ' - 'than deleting the check') - assert stated is not None # for type checkers; asserted above - self.assertEqual(int(stated.group(1)), measured, - f'the page says {stated.group(1)} {what}, the tree has ' - f'{measured}') - # "every one of them under pcapkit.const" is a claim about the *whole* - # population, not a count, so a figure comparison cannot reach it. + # population, so the walk is asked about every registry. self.assertIn('every one of them under :mod:`pcapkit.const`', self.note, 'the page no longer claims every registry lives under ' 'pcapkit.const, so this check is pinning a claim it has dropped') @@ -1087,17 +1029,17 @@ def test_a_non_string_key_raises_a_value_miss_on_every_helper(self) -> 'None': The base branches on ``isinstance(key, str)`` and treats everything else as a value, so ``get(None)`` is a *value* miss. The deleted overrides branched on :class:`int` and fell through to the name path, making it a ``KeyError`` - on two of the six and a ``ValueError`` on the other four -- the same figures - the page carries, and the ones this method derives below rather than trusts. + on those two and a ``ValueError`` on the other helpers. The page names the + helpers rather than counting them (#1811), so each name is checked against + the derived population below. """ from pcapkit.utilities.exceptions import EnumValueError # Derived, not listed: the population is every EnumLookup subclass those two # modules *define* -- re-exports from pcapkit.const.ngap.* are not helpers of - # theirs. An earlier version hard-coded three while its own name said seven - # and the page said five; the real answer is six, so it is measured here and - # asserted, rather than any of the three being trusted. + # theirs. An earlier version hard-coded a tally that disagreed with both its + # own name and the page, so the set is measured here and asserted by name. import inspect from pcapkit.corekit.enum import EnumLookup @@ -1108,21 +1050,23 @@ def test_a_non_string_key_raises_a_value_miss_on_every_helper(self) -> 'None': obj for mod in (mh_mod, ngap_mod) for obj in vars(mod).values() if inspect.isclass(obj) and issubclass(obj, EnumLookup) and obj is not EnumLookup and obj.__module__ == mod.__name__) - self.assertEqual( - len(helpers), 6, - 'the mh/ngap helper population changed, so the page\'s count of them is ' - f'stale: {sorted(k.__name__ for k in helpers)}') self.assertEqual( {k.__name__ for k in helpers}, {'FastBindingAcknowledgmentStatus', 'IPv6AddressPrefixCode', 'LMAAddressCode', 'LocalizedRoutingStatus', 'Criticality', 'PDUKind'}) - # The page's own figure, which measurement alone does not pin. Round 6 found - # this row reading "The 5" against the measured six; the correction landed - # unguarded, so it could have regressed exactly as it arrived. - self.assertIn(f'The {len(helpers)} :mod:', _page('registry-protocol'), - 'the audit table no longer states the measured helper count, ' - 'which is the figure that was already wrong once') + # The page's own row, which measurement alone does not pin. It once stated a + # helper count that disagreed with the measured one; it now names each + # helper, so a helper added without its name on the row fails here. + page = _page('registry-protocol') + head = ' * - The :mod:`~pcapkit.protocols.internet.mh` and' + self.assertIn(head, page, 'the audit table has no mh/ngap helper row') + start = page.index(head) + row = page[start:page.index('\n * - ', start)] + for klass in helpers: + with self.subTest(named=klass.__name__): + self.assertIn(f'``{klass.__name__}``', row, + 'the audit table\'s helper row no longer names this helper') for klass in helpers: for key in (None, 1.5): @@ -1134,7 +1078,7 @@ def test_a_non_string_key_raises_a_value_miss_on_every_helper(self) -> 'None': self.assertIn('``isinstance(key, str)`` and treats everything else as a ' '*value*', flat, "the page no longer records the base's own key dispatch, which " - "is the reason #940's deletion converged the six") + "is the reason #940's deletion converged the helpers") def _optional_dependencies() -> 'dict[str, list[str]]': @@ -1431,14 +1375,14 @@ def test_the_page_names_every_commit_type_the_template_ticks(self) -> 'None': 'label, which the pull request template asks every ' 'contributor to tick') - def test_the_page_pins_its_own_measured_numbers(self) -> 'None': - """Every figure the page states that has local ground truth. + def test_the_page_names_what_the_tree_has(self) -> 'None': + """Every claim the page makes that has local ground truth. A cross-review corrupted **46** claims on these two pages simultaneously and the suite stayed green, which is the honest measure of how much of the prose - was decorative. This closes the subset that has local ground truth: a figure - derived from a file in this repository, a path the page cites, a module it - names. + was decorative. This closes the subset that has local ground truth: a path + the page cites, a module it names. The page states no entry count any more + (#1103's ruling, #1811), so none is pinned. **What stays unpinned, and why, so the gap is stated rather than implied.** Every claim whose ground truth is a GitHub query -- the label count, which @@ -1450,19 +1394,8 @@ def test_the_page_pins_its_own_measured_numbers(self) -> 'None': and no offline test can change that. """ - import inspect import re as _re - changelog = (ROOT / 'docs' / 'source' / 'changelog' / '1.5.0.rst') \ - .read_text(encoding='utf-8') - # ``^\* \*\*Kind\*\*`` before the by-module restructure; the kinds are now - # subsection headings, so an entry is simply a column-zero bullet. - entries = _re.findall(r'^\* ', changelog, _re.MULTILINE) - - self.assertIn(f'{len(entries)} entries', self.flat, - f'the page no longer states the entry count, measured at ' - f'{len(entries)}') - # The module list the page prints as the by-module target. The tree half was # already checked; this is the page half, which a corruption inserting a # non-existent module survived. @@ -1674,9 +1607,10 @@ def test_the_page_describes_the_changelog_grouping_as_it_is(self) -> 'None': What is checked now is the shape that actually shipped: one ``-`` underlined section per top-level module plus one for what belongs to none, each carrying ``~`` underlined kind subsections, with no inline kind label left anywhere. The - section count is required in the page's prose for the same reason the run count - was -- a page that states a measured number has to restate it when the - measurement moves. + page used to state the section count as well, pinned here; it states the shape + instead (#1103's ruling, #1811), so what is checked is its one-to-one claim: + the sections are the packages under :file:`pcapkit/` and *Project + infrastructure*, nothing more and nothing less. """ changelog = (ROOT / 'docs' / 'source' / 'changelog' / '1.5.0.rst') \ @@ -1709,9 +1643,15 @@ def test_the_page_describes_the_changelog_grouping_as_it_is(self) -> 'None': 'an entry carries an inline kind label again, so the file is back to the ' 'shape the page says it left') - self.assertIn(f'**{len(sections)}** module-level sections', self.flat, - 'the page no longer states the measured section count, which is ' - 'the figure a reader would check the restructure against') + packages = {f'pcapkit.{d.name}' for d in (ROOT / 'pcapkit').iterdir() + if d.is_dir() and not d.name.startswith('__')} + self.assertEqual(sorted(sections), sorted(packages | {'Project infrastructure'}), + 'the changelog sections are not one per package plus Project ' + 'infrastructure, which is the map the page says they follow') + for claim in ('one-to-one with the package list', '*Project infrastructure*'): + with self.subTest(claim=claim): + self.assertIn(claim, self.flat, + 'the page no longer states the map this test checks') self.assertNotIn('separate runs', self.flat, 'the page still describes the entries as kind-label runs, ' 'which the restructure removed') diff --git a/tests/project/test_python_matrix_tiers_1633.py b/tests/project/test_python_matrix_tiers_1633.py index 97364e6dc..2f9fed076 100644 --- a/tests/project/test_python_matrix_tiers_1633.py +++ b/tests/project/test_python_matrix_tiers_1633.py @@ -176,10 +176,6 @@ def tier(impl: 'str', name: 'str') -> 'tuple[str, ...]': ('util', ('*.py',)), ) PROSE_FILES = ('pyproject.toml', 'README.md', 'SECURITY.md', 'CONTRIBUTING.md', 'Makefile') -#: Spelled-out counts, as the workflows page writes them. -WORDS = {'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, - 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, - 'eleven': 11, 'twelve': 12, 'thirteen': 13, 'fourteen': 14, 'fifteen': 15} def job(name: 'str', job_id: 'str') -> 'dict[str, Any]': @@ -764,27 +760,28 @@ class TestTheProseFollowsTheTiers(unittest.TestCase): """What the docs and comments say about the matrices, read against the matrices. Nothing executes these, so a tier change leaves them stale without failing - anything else: round one of #1633 left the workflows page counting five - ``test`` legs where there are eight, and six comments naming 3.10 as CI's floor - or 3.15 as allowed to fail. + anything else: round one of #1633 left the workflows page miscounting the + ``test`` legs, and comments naming 3.10 as CI's floor or 3.15 as allowed to fail. """ - def test_the_workflows_page_counts_each_matrix_jobs_legs(self) -> None: + def test_the_workflows_page_names_each_matrix_job_gate_only_skips(self) -> None: + """The ``gate-only`` paragraph's matrix jobs, against ``unit-tests.yml``'s. + + The paragraph counted each job's legs until #1811 (#1103's ruling: a count + drifts, and the tiers moved these). What it still claims is which matrix jobs + a ``gate-only`` caller skips, so a matrix job added, dropped or no longer + gated out fails here. + """ page = prose(ROOT / 'docs' / 'source' / 'contributing' / 'workflows.rst') - engines = {version for _, versions in engine_inputs() for version in versions.split()} - claims = { - r'``test`` \((\w+) Python legs': len(legs('unit-tests.yml', 'test')), - r'``test-ktlo`` \((\w+)\)': len(legs('unit-tests.yml', 'test-ktlo')), - r'``integration`` \((\w+)\)': len(legs('unit-tests.yml', 'integration')), - r'``integration-ktlo`` \((\w+)\)': len(legs('unit-tests.yml', 'integration-ktlo')), - r'``pypcap-parity`` \((\w+)\)': len(legs('unit-tests.yml', 'pypcap-parity')), - r'``pypcap-parity-ktlo`` \((\w+)\)': len(legs('unit-tests.yml', 'pypcap-parity-ktlo')), - r'each looping over (\w+) Python versions': len(engines), - } - for pattern, count in claims.items(): - with self.subTest(claim=pattern): - self.assertEqual([WORDS.get(word) for word in re.findall(pattern, page)], [count]) + listed = re.findall(r'It skips the rest -- the matrix jobs, (.*?), each of which has already run', page) + self.assertEqual(len(listed), 1, 'the page no longer lists the matrix jobs gate-only skips') + named = re.findall(r'``([\w-]+)``', listed[0]) + skipped = [job_id for job_id, spec in workflow('unit-tests.yml')['jobs'].items() + if 'matrix' in (spec.get('strategy') or {}) + and 'inputs.gate-only != true' in str(spec.get('if', ''))] + self.assertTrue(skipped, 'no matrix job of unit-tests.yml is gated out; the check below would be vacuous') + self.assertEqual(sorted(named), sorted(skipped)) def test_no_retired_tier_claim_is_left_in_the_tree(self) -> None: paths = [ROOT / name for name in PROSE_FILES if (ROOT / name).is_file()] diff --git a/tests/project/test_python_versions_1660.py b/tests/project/test_python_versions_1660.py index a4d848f5f..7493e6bd7 100644 --- a/tests/project/test_python_versions_1660.py +++ b/tests/project/test_python_versions_1660.py @@ -27,7 +27,7 @@ script are facts about the engines, not pins, and are not read.) * **The prose follows the file.** The classifiers, README, SECURITY.md, the docs' version lists, the bug-report template, and the comments and docs - naming the coverage leg's version or the release checks' counts. None of + naming the coverage leg's or the sdist leg's version. None of them is a workflow: the weekly job that regenerates them (:file:`util/update_python_versions.py`) cannot push one, so workflow comments name the tiers instead, as @@ -871,9 +871,6 @@ def setUpClass(cls) -> None: cls.latest = data['latest'] cls.supported = script().tier(data, 'cpython', 'supported') cls.ktlo = script().tier(data, 'cpython', 'ktlo') - values = script().outputs(data) - cls.wheels = len(values['pypi-wheels'].split()) - cls.conda = len(json.loads(values['conda'])) def test_the_classifiers_name_the_supported_tier_and_both_implementations(self) -> None: text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8') @@ -926,7 +923,7 @@ def test_the_workflows_page_cites_each_version_on_its_line(self) -> None: with self.subTest(version=version): self.assertTrue(lines[int(line) - 1].startswith(f'"{version}" = '), lines[int(line) - 1]) for label, versions in (('supported tier', self.supported), ('KTLO legs', self.ktlo)): - spans = re.findall(rf'its (?:\w+ )?{label} \(``python-versions\.toml:(\d+)-(\d+)``\)', page) + spans = re.findall(rf'its {label} \(``python-versions\.toml:(\d+)-(\d+)``\)', page) with self.subTest(tier=label): self.assertEqual(len(spans), 1, spans) first, last = map(int, spans[0]) @@ -955,19 +952,6 @@ def test_each_claim_about_the_latest_leg_names_latest(self) -> None: with self.subTest(path=path, claim=pattern): self.assertEqual(re.findall(pattern, prose(ROOT / path)), [self.latest]) - def test_the_release_checks_counts_follow_the_matrices(self) -> None: - """version_check's own counts come from the file; the prose stating them must too.""" - page = prose(ROOT / 'docs' / 'source' / 'contributing' / 'releasing.rst') - claims = ( - (page, r'against the (\d+) expected -- the (\d+) supported legs', - [(str(self.wheels + 1), str(self.wheels))]), - (page, r'against the (\d+) expected \(2 OS x (\d+) Python versions\)', - [(str(2 * self.conda), str(self.conda))]), - ) - for text, pattern, expected in claims: - with self.subTest(claim=pattern): - self.assertEqual(re.findall(pattern, text), expected) - #: The two builds as setup-python spells them, at actions/setup-python 3892123fb: a #: free-threaded CPython as ``3.14t`` (src/find-python.ts:240, diff --git a/tests/project/test_python_versions_update_1660.py b/tests/project/test_python_versions_update_1660.py index bba713280..592691ab3 100644 --- a/tests/project/test_python_versions_update_1660.py +++ b/tests/project/test_python_versions_update_1660.py @@ -276,7 +276,8 @@ def test_the_snapshot_reproduces_the_file_but_for_pypy_312(self) -> None: _, plan, texts = plan_for(self, snapshot()) self.assertEqual(changes(plan), {'pypy3.12': (None, {'tier': 'supported'})}) self.assertEqual(plan['latest'], ['3.15', '3.15']) - self.assertEqual(sorted(texts), ['.github/python-versions.toml', 'docs/source/contributing/releasing.rst']) + # Only the tier file: releasing.rst counted the wheels until #1811, so a new PyPy rewrote it. + self.assertEqual(sorted(texts), ['.github/python-versions.toml']) self.assertEqual(plan['required'], {'remove': [], 'add': []}) self.assertEqual(plan['added'], ['PyPI distribution for Python pypy3.12']) @@ -550,15 +551,17 @@ def test_every_claim_and_rewriter_matches_once_on_main(self) -> None: self.assertEqual(problems, []) self.assertEqual(rewritten, text) - def test_a_count_keeps_its_case_and_a_claim_never_spans_a_break(self) -> None: + def test_a_claim_rewrites_each_group_and_never_spans_a_break(self) -> None: + """#1811 retired the count claims and their spelt-out words; a claim states versions and lines.""" module = updater() - claim = module.Claim(r'(\w+) legs, (3\.\d+)', lambda facts: (module.Word(4), '3.16')) - self.assertEqual(module.apply_claims('Five legs, 3.15.', [claim], {}), ('Four legs, 3.16.', [])) - self.assertEqual(module.apply_claims('# five legs,\n# 3.15', [claim], {}), ('# four legs,\n# 3.16', [])) - broken = module.Claim(r'(legs, 3\.\d+)', lambda facts: ('x',)) - self.assertEqual(module.apply_claims('legs,\n3.15', [broken], {})[1], [ - '`(legs, 3\\.\\d+)` group 1 spans a line break']) - self.assertEqual(module.apply_claims('none', [claim], {})[1], ['`(\\w+) legs, (3\\.\\d+)` matches 0 times']) + claim = module.Claim(r'(3\.\d+) to (3\.\d+)', lambda facts: ('3.12', '3.16')) + self.assertEqual(module.apply_claims('Tests 3.11 to 3.15.', [claim], {}), ('Tests 3.12 to 3.16.', [])) + self.assertEqual(module.apply_claims('# tests 3.11 to\n# 3.15', [claim], {}), ('# tests 3.12 to\n# 3.16', [])) + broken = module.Claim(r'(to 3\.\d+)', lambda facts: ('x',)) + self.assertEqual(module.apply_claims('to\n3.15', [broken], {})[1], [ + '`(to 3\\.\\d+)` group 1 spans a line break']) + self.assertEqual(module.apply_claims('none', [claim], {})[1], ['`(3\\.\\d+) to (3\\.\\d+)` matches 0 times']) + self.assertFalse(hasattr(module, 'Word'), 'a count claim is back; #1103 rules counts out of the prose') def test_no_file_under_workflows_is_written_or_restates_a_tier(self) -> None: """An App token needs ``workflows: write`` to push a workflow change, and ``pr``'s has none. @@ -620,9 +623,16 @@ def test_a_run_that_changes_nothing_writes_nothing(self) -> None: root = self.scratch() plan, texts = updater().compute_plan(root, as_of_the_file(snapshot())) self.assertEqual((plan['changes'], texts, plan['notes']), ([], {}, [])) - page = ' '.join((root / 'docs/source/contributing/workflows.rst').read_text(encoding='utf-8').split()) - self.assertIn('``test`` (five Python legs), ``test-ktlo`` (four)', page) - self.assertIn('``experimental`` (two)', page) + # The leg counts this read back went with #1811; the fact behind them is + # that the hand-kept entries are experimental.yml's legs and no Unit Tests job's. + module = updater() + values = module.pv.outputs(module.validate((root / module.TIER_FILE).read_text(encoding='utf-8'))) + found = {job: {leg.get('python-version') for leg in legs} + for job, legs in module.matrix_legs(module.load_workflows(root), values).items()} + hand_kept = {'3.14t', 'graalpy-25.4'} + self.assertEqual(found[('experimental.yml', 'experimental')], hand_kept) + self.assertEqual({job: versions & hand_kept for job, versions in found.items() + if job[0] == 'unit-tests.yml' and versions & hand_kept}, {}) def test_a_tier_move_keeps_them_byte_for_byte_after_the_computed_versions(self) -> None: root = self.scratch() @@ -672,14 +682,14 @@ def test_no_experimental_leg_is_proposed_as_a_required_context(self) -> None: def run_prose_audit(root: 'pathlib.Path') -> 'unittest.TestResult': - """The prose classes of the audit, reading ``root`` instead of the checkout. + """The audit's prose class, reading ``root`` instead of the checkout. - Imported here, as :mod:`tests.project.test_python_matrix_tiers_1633` reads the - tier file on import, which skips the whole module where :mod:`tomllib` is missing. + The prose the script rewrites is what :class:`TestTheProseFollowsTheFile` reads; + the leg-count check :mod:`tests.project.test_python_matrix_tiers_1633` added to + it went with the counts (#1811). """ # pylint: disable=import-outside-toplevel - from tests.project import test_python_matrix_tiers_1633 as tiers from tests.project import test_python_versions_1660 as versions script = versions.script() @@ -687,13 +697,11 @@ def run_prose_audit(root: 'pathlib.Path') -> 'unittest.TestResult': tier_file = root / '.github' / 'python-versions.toml' suite = unittest.TestSuite() suite.addTests(unittest.TestLoader().loadTestsFromTestCase(versions.TestTheProseFollowsTheFile)) - suite.addTest(tiers.TestTheProseFollowsTheTiers('test_the_workflows_page_counts_each_matrix_jobs_legs')) with contextlib.ExitStack() as stack: stack.enter_context(mock.patch.object(versions, 'ROOT', root)) stack.enter_context(mock.patch.object(versions, 'WORKFLOWS', root / '.github' / 'workflows')) stack.enter_context(mock.patch.object(versions, 'TIER_FILE', tier_file)) stack.enter_context(mock.patch.object(script, 'load', lambda path=tier_file: original(path))) - stack.enter_context(mock.patch.object(tiers, 'ROOT', root)) return unittest.TextTestRunner(stream=io.StringIO(), verbosity=0).run(suite) diff --git a/tests/project/test_workflows_at_a_glance_1732.py b/tests/project/test_workflows_at_a_glance_1732.py index 17df551fa..fafb0783d 100644 --- a/tests/project/test_workflows_at_a_glance_1732.py +++ b/tests/project/test_workflows_at_a_glance_1732.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- -"""The workflows page's count, "At a Glance" table and graph, against the files. +"""The workflows page's "At a Glance" table and graph, against the files. -:file:`docs/source/contributing/workflows.rst` opens by counting the workflows under -:file:`.github/workflows/`, tabulates each one's triggers, and draws them as a -graph. #1732 added :file:`experimental.yml` and left all three describing twelve, -so each is checked here against the ``on:`` block of every file: the count, one -table row per file with its name and triggers, and one graph node per file with -an edge for each ``push``, ``pull_request`` and ``workflow_run`` trigger. +:file:`docs/source/contributing/workflows.rst` tabulates the triggers of each +workflow under :file:`.github/workflows/` and draws them as a graph. #1732 added +:file:`experimental.yml` and left both without it, so each is checked here against +the ``on:`` block of every file: one table row per file with its name and +triggers, and one graph node per file with an edge for each ``push``, +``pull_request`` and ``workflow_run`` trigger. The page states no count of the +files (#1103's ruling, #1811); the row and node per file are what keep it whole. """ @@ -24,8 +25,6 @@ WORKFLOWS = ROOT / '.github' / 'workflows' DOC = ROOT / 'docs' / 'source' / 'contributing' / 'workflows.rst' -NUMBERS = {'ten': 10, 'eleven': 11, 'twelve': 12, 'thirteen': 13, 'fourteen': 14, - 'fifteen': 15, 'sixteen': 16} DAYS = {'0': 'Sun', '1': 'Mon', '2': 'Tue', '3': 'Wed', '4': 'Thu', '5': 'Fri', '6': 'Sat', '*': 'daily'} #: Events the table gives a column of their own; the rest go under "other". @@ -113,10 +112,6 @@ def graph(page: 'str') -> 'tuple[dict[str, str], set[str]]': def problems(page: 'str', files: 'dict[pathlib.Path, str]') -> 'list[str]': """Every way the page disagrees with the workflow files.""" found = [] # type: list[str] - count = re.search(r'The (\w+) workflows under :file:`\.github/workflows/`', ' '.join(page.split())) - if count is None or NUMBERS.get(count.group(1)) != len(files): - found.append(f'the note counts {count and count.group(1)!r} workflows; there are {len(files)}') - rows = {row[1]: row for row in table_rows(page)} nodes, edges = graph(page) by_name = {workflow_name(text): nodes.get(path.name) for path, text in files.items()} @@ -158,15 +153,17 @@ def test_the_page_describes_every_workflow_file(self) -> None: self.assertEqual(problems(DOC.read_text(encoding='utf-8'), FILES), []) def test_a_missing_row_is_caught(self) -> None: - """The check bites: drop experimental.yml's row, node and count, and it fails.""" + """The check bites: drop experimental.yml's row, or its node, and it fails.""" page = DOC.read_text(encoding='utf-8') row = re.search(r'(?m)^ \* - Experimental Tests\n(?: - .*\n)+', page) self.assertIsNotNone(row) assert row is not None found = problems(page.replace(row.group(0), ''), FILES) self.assertEqual(found, ['no table row for experimental.yml']) - found = problems(page.replace('The thirteen workflows', 'The twelve workflows'), FILES) - self.assertEqual(found, ["the note counts 'twelve' workflows; there are 13"]) + node = re.search(r'(?m)^ +\w+\["[^"<]+
experimental\.yml"\]\n', page) + self.assertIsNotNone(node) + assert node is not None + self.assertEqual(problems(page.replace(node.group(0), ''), FILES), ['no graph node for experimental.yml']) if __name__ == '__main__': diff --git a/util/update_python_versions.py b/util/update_python_versions.py index 322118db2..774ab4fa0 100644 --- a/util/update_python_versions.py +++ b/util/update_python_versions.py @@ -836,12 +836,6 @@ def fingerprint(latest: 'str', versions: 'dict[str, Any]') -> 'str': #: A line break, and a comment marker after it, as the audit tests' ``prose()`` reads them. SEPARATOR = re.compile(r'[ \t]*\n[ \t]*(?:#:?[ \t]*)?') -WORDS = ('zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', - 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty') - - -class Word(int): - """A count written as a word, in the case of the word it replaces.""" def prose_view(text: 'str') -> 'tuple[str, list[int]]': @@ -859,15 +853,6 @@ def prose_view(text: 'str') -> 'tuple[str, list[int]]': return ''.join(pieces), origin -def spell(value: 'Any', old: 'str') -> 'str': - if isinstance(value, Word): - if not 0 <= value < len(WORDS): - raise ValueError(f'no word for {int(value)}') - word = WORDS[value] - return word.capitalize() if old[:1].isupper() else word - return str(value) - - class Claim(NamedTuple): """A sentence restating the tiers: its pattern on the prose view, and each group's value.""" @@ -895,7 +880,7 @@ def apply_claims(text: 'str', claims: 'Iterable[Claim]', facts: 'dict[str, Any]' problems.append(f'`{claim.pattern}` group {group} spans a line break') break first, last = offsets[0], offsets[0] + len(offsets) - edits.append((first, last, spell(value, text[first:last]))) + edits.append((first, last, str(value))) for start, end, value in sorted(edits, reverse=True): text = text[:start] + value + text[end:] return text, problems @@ -921,11 +906,6 @@ def lines_of(facts: 'dict[str, Any]', tier: 'str') -> 'Optional[tuple[int, int]] return (numbers[0], numbers[-1]) if consecutive(numbers) else None -def counted(count: 'int', ends: 'Optional[tuple[Any, Any]]') -> 'Optional[tuple[Any, ...]]': - """``count`` as a word, then ``ends``; :data:`None` when ``ends`` is.""" - return None if ends is None else (Word(count),) + ends - - #: The sentences restating the tiers, by file. Never a workflow (see the module docstring). CLAIMS = { 'pyproject.toml': ( @@ -949,22 +929,10 @@ def counted(count: 'int', ends: 'Optional[tuple[Any, Any]]') -> 'Optional[tuple[ 'docs/source/contributing/workflows.rst': ( Claim(r'The \*\*supported\*\* tier, (3\.\d+)-(3\.\d+),', lambda f: span(f['supported'])), Claim(r'end-of-life (3\.\d+)-(3\.\d+), still built', lambda f: span(f['ktlo'])), - Claim(r'requires (\w+) contexts', lambda f: (Word(len(f['required'])),)), - Claim(r'the (\w+) required legs are its supported tier \(``python-versions\.toml:(\d+)-(\d+)``\)', - lambda f: counted(len(f['required']) - 1, lines_of(f, 'supported'))), - Claim(r'its (\w+) KTLO legs \(``python-versions\.toml:(\d+)-(\d+)``\)', - lambda f: counted(len(f['ktlo']), lines_of(f, 'ktlo'))), - Claim(r'and the (\w+) ``Compat Python (3\.\d+)``-``(3\.\d+)`` legs', - lambda f: counted(len(f['required']) - 1, span(f['supported']))), - # #1738: each gating job's legs, then its KTLO twin's. - Claim(r'``test`` \((\w+) Python legs\)', lambda f: (Word(f['legs']['test']),)), - Claim(r'``test-ktlo`` \((\w+)\)', lambda f: (Word(f['legs']['test-ktlo']),)), - Claim(r'``experimental`` \((\w+)\)', lambda f: (Word(f['legs']['experimental']),)), - Claim(r'``integration`` \((\w+)\)', lambda f: (Word(f['legs']['integration']),)), - Claim(r'``integration-ktlo`` \((\w+)\)', lambda f: (Word(f['legs']['integration-ktlo']),)), - Claim(r'each looping over (\w+) Python versions', lambda f: (Word(f['engines']),)), - Claim(r'``pypcap-parity`` \((\w+)\)', lambda f: (Word(f['legs']['pypcap-parity']),)), - Claim(r'``pypcap-parity-ktlo`` \((\w+)\)', lambda f: (Word(f['legs']['pypcap-parity-ktlo']),)), + Claim(r'the required legs are its supported tier \(``python-versions\.toml:(\d+)-(\d+)``\)', + lambda f: lines_of(f, 'supported')), + Claim(r'its KTLO legs \(``python-versions\.toml:(\d+)-(\d+)``\)', lambda f: lines_of(f, 'ktlo')), + Claim(r'and the ``Compat Python (3\.\d+)``-``(3\.\d+)`` legs', lambda f: span(f['supported'])), Claim(r'the ``Python (3\.\d+)`` leg of ``test`` \(matrix key ``coverage: true``\)', lambda f: (f['latest'],)), ), @@ -980,9 +948,6 @@ def counted(count: 'int', ends: 'Optional[tuple[Any, Any]]') -> 'Optional[tuple[ ), 'docs/source/contributing/releasing.rst': ( Claim(r'the sdist kept only on the (\d+\.\d+) leg', lambda f: (f['latest'],)), - Claim(r'against the (\d+) expected -- the (\d+) supported legs', lambda f: (f['wheels'] + 1, f['wheels'])), - Claim(r'against the (\d+) expected \(2 OS x (\d+) Python versions\)', - lambda f: (2 * f['conda'], f['conda'])), ), } # type: dict[str, tuple[Claim, ...]] @@ -1046,13 +1011,6 @@ def rewrite_bug_template(text: 'str', facts: 'dict[str, Any]') -> 'tuple[str, li def make_facts(data: 'dict[str, Any]', values: 'dict[str, str]', workflows: 'dict[str, Any]', tier_file: 'str') -> 'dict[str, Any]': """What the prose states, computed from the new tiers and the workflows' matrices.""" - legs = matrix_legs(workflows, values) - engine = workflows['unit-tests.yml']['jobs']['engine-tests'] - engines = set() # type: set[str] - for step in setup_steps(engine): - found = resolve(workflows, 'unit-tests.yml', 'engine-tests', step['with']['python-version'], values) - if found is not NOTHING: - engines.update(str(found).split()) tier_lines = {} for number, line in enumerate(tier_file.splitlines(), 1): match = ENTRY.match(line) @@ -1063,12 +1021,6 @@ def make_facts(data: 'dict[str, Any]', values: 'dict[str, str]', workflows: 'dic 'supported': pv.tier(data, 'cpython', 'supported'), 'ktlo': pv.tier(data, 'cpython', 'ktlo'), 'experimental': [version for impl in data['versions'] for version in pv.tier(data, impl, 'experimental')], - 'wheels': len(values['pypi-wheels'].split()), - 'conda': len(json.loads(values['conda'])), - # #1732: experimental.yml's one job, which unit-tests.yml's prose counts with its own. - 'legs': {job_id: len(found) for (name, job_id), found in legs.items() - if name in ('unit-tests.yml', 'experimental.yml')}, - 'engines': len(engines), 'required': required_contexts(workflows, values), 'tier_lines': tier_lines, }