Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/source/pcapkit/const/reg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,12 @@ registry is one enumeration per transport protocol sharing a memberless base.
``aenum.IntEnum`` rather than an ``aenum.IntFlag`` (:issue:`808`), numbered
sequentially: ``sctp`` is ``3`` and ``dccp`` is ``4``, where they were ``4``
and ``8``, and no :class:`int` is decoded as a ``|`` composite
(:issue:`860`). Its ``get()`` refuses a name no member declares, with
:exc:`~pcapkit.utilities.exceptions.EnumKeyError`, rather than minting a
member for it (:issue:`808`, :issue:`923`).
(:issue:`860`). An :class:`int` kept from the old numbering is refused
unless a member carries it, and where one does it can name a different
transport: ``3``, once ``tcp | udp``, is ``sctp``, and a bare ``4`` is
``dccp``, not ``sctp``. ``TransportProtocol.get()`` refuses a name no member
declares, with :exc:`~pcapkit.utilities.exceptions.EnumKeyError`, rather
than minting a member for it (:issue:`808`, :issue:`923`).

.. autoclass:: pcapkit.const.reg.apptype.apptype.TransportProtocol
:members:
Expand Down
20 changes: 7 additions & 13 deletions pcapkit/const/reg/apptype/apptype.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,6 @@ class TransportProtocol(EnumLookup, IntEnum):
# value it already is; a plain ``dict.get`` lookup cannot tell a composed
# ``int`` from any other one that happens to equal it. So ``tcp | udp``
# (``3``) resolves as ``sctp``'s own value, silently, with no exception.
# Before 1.5.0 the class was an ``IntFlag`` with ``sctp`` at ``4`` and
# ``dccp`` at ``8``, so an integer held from that numbering (composed or
# not) can land on a different real transport's registry rather than
# merely be refused: a bare ``4`` resolves as ``dccp``'s.

#: No transport protocol. ``TransportProtocol(0) is undefined`` and
#: ``bool(undefined)`` is ``False``; it is the ``proto`` sentinel default
Expand Down Expand Up @@ -99,15 +95,13 @@ def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtoco
that the exception follows what stdlib's ``Enum`` would raise in the
circumstances, :exc:`ValueError` or :exc:`KeyError` as the case may
be, and comes from :mod:`pcapkit.utilities.exceptions` rather than
being a builtin. A
stdlib ``E['nosuch']`` raises :exc:`KeyError`, and :issue:`923`'s census
found :class:`~pcapkit.corekit.enum.EnumLookup` subclasses answering a
name miss that way. The exceptions were :class:`AppType` and its
per-transport registries, and they answered with :exc:`ValueError` only
because their own ``get()`` takes an :class:`int` port and never accepts
a name at all, rather than from any name-miss policy. So there was no policy here to
preserve, and a name miss reaches the caller as
:exc:`~pcapkit.utilities.exceptions.EnumKeyError` from the base. The
being a builtin. A stdlib ``E['nosuch']`` raises :exc:`KeyError`, so a
name miss reaches the caller as
:exc:`~pcapkit.utilities.exceptions.EnumKeyError` from the base.
:class:`AppType` and its per-transport registries raise
:exc:`ValueError` from their own ``get()`` because it takes an
:class:`int` port and never accepts a name; that is not a name-miss
policy, so it sets none for this class to follow. The
maintainer's ruling given in review of the work for :issue:`808`, that
:class:`TransportProtocol` is not to be extended at all, holds either
way: the refusal is a refusal and mints nothing, whichever exception
Expand Down
20 changes: 7 additions & 13 deletions pcapkit/vendor/reg/apptype/apptype.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,6 @@ class TransportProtocol(EnumLookup, IntEnum):
# value it already is; a plain ``dict.get`` lookup cannot tell a composed
# ``int`` from any other one that happens to equal it. So ``tcp | udp``
# (``3``) resolves as ``sctp``'s own value, silently, with no exception.
# Before 1.5.0 the class was an ``IntFlag`` with ``sctp`` at ``4`` and
# ``dccp`` at ``8``, so an integer held from that numbering (composed or
# not) can land on a different real transport's registry rather than
# merely be refused: a bare ``4`` resolves as ``dccp``'s.

