From 9b6b2514b50e384df2df2dfa1efaddcf7bdb9b63 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 19 Oct 2026 10:26:55 -0400 Subject: [PATCH 1/2] docs(const): document the get overrides instead of hiding them (#1921) - Drop `:meta private:` from the `get` overrides of `Command`, `Method`, `OptionType`, `TransportProtocol` and `AppType`, in the vendor templates and the const modules regenerated from them, so `:meth:`get`` links again - Replace each "Backport support for original codes." summary with the lookup it performs, and state what `get` returns on a miss - Give `OptionType.get` a contract: namespace overlay for codes, case-sensitive names, and what `default` is for - Add a project test that no override, template or `:meth:` reference to one is marked private Closes #1921 --- pcapkit/const/ftp/command.py | 9 +- pcapkit/const/http/method.py | 13 +- pcapkit/const/pcapng/option_type.py | 23 +- pcapkit/const/reg/apptype/apptype.py | 11 +- pcapkit/vendor/ftp/command.py | 9 +- pcapkit/vendor/http/method.py | 13 +- pcapkit/vendor/pcapng/option_type.py | 23 +- pcapkit/vendor/reg/apptype/apptype.py | 11 +- ...onst_get_overrides_documented_1921_unit.py | 210 ++++++++++++++++++ 9 files changed, 290 insertions(+), 32 deletions(-) create mode 100644 tests/project/test_const_get_overrides_documented_1921_unit.py diff --git a/pcapkit/const/ftp/command.py b/pcapkit/const/ftp/command.py index b4328e6be..5337de3b9 100644 --- a/pcapkit/const/ftp/command.py +++ b/pcapkit/const/ftp/command.py @@ -559,7 +559,7 @@ def register_alias(cls, value: 'str', name: 'str') -> 'Command': @classmethod def get(cls, key: 'str', default: 'Optional[str]' = None) -> 'Command': - """Backport support for original codes. + """Resolve ``key`` to a member, matching the command code case-insensitively. A non-:class:`str` ``key`` is delegated to :meth:`~pcapkit.corekit.enum.EnumLookup.get`, as @@ -584,6 +584,12 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> 'Command': matches no member name. ``None``, the default, uses ``key`` itself. + Returns: + The member named ``key`` upper-cased. On a miss, an unregistered + member with that name and the value ``default`` (or ``key``), + which is not added to the lookup tables, so ``get`` never registers + a command (:issue:`860`). + Raises: EnumValueError: If ``key`` is not a :class:`str`. This reaches the caller from the base's non-``str`` branch, where @@ -591,7 +597,6 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> 'Command': reach :meth:`_missing_`. A :exc:`ValueError`, so an ``except ValueError`` caller is unaffected. - :meta private: """ if not isinstance(key, str): # NOTE: unreachable only by the ``str`` annotation, which a caller diff --git a/pcapkit/const/http/method.py b/pcapkit/const/http/method.py index e6add5407..3a7597cdc 100644 --- a/pcapkit/const/http/method.py +++ b/pcapkit/const/http/method.py @@ -210,7 +210,7 @@ def _unregistered_member(cls, value: 'str', name: 'str') -> 'Method': @classmethod def get(cls, key: 'str', default: 'Optional[str]' = None) -> 'Method': - """Backport support for original codes. + """Resolve ``key`` to a member, matching the method token case-sensitively. Delegates to :meth:`~pcapkit.corekit.enum.EnumLookup.get` for the lookup itself, so ``key`` is checked against member values as well @@ -246,18 +246,23 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> 'Method': matches neither a name nor a value. ``None``, the default, uses ``key`` itself. + Returns: + The member whose name or value is ``key``. On a miss, an + unregistered member named ``key`` upper-cased, with the value + ``default`` (or ``key``), which is not added to the lookup tables, + so ``get`` never registers a method (:issue:`860`). + Raises: ValueError: If ``key`` is not a :class:`str`. This reaches the caller from the base's non-``str`` branch, where :meth:`_validate_value` refuses it before ``cls(key)`` could reach ``_missing_``, and is **not** caught by - the ``except KeyError`` fallback below -- only a failed - *name* lookup is. Raising is the intended behaviour for + the miss fallback described under *Returns* -- only a failed + lookup is. Raising is the intended behaviour for ``get(b'GET')`` as well: a member whose name and value were both the :class:`bytes` object is not a thing this registry should hand back. - :meta private: """ try: return super().get(key) diff --git a/pcapkit/const/pcapng/option_type.py b/pcapkit/const/pcapng/option_type.py index ddc2db43b..caae049e8 100644 --- a/pcapkit/const/pcapng/option_type.py +++ b/pcapkit/const/pcapng/option_type.py @@ -335,14 +335,27 @@ def register_alias(cls, value: 'int', name: 'str') -> 'OptionType': @staticmethod def get(key: 'int | str', default: 'int' = -1, *, namespace: 'str' = 'opt') -> 'OptionType': - """Backport support for original codes. + """Resolve ``key`` to a member, by option code or by name. + + An :class:`int` ``key`` is an option code, looked up in ``namespace`` + overlaid on ``opt``, so a code the namespace assigns itself wins over + the generic one. A :class:`str` ``key`` is matched case-sensitively + against member names, then against member values. Args: - key: Key to get enum item. - default: Default value if not found. - namespace: Namespace of the enum item. + key: Option code, member name or member value to look up. + default: Option code of the unregistered member built when a + :class:`str` ``key`` matches nothing. Unused for an + :class:`int` ``key``. + namespace: Namespace to resolve an :class:`int` ``key`` in. + + Returns: + The matching member. On a miss, an unregistered member, which is + not added to the lookup tables, so ``get`` never registers an + option (:issue:`860`). For an option code, in range or not, it is + named ``{namespace}_unknown``; for a string, it is named ``key`` + and carries the option code ``default``. - :meta private: """ if isinstance(key, int): temp_ns = OptionType.__members_ns__.get('opt', {}).copy() diff --git a/pcapkit/const/reg/apptype/apptype.py b/pcapkit/const/reg/apptype/apptype.py index d8a447c57..e3d864f6a 100644 --- a/pcapkit/const/reg/apptype/apptype.py +++ b/pcapkit/const/reg/apptype/apptype.py @@ -88,7 +88,7 @@ class TransportProtocol(EnumLookup, IntEnum): @classmethod def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtocol': - """Backport support for original codes. + """Resolve ``key`` to a member, matching a name case-insensitively. Delegates to :meth:`~pcapkit.corekit.enum.EnumLookup.get` for GitHub issue :issue:`877`'s re-parenting, but keeps this override rather than dropping @@ -161,7 +161,6 @@ def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtoco EnumValueError: If ``key`` is a value no member carries and there is no usable ``default``. A :exc:`ValueError`, from the base. - :meta private: """ if isinstance(key, str): return super().get(key.lower(), default) @@ -2607,7 +2606,7 @@ def _dispatch(cls, key: 'int', proto: 'TransportProtocol | str | int') -> 'Type[ @classmethod def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=arguments-differ proto: 'TransportProtocol | str | int' = TransportProtocol.undefined) -> 'AppType': - """Backport support for original codes. + """Resolve port ``key`` to the member for its canonical service. Args: key: Port number to look up. @@ -2628,7 +2627,10 @@ def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=argument to some ports -- ``80`` carries ``http``, ``www`` and ``www-http`` -- and names no precedence among them, so :data:`__canonical__` supplies the one the rest of the world answers with. The others remain real, - named members and are reached through :meth:`get_all`. + named members and are reached through :meth:`get_all`. A port + with no row resolves to an unregistered member that is not added + to the registry (:issue:`860`), named after the IANA range holding + the port, or ``unknown`` outside every range. Raises: ValueError: If called on a class that holds no members, i.e. on @@ -2645,7 +2647,6 @@ def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=argument them, not simply for being composite -- see :meth:`_dispatch`. - :meta private: """ owner = cls._dispatch(key, proto) diff --git a/pcapkit/vendor/ftp/command.py b/pcapkit/vendor/ftp/command.py index 745ccdc78..ad2521d52 100644 --- a/pcapkit/vendor/ftp/command.py +++ b/pcapkit/vendor/ftp/command.py @@ -393,7 +393,7 @@ def register_alias(cls, value: 'str', name: 'str') -> '{NAME}': @classmethod def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}': - """Backport support for original codes. + """Resolve ``key`` to a member, matching the command code case-insensitively. A non-:class:`str` ``key`` is delegated to :meth:`~pcapkit.corekit.enum.EnumLookup.get`, as @@ -418,6 +418,12 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}': matches no member name. ``None``, the default, uses ``key`` itself. + Returns: + The member named ``key`` upper-cased. On a miss, an unregistered + member with that name and the value ``default`` (or ``key``), + which is not added to the lookup tables, so ``get`` never registers + a command (:issue:`860`). + Raises: EnumValueError: If ``key`` is not a :class:`str`. This reaches the caller from the base's non-``str`` branch, where @@ -425,7 +431,6 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}': reach :meth:`_missing_`. A :exc:`ValueError`, so an ``except ValueError`` caller is unaffected. - :meta private: """ if not isinstance(key, str): # NOTE: unreachable only by the ``str`` annotation, which a caller diff --git a/pcapkit/vendor/http/method.py b/pcapkit/vendor/http/method.py index 5d66030f2..03cb433ad 100644 --- a/pcapkit/vendor/http/method.py +++ b/pcapkit/vendor/http/method.py @@ -115,7 +115,7 @@ def _unregistered_member(cls, value: 'str', name: 'str') -> '{NAME}': @classmethod def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}': - """Backport support for original codes. + """Resolve ``key`` to a member, matching the method token case-sensitively. Delegates to :meth:`~pcapkit.corekit.enum.EnumLookup.get` for the lookup itself, so ``key`` is checked against member values as well @@ -151,18 +151,23 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}': matches neither a name nor a value. ``None``, the default, uses ``key`` itself. + Returns: + The member whose name or value is ``key``. On a miss, an + unregistered member named ``key`` upper-cased, with the value + ``default`` (or ``key``), which is not added to the lookup tables, + so ``get`` never registers a method (:issue:`860`). + Raises: ValueError: If ``key`` is not a :class:`str`. This reaches the caller from the base's non-``str`` branch, where :meth:`_validate_value` refuses it before ``cls(key)`` could reach ``_missing_``, and is **not** caught by - the ``except KeyError`` fallback below -- only a failed - *name* lookup is. Raising is the intended behaviour for + the miss fallback described under *Returns* -- only a failed + lookup is. Raising is the intended behaviour for ``get(b'GET')`` as well: a member whose name and value were both the :class:`bytes` object is not a thing this registry should hand back. - :meta private: """ try: return super().get(key) diff --git a/pcapkit/vendor/pcapng/option_type.py b/pcapkit/vendor/pcapng/option_type.py index cf8af1945..948b88c8c 100644 --- a/pcapkit/vendor/pcapng/option_type.py +++ b/pcapkit/vendor/pcapng/option_type.py @@ -245,14 +245,27 @@ def register_alias(cls, value: 'int', name: 'str') -> '{NAME}': @staticmethod def get(key: 'int | str', default: 'int' = -1, *, namespace: 'str' = 'opt') -> '{NAME}': - """Backport support for original codes. + """Resolve ``key`` to a member, by option code or by name. + + An :class:`int` ``key`` is an option code, looked up in ``namespace`` + overlaid on ``opt``, so a code the namespace assigns itself wins over + the generic one. A :class:`str` ``key`` is matched case-sensitively + against member names, then against member values. Args: - key: Key to get enum item. - default: Default value if not found. - namespace: Namespace of the enum item. + key: Option code, member name or member value to look up. + default: Option code of the unregistered member built when a + :class:`str` ``key`` matches nothing. Unused for an + :class:`int` ``key``. + namespace: Namespace to resolve an :class:`int` ``key`` in. + + Returns: + The matching member. On a miss, an unregistered member, which is + not added to the lookup tables, so ``get`` never registers an + option (:issue:`860`). For an option code, in range or not, it is + named ``{{namespace}}_unknown``; for a string, it is named ``key`` + and carries the option code ``default``. - :meta private: """ if isinstance(key, int): temp_ns = {NAME}.__members_ns__.get('opt', {{}}).copy() diff --git a/pcapkit/vendor/reg/apptype/apptype.py b/pcapkit/vendor/reg/apptype/apptype.py index 248f456bd..9e2843b8e 100644 --- a/pcapkit/vendor/reg/apptype/apptype.py +++ b/pcapkit/vendor/reg/apptype/apptype.py @@ -180,7 +180,7 @@ class TransportProtocol(EnumLookup, IntEnum): @classmethod def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtocol': - """Backport support for original codes. + """Resolve ``key`` to a member, matching a name case-insensitively. Delegates to :meth:`~pcapkit.corekit.enum.EnumLookup.get` for GitHub issue :issue:`877`'s re-parenting, but keeps this override rather than dropping @@ -253,7 +253,6 @@ def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtoco EnumValueError: If ``key`` is a value no member carries and there is no usable ``default``. A :exc:`ValueError`, from the base. - :meta private: """ if isinstance(key, str): return super().get(key.lower(), default) @@ -561,7 +560,7 @@ def _dispatch(cls, key: 'int', proto: 'TransportProtocol | str | int') -> 'Type[ @classmethod def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=arguments-differ proto: 'TransportProtocol | str | int' = TransportProtocol.undefined) -> '{NAME}': - """Backport support for original codes. + """Resolve port ``key`` to the member for its canonical service. Args: key: Port number to look up. @@ -582,7 +581,10 @@ def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=argument to some ports -- ``80`` carries ``http``, ``www`` and ``www-http`` -- and names no precedence among them, so :data:`__canonical__` supplies the one the rest of the world answers with. The others remain real, - named members and are reached through :meth:`get_all`. + named members and are reached through :meth:`get_all`. A port + with no row resolves to an unregistered member that is not added + to the registry (:issue:`860`), named after the IANA range holding + the port, or ``unknown`` outside every range. Raises: ValueError: If called on a class that holds no members, i.e. on @@ -599,7 +601,6 @@ def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=argument them, not simply for being composite -- see :meth:`_dispatch`. - :meta private: """ owner = cls._dispatch(key, proto) diff --git a/tests/project/test_const_get_overrides_documented_1921_unit.py b/tests/project/test_const_get_overrides_documented_1921_unit.py new file mode 100644 index 000000000..933fd4bf3 --- /dev/null +++ b/tests/project/test_const_get_overrides_documented_1921_unit.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +"""Pins the registry ``get`` overrides as documented API targets. + +GitHub issue #1921: ``Command.get``, ``Method.get``, ``OptionType.get``, +``TransportProtocol.get`` and ``AppType.get`` carried ``:meta private:``, so +autodoc left them off their pages and every ``:meth:`get``` naming one rendered +as an unlinked ``get()`` chip. The owner ruled that the overrides are documented +instead, since they carry contract the base +:meth:`~pcapkit.corekit.enum.EnumLookup.get` does not describe. + +Whether a reference resolves is a property of the built inventory, which the +pull request checks in the rendered HTML. What this pins is the source-side +precondition: no override carries the tag, in the generated module or in the +vendor template that regenerates it, and no ``:meth:`` reference to one lands +on a member marked private. + +""" + +from __future__ import annotations + +import importlib +import inspect +import pathlib +import re +import unittest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Iterator, Optional + +ROOT = pathlib.Path(__file__).resolve().parents[2] + +#: The directive that hides a member from autodoc. +PRIVATE = ':meta private:' + +#: Each override, as ``(const module, class, vendor template)``. +OVERRIDES = ( + ('pcapkit.const.ftp.command', 'Command', 'pcapkit/vendor/ftp/command.py'), + ('pcapkit.const.http.method', 'Method', 'pcapkit/vendor/http/method.py'), + ('pcapkit.const.pcapng.option_type', 'OptionType', 'pcapkit/vendor/pcapng/option_type.py'), + ('pcapkit.const.reg.apptype.apptype', 'TransportProtocol', + 'pcapkit/vendor/reg/apptype/apptype.py'), + ('pcapkit.const.reg.apptype.apptype', 'AppType', 'pcapkit/vendor/reg/apptype/apptype.py'), +) + +#: Modules whose docstrings name an override relatively, as ``:meth:`get```. +RELATIVE_MODULES = ( + 'pcapkit.const.ftp.command', + 'pcapkit.const.http.method', + 'pcapkit.const.pcapng.option_type', + 'pcapkit.const.reg.apptype.apptype', + 'pcapkit.const.reg.apptype.tcp', + 'pcapkit.const.reg.apptype.udp', + 'pcapkit.const.reg.apptype.sctp', + 'pcapkit.const.reg.apptype.dccp', +) + +#: A ``:meth:`` role, with or without a title, capturing its target. +METH = re.compile(r':meth:`(?:[^`<]*<)?[~.]*(?P[\w.]+)>?`') + + +def raw_get(cls: 'type') -> 'tuple[type, Any]': + """The class that defines ``get`` for ``cls``, and its undecorated function.""" + for owner in cls.__mro__: + if 'get' in vars(owner): + attr = vars(owner)['get'] + return owner, getattr(attr, '__func__', attr) + raise AssertionError(f'{cls!r} has no get') + + +def docstrings(cls: 'type') -> 'Iterator[tuple[str, str]]': + """Yield ``(qualified name, docstring)`` for ``cls`` and the members it defines.""" + yield cls.__qualname__, inspect.cleandoc(cls.__doc__ or '') + for name, attr in vars(cls).items(): + func = getattr(attr, '__func__', getattr(attr, 'fget', attr)) + doc = getattr(func, '__doc__', None) + if isinstance(doc, str) and callable(func): + yield f'{cls.__qualname__}.{name}', doc + + +def resolve(target: 'str', module: 'Any', cls: 'Optional[type]') -> 'Optional[type]': + """The class whose ``get`` a ``:meth:`` ``target`` ending in ``get`` names.""" + *path, last = target.split('.') + if last != 'get': + return None + obj: 'Any' + if path[:1] == ['pcapkit']: + for cut in range(len(path), 0, -1): + try: + obj = importlib.import_module('.'.join(path[:cut])) + except ImportError: + continue + for part in path[cut:]: + obj = getattr(obj, part, None) + return obj if isinstance(obj, type) else None + return None + scopes = [cls, module] if cls is not None else [module] + for scope in scopes: + obj = scope + try: + for part in path: + obj = getattr(obj, part) + except AttributeError: + continue + if isinstance(obj, type): + return obj + return None + + +def template_get_docstrings(path: 'pathlib.Path') -> 'list[tuple[int, str]]': + """The ``def get`` docstrings in a vendor template, with their line numbers. + + A template is an f-string inside a lambda, so the class body it renders is + text rather than code: each ``def get(`` is found by its line and its + docstring read up to the closing triple quote. + + """ + lines = path.read_text(encoding='utf-8').splitlines() + found = [] + for index, line in enumerate(lines): + if not re.match(r'\s+def get\(', line): + continue + start = next(i for i in range(index, len(lines)) if '"""' in lines[i]) + end = next(i for i in range(start + 1, len(lines)) if '"""' in lines[i]) + found.append((start + 1, '\n'.join(lines[start:end + 1]))) + return found + + +class OverrideDocstringTests(unittest.TestCase): + """No ``get`` override is hidden from autodoc.""" + + def test_const_overrides_are_not_private(self) -> 'None': + """The generated modules' ``get`` overrides carry no ``:meta private:``.""" + for modname, clsname, _ in OVERRIDES: + with self.subTest(override=f'{modname}.{clsname}.get'): + cls = getattr(importlib.import_module(modname), clsname) + owner, func = raw_get(cls) + self.assertIs(owner, cls, f'{clsname} no longer overrides get') + self.assertTrue(func.__doc__, f'{clsname}.get has no docstring') + self.assertFalse(PRIVATE in func.__doc__, f'{clsname}.get is marked private') + + def test_templates_are_not_private(self) -> 'None': + """The vendor templates' ``get`` docstrings carry no ``:meta private:``. + + A tag left in a template comes back the next time the scripts + regenerate the const module, so the template is checked separately. + + """ + for template in sorted({path for _, _, path in OVERRIDES}): + found = template_get_docstrings(ROOT / template) + with self.subTest(template=template): + self.assertTrue(found, f'{template} has no def get') + for line, doc in found: + self.assertFalse(PRIVATE in doc, f'{template}:{line}: get is marked private') + + +class ReferenceTargetTests(unittest.TestCase): + """Every ``:meth:`` reference to a ``get`` override lands on a documented member.""" + + def check(self, where: 'str', target: 'str', cls: 'Optional[type]') -> 'bool': + """Whether ``cls`` names an override; if so, assert it is not private. + + Called inside a ``subTest``, so the answer is reported whether or not + the assertion fails. + + """ + if cls is None: + return False + owner, func = raw_get(cls) + if not owner.__module__.startswith('pcapkit.const.'): + return False + with self.subTest(where=where, target=target): + self.assertFalse(PRIVATE in (func.__doc__ or ''), + f'{where}: :meth:`{target}` targets {owner.__qualname__}.get, ' + 'which is marked private') + return True + + def test_relative_references(self) -> 'None': + """``:meth:`get``` in a registry docstring targets a documented override.""" + hits = 0 + for modname in RELATIVE_MODULES: + module = importlib.import_module(modname) + for cls in vars(module).values(): + if not (isinstance(cls, type) and cls.__module__ == modname): + continue + for where, doc in docstrings(cls): + for match in METH.finditer(doc): + target = match.group('target') + hits += self.check(f'{modname}.{where}', target, + resolve(target, module, cls)) + self.assertTrue(hits, 'no relative reference to a get override was found') + + def test_absolute_references(self) -> 'None': + """A fully qualified ``:meth:`` to an override, in code or docs, is documented.""" + paths = sorted((ROOT / 'pcapkit').rglob('*.py')) + sorted((ROOT / 'docs').rglob('*.rst')) + hits = 0 + for path in paths: + text = path.read_text(encoding='utf-8') + for match in METH.finditer(text): + target = match.group('target') + if not target.startswith('pcapkit.const.') or not target.endswith('.get'): + continue + line = text.count('\n', 0, match.start()) + 1 + hits += self.check(f'{path.relative_to(ROOT)}:{line}', target, + resolve(target, None, None)) + self.assertTrue(hits, 'no fully qualified reference to a get override was found') + + +if __name__ == '__main__': + unittest.main() From b20a6a62c800f580d2e1cb77eab933c1492568ed Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 19 Oct 2026 10:46:44 -0400 Subject: [PATCH 2/2] docs(const): state the AppType miss label exactly (#1921) - Say that `AppType.get` names a miss after the label of the IANA span holding the port, whatever that label is, or `unknown` when no span holds it, in the template and the regenerated const module - State the #1921 test's prose as what it checks, without timed context - Pin known references among the `:meth:` hits, so a reference that stops resolving fails the test Closes #1921 --- pcapkit/const/reg/apptype/apptype.py | 8 +-- pcapkit/vendor/reg/apptype/apptype.py | 8 +-- ...onst_get_overrides_documented_1921_unit.py | 53 +++++++++++++------ 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/pcapkit/const/reg/apptype/apptype.py b/pcapkit/const/reg/apptype/apptype.py index e3d864f6a..984732e67 100644 --- a/pcapkit/const/reg/apptype/apptype.py +++ b/pcapkit/const/reg/apptype/apptype.py @@ -2628,9 +2628,11 @@ def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=argument and names no precedence among them, so :data:`__canonical__` supplies the one the rest of the world answers with. The others remain real, named members and are reached through :meth:`get_all`. A port - with no row resolves to an unregistered member that is not added - to the registry (:issue:`860`), named after the IANA range holding - the port, or ``unknown`` outside every range. + with no row of its own resolves to an unregistered member that is + not added to the registry (:issue:`860`). Its name is the label of + the IANA span holding the port, whatever that label is -- + ``reserved``, ``unassigned``, or a service name such as ``swx`` -- + or ``unknown`` when no span holds it. Raises: ValueError: If called on a class that holds no members, i.e. on diff --git a/pcapkit/vendor/reg/apptype/apptype.py b/pcapkit/vendor/reg/apptype/apptype.py index 9e2843b8e..095ab5fad 100644 --- a/pcapkit/vendor/reg/apptype/apptype.py +++ b/pcapkit/vendor/reg/apptype/apptype.py @@ -582,9 +582,11 @@ def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=argument and names no precedence among them, so :data:`__canonical__` supplies the one the rest of the world answers with. The others remain real, named members and are reached through :meth:`get_all`. A port - with no row resolves to an unregistered member that is not added - to the registry (:issue:`860`), named after the IANA range holding - the port, or ``unknown`` outside every range. + with no row of its own resolves to an unregistered member that is + not added to the registry (:issue:`860`). Its name is the label of + the IANA span holding the port, whatever that label is -- + ``reserved``, ``unassigned``, or a service name such as ``swx`` -- + or ``unknown`` when no span holds it. Raises: ValueError: If called on a class that holds no members, i.e. on diff --git a/tests/project/test_const_get_overrides_documented_1921_unit.py b/tests/project/test_const_get_overrides_documented_1921_unit.py index 933fd4bf3..e24f59e08 100644 --- a/tests/project/test_const_get_overrides_documented_1921_unit.py +++ b/tests/project/test_const_get_overrides_documented_1921_unit.py @@ -2,11 +2,11 @@ """Pins the registry ``get`` overrides as documented API targets. GitHub issue #1921: ``Command.get``, ``Method.get``, ``OptionType.get``, -``TransportProtocol.get`` and ``AppType.get`` carried ``:meta private:``, so -autodoc left them off their pages and every ``:meth:`get``` naming one rendered -as an unlinked ``get()`` chip. The owner ruled that the overrides are documented -instead, since they carry contract the base -:meth:`~pcapkit.corekit.enum.EnumLookup.get` does not describe. +``TransportProtocol.get`` and ``AppType.get`` are documented rather than marked +``:meta private:``, per the owner's ruling, because they carry contract the base +:meth:`~pcapkit.corekit.enum.EnumLookup.get` does not describe. Autodoc leaves a +private member off its page, so a ``:meth:`get``` naming one renders as an +unlinked ``get()`` chip. Whether a reference resolves is a property of the built inventory, which the pull request checks in the rendered HTML. What this pins is the source-side @@ -55,6 +55,23 @@ 'pcapkit.const.reg.apptype.dccp', ) +#: Docstrings whose relative ``:meth:`get``` must be among the references found. +RELATIVE_PINS = ( + 'pcapkit.const.ftp.command.Command.register', + 'pcapkit.const.http.method.Method._missing_', + 'pcapkit.const.pcapng.option_type.OptionType.register_alias', + 'pcapkit.const.reg.apptype.apptype.AppType.get_all', + 'pcapkit.const.reg.apptype.apptype.AppType.register', +) + +#: ``(file, target)`` pairs that must be among the fully qualified references found. +ABSOLUTE_PINS = ( + ('pcapkit/vendor/http/method.py', 'pcapkit.const.ftp.command.Command.get'), + ('pcapkit/vendor/ftp/command.py', 'pcapkit.const.http.method.Method.get'), + ('pcapkit/protocols/schema/misc/pcapng.py', + 'pcapkit.const.pcapng.option_type.OptionType.get'), +) + #: A ``:meth:`` role, with or without a title, capturing its target. METH = re.compile(r':meth:`(?:[^`<]*<)?[~.]*(?P[\w.]+)>?`') @@ -135,15 +152,15 @@ def test_const_overrides_are_not_private(self) -> 'None': with self.subTest(override=f'{modname}.{clsname}.get'): cls = getattr(importlib.import_module(modname), clsname) owner, func = raw_get(cls) - self.assertIs(owner, cls, f'{clsname} no longer overrides get') + self.assertIs(owner, cls, f'{clsname} does not override get') self.assertTrue(func.__doc__, f'{clsname}.get has no docstring') self.assertFalse(PRIVATE in func.__doc__, f'{clsname}.get is marked private') def test_templates_are_not_private(self) -> 'None': """The vendor templates' ``get`` docstrings carry no ``:meta private:``. - A tag left in a template comes back the next time the scripts - regenerate the const module, so the template is checked separately. + The scripts write a template's tag into the const module whenever they + regenerate it, so the template is checked separately. """ for template in sorted({path for _, _, path in OVERRIDES}): @@ -177,7 +194,7 @@ def check(self, where: 'str', target: 'str', cls: 'Optional[type]') -> 'bool': def test_relative_references(self) -> 'None': """``:meth:`get``` in a registry docstring targets a documented override.""" - hits = 0 + hits = set() for modname in RELATIVE_MODULES: module = importlib.import_module(modname) for cls in vars(module).values(): @@ -186,14 +203,16 @@ def test_relative_references(self) -> 'None': for where, doc in docstrings(cls): for match in METH.finditer(doc): target = match.group('target') - hits += self.check(f'{modname}.{where}', target, - resolve(target, module, cls)) - self.assertTrue(hits, 'no relative reference to a get override was found') + if self.check(f'{modname}.{where}', target, + resolve(target, module, cls)): + hits.add(f'{modname}.{where}') + for pin in RELATIVE_PINS: + self.assertTrue(pin in hits, f'{pin} has no :meth:`get` reaching an override') def test_absolute_references(self) -> 'None': """A fully qualified ``:meth:`` to an override, in code or docs, is documented.""" paths = sorted((ROOT / 'pcapkit').rglob('*.py')) + sorted((ROOT / 'docs').rglob('*.rst')) - hits = 0 + hits = set() for path in paths: text = path.read_text(encoding='utf-8') for match in METH.finditer(text): @@ -201,9 +220,11 @@ def test_absolute_references(self) -> 'None': if not target.startswith('pcapkit.const.') or not target.endswith('.get'): continue line = text.count('\n', 0, match.start()) + 1 - hits += self.check(f'{path.relative_to(ROOT)}:{line}', target, - resolve(target, None, None)) - self.assertTrue(hits, 'no fully qualified reference to a get override was found') + relative = path.relative_to(ROOT).as_posix() + if self.check(f'{relative}:{line}', target, resolve(target, None, None)): + hits.add((relative, target)) + for pin in ABSOLUTE_PINS: + self.assertTrue(pin in hits, f'{pin[0]} has no :meth:`{pin[1]}` reaching an override') if __name__ == '__main__':