#: No transport protocol. ``TransportProtocol(0) is undefined`` and
#: ``bool(undefined)`` is ``False``; it is the ``proto`` sentinel default
Expand Down Expand Up @@ -191,15 +187,13 @@ def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtoco
that the exception follows what stdlib's ``Enum`` would raise in the
circumstances, :exc:`ValueError` or :exc:`KeyError` as the case may
be, and comes from :mod:`pcapkit.utilities.exceptions` rather than
being a builtin. A
stdlib ``E['nosuch']`` raises :exc:`KeyError`, and :issue:`923`'s census
found :class:`~pcapkit.corekit.enum.EnumLookup` subclasses answering a
name miss that way. The exceptions were :class:`AppType` and its
per-transport registries, and they answered with :exc:`ValueError` only
because their own ``get()`` takes an :class:`int` port and never accepts
a name at all, rather than from any name-miss policy. So there was no policy here to
preserve, and a name miss reaches the caller as
:exc:`~pcapkit.utilities.exceptions.EnumKeyError` from the base. The
being a builtin. A stdlib ``E['nosuch']`` raises :exc:`KeyError`, so a
name miss reaches the caller as
:exc:`~pcapkit.utilities.exceptions.EnumKeyError` from the base.
:class:`AppType` and its per-transport registries raise
:exc:`ValueError` from their own ``get()`` because it takes an
:class:`int` port and never accepts a name; that is not a name-miss
policy, so it sets none for this class to follow. The
maintainer's ruling given in review of the work for :issue:`808`, that
:class:`TransportProtocol` is not to be extended at all, holds either
way: the refusal is a refusal and mints nothing, whichever exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
checks on a sample module which literals count as template text.
* :class:`TestDocsPages` pins the notes the docs pages carry for the tables (#1884).

The same holds for a release-versioned note written as prose rather than as a
directive, such as "Before 1.5.0 the class was ..." (#2315), so both scans also
refuse :data:`RELEASE_PROSE`.

This module is unit-tier and reads no capture.

"""
Expand All @@ -41,6 +45,11 @@
#: A version directive, as Sphinx spells it: reST allows any whitespace after the ``..`` (#1902).
DIRECTIVE = re.compile(r'\.\.\s+version(?:added|changed|removed)::.*')

#: A note dated to a release in prose (#2315): a lead-in such as ``Before`` and
#: a three-part release number. Python versions are spelled with two parts here.
RELEASE_PROSE = re.compile(r'\b(?:[Bb]efore|[Ss]ince|[Uu]ntil|[Aa]s of|[Pp]rior to)\s+'
r'(?:(?:PyPCAPKit|pcapkit|version|release)\s+)?v?\d+\.\d+\.\d+')

#: The crawler modules #1884 names, whose templates must carry no directive.
ISSUE_VENDORS = (
'ftp/command.py',
Expand All @@ -65,6 +74,7 @@
('reg.rst', 'versionchanged:: 1.1.1', 'port number'),
('reg.rst', 'versionchanged:: 1.5.0', ':issue:`732`'),
('reg.rst', 'versionchanged:: 1.5.0', ':issue:`808`'),
('reg.rst', 'versionchanged:: 1.5.0', 'a bare ``4``'),
('tcp.rst', 'versionchanged:: 1.5.0', '16-bit'),
)

Expand Down Expand Up @@ -127,11 +137,12 @@ def template_literals(tree: 'ast.Module', crawlers: 'frozenset[str]') -> 'list[a
return literals


def template_directives(tree: 'ast.Module', crawlers: 'frozenset[str]') -> 'list[tuple[int, str]]':
"""Every directive in the template text, as ``(literal's first line, directive)``."""
def template_directives(tree: 'ast.Module', crawlers: 'frozenset[str]',
pattern: 're.Pattern[str]' = DIRECTIVE) -> 'list[tuple[int, str]]':
"""Every ``pattern`` match in the template text, as ``(literal's first line, match)``."""
return [(node.lineno, match.group().strip())
for node in template_literals(tree, crawlers)
for match in DIRECTIVE.finditer(node.value)]
for match in pattern.finditer(node.value)]


def _vendor_trees() -> 'dict[pathlib.Path, ast.Module]':
Expand All @@ -141,7 +152,7 @@ def _vendor_trees() -> 'dict[pathlib.Path, ast.Module]':


class TestGeneratedModules(unittest.TestCase):
"""No module under :mod:`pcapkit.const` carries a version directive."""
"""No module under :mod:`pcapkit.const` carries a version note."""

def test_no_const_module_carries_a_directive(self) -> 'None':
paths = sorted((ROOT / 'pcapkit' / 'const').rglob('*.py'))
Expand All @@ -154,9 +165,19 @@ def test_no_const_module_carries_a_directive(self) -> 'None':
found.append(f'{path.relative_to(ROOT)}:{lineno}: {match.group().strip()}')
self.assertEqual(found, [])

def test_no_const_module_dates_a_note_to_a_release(self) -> 'None':
# #2315: "Before 1.5.0 ..." is a version note in prose.
found = [] # type: list[str]
for path in sorted((ROOT / 'pcapkit' / 'const').rglob('*.py')):
for lineno, line in enumerate(path.read_text(encoding='utf-8').splitlines(), start=1):
match = RELEASE_PROSE.search(line)
if match is not None:
found.append(f'{path.relative_to(ROOT)}:{lineno}: {match.group()}')
self.assertEqual(found, [])


class TestVendorTemplates(unittest.TestCase):
"""No crawler's template text carries a version directive."""
"""No crawler's template text carries a version note."""

def test_no_template_carries_a_directive(self) -> 'None':
trees = _vendor_trees()
Expand All @@ -166,6 +187,23 @@ def test_no_template_carries_a_directive(self) -> 'None':
for lineno, directive in template_directives(tree, crawlers)]
self.assertEqual(found, [])

def test_no_template_dates_a_note_to_a_release(self) -> 'None':
# #2315: "Before 1.5.0 ..." is a version note in prose.
trees = _vendor_trees()
crawlers = vendor_classes(trees.values())
found = [f'{path.relative_to(ROOT)}:{lineno}: {match}'
for path, tree in trees.items()
for lineno, match in template_directives(tree, crawlers, RELEASE_PROSE)]
self.assertEqual(found, [])

def test_release_prose_is_matched(self) -> 'None':
for text in ('Before 1.5.0 the class', 'since PyPCAPKit 1.4.1', 'Prior to\nv1.3.0'):
with self.subTest(text=text):
self.assertIsNotNone(RELEASE_PROSE.search(text))
for text in ('from 3.9 only', 'before 3.9', 'RFC 1.5'):
with self.subTest(text=text):
self.assertIsNone(RELEASE_PROSE.search(text))

def test_the_issue_templates_are_read(self) -> 'None':
# Guards the scan above against passing because it read nothing.
trees = _vendor_trees()
Expand Down
Loading