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
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ IPsec one.
The code cannot be used as evidence
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

**This is the part a future reader will get wrong, so it is stated before the
criterion itself.** The obvious way to decide whether a header is "usable as a
standalone protocol" is to ask what the library's own dispatch already allows. That
answer is useless, and measurably so:
**Stated before the criterion itself, because it is the part a future reader will get
wrong.** The obvious way to decide whether a header is "usable as a standalone protocol"
is to ask what the library's own dispatch already allows. That answer is useless, and
measurably so:

.. code-block:: pycon

Expand Down Expand Up @@ -96,9 +96,9 @@ fields"* whose addresses are *"the addresses that appear in the Source and
Destination Address fields in the IPv6 packet carrying the Mobility Header"* -- there
is no IPv4 variant of that computation -- and the IPv4 equivalent function is not
protocol 135 at all, since :rfc:`5944` carries Mobile IPv4 over UDP port 434.
``Shim6`` (140) has the same shape and the same verdict; this package has never had a
parser class for it, so nothing implements the classification, but a future one
inherits :class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext` alone.
``Shim6`` (140) has the same shape and the same verdict; this package carries no parser
class for it, so nothing implements the classification, but a future one inherits
:class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext` alone.

.. important::

Expand Down Expand Up @@ -139,11 +139,11 @@ was left behind, and that was deliberate. The owner ruled that the
``IPv6_GenericExt`` name goes for good: it was an intermediate state, and it was never
released.

The reasoning is what makes it safe rather than merely decided: the old name existed
on ``main`` from ``b3551cb63`` to ``93cf940b3`` -- under four hours on one day, and
after the most recent release tag -- so it appears in **no** release, and the break
has no callers to inconvenience. Do not reintroduce it as an alias, and do not cite
it in prose as a former public name; it was never one.
What makes that safe rather than merely decided: the old name existed on ``main`` from
``b3551cb63`` to ``93cf940b3``, under four hours on one day, and no release tag falls
between the two commits -- ``git tag --contains`` names the same tags for both -- so it
appears in **no** release and the break has no callers to inconvenience. Do not
reintroduce it as an alias, and do not cite it as a former public name.

ESP is an extension header, and still cannot short-circuit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
15 changes: 6 additions & 9 deletions docs/source/contributing/conventions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,19 @@ House Conventions
automated contributor would otherwise have to rediscover by reading a closed
issue thread. Each ruling names where it was settled.

Most of them govern :mod:`pcapkit.const`, which is where the settled
questions have mostly arisen, and the page was titled *Registry Conventions*
until `#918 <https://github.com/JarryShaw/PyPCAPKit/issues/918>`__ widened
it, then split it. :ref:`extension-header-subclassing` is the first ruling
here that governs a protocol class hierarchy rather than a registry, and
:ref:`process` the first that governs the repository rather than any of its
code.
:ref:`mint-criterion` and :ref:`registry-protocol` govern
:mod:`pcapkit.const`, which is where the settled questions have mostly
arisen. :ref:`sentinel-convention` governs :mod:`pcapkit.corekit`,
:ref:`extension-header-subclassing` a protocol class hierarchy, and
:ref:`process` the repository rather than any of its code.

**A ruling that stays in its thread is a ruling that gets rediscovered.** So
when a question is answered in a way the code cannot express on its own -- a
classification, a naming rule, a deliberate asymmetry -- it is written onto
the page that covers it in the same change that implements it, rather than
left in the issue for the next contributor to find. That is the owner's
standing ask on
`#918 <https://github.com/JarryShaw/PyPCAPKit/issues/918>`__: every
convention settled from here on gets documented here as well.
`#918 <https://github.com/JarryShaw/PyPCAPKit/issues/918>`__.

.. toctree::
:maxdepth: 1
Expand Down
102 changes: 66 additions & 36 deletions docs/source/contributing/conventions/mint-criterion.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,77 +3,102 @@
When an unrecognised value may mint a member
--------------------------------------------

Every registry under :mod:`pcapkit.const` defines ``_missing_``, which decides what
happens when a value has no member. There are two possible behaviours, and which one
a given range gets is a **design decision, not a style preference**:
121 of the 127 registries under :mod:`pcapkit.const` define ``_missing_``, which decides
what happens when a value has no member. The six without one --
:class:`~pcapkit.const.ipv6.extension_header.ExtensionHeader`,
:class:`~pcapkit.const.pcapng.tls_key_label.TLSKeyLabel` and
:class:`~pcapkit.const.reg.apptype.apptype.AppType`'s four transport subclasses -- carry
no declared-but-unassigned range for one to resolve. There are two possible behaviours,
and which one a given range gets is a **design decision, not a style preference**:

``extend_enum(cls, name, value)`` -- *mint*
Creates a real, permanent member on the class. It is installed in
``_member_map_`` and ``_value2member_map_``, so it is visible to iteration,
lookup and ``__members__`` from then on, for the life of the process.

:meth:`~pcapkit.corekit.enum.EnumRegistry._unregistered_member` -- *unmint*
Returns a member-like object for the value **without** installing it. The
registry does not grow, and a second lookup of the same value is indistinguishable
from the first.
Returns a member-like object for the value **without** installing it. The registry
does not grow, and a second lookup of the same value builds an equal but distinct
object rather than handing back the first.

**Exactly one** ``_missing_`` in the tree mints:
:class:`~pcapkit.const.mh.cga_type.CGAType`'s, whose values are 128-bit CGA extension
tags rather than an IANA-style range of codes. 114 unmint, and the remaining six -- the
five flag registries and :class:`~pcapkit.const.hip.transport.Transport` -- only
range-check and hand back to ``super()._missing_``, declaring no unassigned range at
all. So outside ``CGAType`` the criterion below decides the *name* an unregistered member
carries rather than whether it registers: the registrar's own label suffixed with the
individual code, or the bare block label on its own. The two crawlers that generate the
ranges record that narrowing in their own comments:
:file:`pcapkit/vendor/reg/ethertype.py`'s ``UNASSIGNED_ROW_NAMES`` and
:file:`pcapkit/vendor/ipx/socket.py`'s ``UNASSIGNED_RANGE_NAMES``.

.. mermaid::

flowchart TD
LOOKUP["cls(value) finds no member"] --> MISSING["_missing_"]
MISSING -->|"out of range, or no block covers it"| RAISE["ValueError"]
MISSING -->|"CGAType only"| MINT["extend_enum<br/>permanent member, registry grows"]
MISSING -->|"block label names a party"| SUFFIXED["_unregistered_member<br/>label + _0x&lt;code&gt;"]
MISSING -->|"block label names a procedure"| BARE["_unregistered_member<br/>bare block label"]

The criterion
~~~~~~~~~~~~~

The test, paraphrased from the maintainer's ruling: does the upstream registry treat
the label as the final, concrete assigned name (**mint**), or only as a notation for a
human reading the table (**unmint**)?
the label as the final, concrete assigned name, or only as a notation for a human
reading the table?

Settled on `#847 <https://github.com/JarryShaw/PyPCAPKit/issues/847>`__ and reaffirmed
Settled on `#847 <https://github.com/JarryShaw/PyPCAPKit/pull/847>`__ and reaffirmed
on `#775 <https://github.com/JarryShaw/PyPCAPKit/issues/775>`__ as a core concept of
the ruling.

So the question to ask of a range is **what the upstream registry actually did**, not
what the generated code happens to look like:

* The source assigns a **real, specific name** to those codes -- minting records
something the registry genuinely says. **Mint.**
* The source assigns a **real, specific name** to those codes. The name records
something the registry genuinely says, so it is carried across and suffixed with the
code it belongs to.
* The source says only that the codes are spoken for, without naming them --
``Unassigned``, ``Reserved``, ``Reserved for Private Use``,
``Reserved for Experimental Use``, ``Deprecated``, ``Dynamically Assigned``
and ``Statically Assigned``. These are written for a human reading the
table. Minting them **manufactures a name nobody
assigned**, and the value will get its real name if and when something assigns
it. **Unmint.**
and ``Statically Assigned``. These are written for a human reading the table, so the
bare label stands: suffixing one with a code would **manufacture a name nobody
assigned**, and the value will get its real name if and when something assigns it.

Worked examples
~~~~~~~~~~~~~~~

*Unmint.* :mod:`pcapkit.const.ipx.socket`'s ``Dynamically Assigned``,
*Bare label.* :mod:`pcapkit.const.ipx.socket`'s ``Dynamically Assigned``,
``Dynamically Assigned Socket Numbers``, ``Statically Assigned Socket Numbers`` and
``Experimental`` ranges. Each names **how the socket will be allocated**, not what
occupies it; the real name arrives with the allocation.

*Mint.* :mod:`pcapkit.const.reg.ethertype`'s company names -- ``Xyplex``,
*Suffixed.* :mod:`pcapkit.const.reg.ethertype`'s company names -- ``Xyplex``,
``Datability``, ``Qualcomm``, ``Motorola`` and forty-two others, 46 names across 50
range blocks, since four of them hold two blocks each -- and, in the same file's
neighbour, :mod:`pcapkit.const.ipx.socket`'s ``Registered by Xerox``. A company name
is the assignment, for the reason in the next section.
range blocks, since four of them hold two blocks each -- and
:mod:`pcapkit.const.ipx.socket`'s ``Registered by Xerox``. A company name is the
assignment, for the reason in the next section.

Two further ranges in that file mint without being company names at all:
``IEEE802.3 Length Field``, which names a field in a standard, and
Two further ranges in that file carry a suffixed name without being company names at
all: ``IEEE802.3 Length Field``, which names a field in a standard, and
``Berkeley Trailer encap/IP``, which names an encapsulation. Both are outside the 46,
and neither mints for the reason the next section gives.
and neither is suffixed for the reason the next section gives.

.. note::

Those two groups look alike and the line between them is **not** range-versus-single
code. ``Registered by Xerox`` covers a range and still mints, because it names *who
registered the socket*. ``Dynamically Assigned`` also covers a range and does not,
because it names only the *mechanism* by which some future party will take it. Ask
what the label tells you: a party, or a procedure.
code. ``Registered by Xerox`` covers a range and is still suffixed per socket,
because it names *who registered the socket*. ``Dynamically Assigned`` also covers a
range and is not, because it names only the *mechanism* by which some future party
will take it. Ask what the label tells you: a party, or a procedure.

Why the company names mint
~~~~~~~~~~~~~~~~~~~~~~~~~~
Why the company names are suffixed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The ethertype case looks like an exception to the rule and is not. The maintainer's
reasoning, settled on `#775 <https://github.com/JarryShaw/PyPCAPKit/issues/775>`__ after
being raised on `#847 <https://github.com/JarryShaw/PyPCAPKit/issues/847>`__: a
being raised on `#847 <https://github.com/JarryShaw/PyPCAPKit/pull/847>`__: a
proprietary protocol will never have a public name, so the company name is what serves
that purpose in its place.

Expand All @@ -88,7 +113,10 @@ Checking the current state

The split is measurable rather than a matter of memory. Slice each ``_missing_`` body
and see which call it makes -- an :mod:`ast` walk is reliable where a text search is
not, because ``extend_enum`` also appears in imports and in prose:
not, because ``extend_enum`` also appears in imports and in prose. Match on the
attribute name as well as on the bare name: ``extend_enum`` is called as a plain
function, but ``_unregistered_member`` is called on ``cls``, so a walk that inspects
only ``ast.Name`` nodes finds every mint and no unmint at all:

.. code-block:: python

Expand All @@ -100,8 +128,9 @@ not, because ``extend_enum`` also appears in imports and in prose:
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name == '_missing_':
calls = {n.func.id for n in ast.walk(node)
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)}
calls = {n.func.id if isinstance(n.func, ast.Name) else n.func.attr
for n in ast.walk(node) if isinstance(n, ast.Call)
and isinstance(n.func, (ast.Name, ast.Attribute))}
if 'extend_enum' in calls:
print('MINT ', path)
elif '_unregistered_member' in calls:
Expand All @@ -111,6 +140,7 @@ not, because ``extend_enum`` also appears in imports and in prose:

Calling ``Cls(value)`` on a registry whose ``_missing_`` mints **mutates the
class**. A probe is not a read: it installs a member that every later lookup then
finds. Snapshot ``{member.value for member in Cls}`` before any lookup, and use a
throwaway process per registry when comparing behaviour across revisions.

finds. :class:`~pcapkit.const.mh.cga_type.CGAType` is the one registry this applies
to today, but snapshot ``{member.value for member in Cls}`` before any lookup
regardless, and use a throwaway process per registry when comparing behaviour across
revisions.
43 changes: 19 additions & 24 deletions docs/source/contributing/conventions/process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ How the repository itself is run
The four pages before this one are about writing library code. The rulings here are
about running the repository -- what an install carries, what a changelog entry is,
and what the issue and pull request labels mean. None of them is derivable from a
module, which is why they are recorded rather than left to be rediscovered.

The page exists because those three settled questions fit none of the code-convention
pages, and the owner ruled on
module, and none fits a code-convention page, so the owner ruled on
`#918 <https://github.com/JarryShaw/PyPCAPKit/issues/918>`__ that they get a page of
their own rather than being left in their threads. Each ruling below is **paraphrased
rather than quoted**, also on his instruction there; the issue named beside it is
Expand All @@ -34,7 +31,7 @@ On the tree, in :file:`pyproject.toml`:
all = [ "emoji", "cryptography>=3.4", "pycrate" ]

So ``all`` is exactly the union of those three extras, written out as literals rather
than referenced -- eight requirements before the ruling, three after it.
than referenced.

Two groups stay out, and the reasons are **different** rather than two versions of
one reason. Keeping them apart is what stops the list drifting the way it already did
Expand Down Expand Up @@ -98,19 +95,17 @@ commands down instead of a figure that will be stale by the next merge:
# commits on the pull request -- a different number, about a different thing
gh pr view 657 -R JarryShaw/PyPCAPKit --json commits -q '.commits|length'

The grouping scheme was settled after that, on #918: **a section per top-level
module, with** ``Added``/``Changed``/``Fixed`` **nested inside each** -- module
granularity, not per-file and not per-subpackage. **That restructure has landed.** It
was a regrouping of scattered entries rather than a transposition of three tidy
blocks: before it, the entries carried their kind as an inline bold label and those
labels alternated in dozens of short stretches, blocked near the top of the file and
thoroughly interleaved below. The file now carries **9** module-level sections holding 155
entries, and no entry carries an inline kind label any more::
The grouping scheme was settled on #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 **9** module-level sections holding
155 entries, and no entry carries an inline kind label::

$ grep -cE '^\* \*\*(Added|Changed|Fixed)\*\*' docs/source/changelog/1.5.0.rst
0

The sections are one per top-level module, plus one for what belongs to no module:
Eight of those nine name a module; the ninth, *Project infrastructure*, is for what
belongs to none. Nor is the map one-to-one with the package list below --
:mod:`pcapkit.interface` has no 1.5.0 entry, so it has no section of its own:

.. code-block:: shell

Expand Down Expand Up @@ -166,10 +161,9 @@ The ones that carry meaning here fall into five groups, which stack rather than
compete: a pull request normally carries one from the first group and as many of the
rest as apply. **The five groups are not the whole label set** -- the repository also
has GitHub's own defaults, of which ``wontfix``, ``invalid``, ``help wanted`` and
``duplicate`` are all in live use -- only ``good first issue`` has never been
applied. They are documented by
GitHub rather than here, and are counted rather than listed so this page does not go
stale every time one is added::
``duplicate`` are all in live use and only ``good first issue`` has never been applied.
Those are documented by GitHub rather than here, and are counted rather than listed so
this page does not go stale every time one is added::

$ gh label list -R JarryShaw/PyPCAPKit --limit 100 --json name -q '.[].name' | wc -l
29
Expand Down Expand Up @@ -215,8 +209,9 @@ commit, so the label and the message agree by construction:
values

**Issue kind**, for issues rather than pull requests: ``design`` marks a pattern being
decided rather than a defect or a request, which is the label most of the rulings on
these pages were settled under; alongside ``bug``, ``enhancement`` and ``question``.
decided rather than a defect or a request, and a majority of the rulings on these pages
were filed under it -- though not all, several having been settled on a ``bug`` or
``enhancement`` thread instead. Alongside ``bug``, ``enhancement`` and ``question``.

**State -- what is happening to it now.** An open issue is meant to carry one of
these, so that its status is readable without opening it:
Expand All @@ -239,7 +234,7 @@ than a category, which is worth checking for rather than assuming away:
--json number,labels -q '.[]|"#\(.number) \(.labels|map(.name)|join(","))"'

**Review -- the cross-review verdict, at the current head.** Separate from CI, which
has its own status of its own: ``review: pending`` means no verdict for this head,
has a status of its own: ``review: pending`` means no verdict for this head,
either never reviewed or the head moved since; ``review: good-to-go`` and
``review: needs-changes`` are the two verdicts. Because they are keyed on the head
rather than on the pull request, a new push invalidates the label -- a verdict that
Expand Down Expand Up @@ -291,9 +286,9 @@ carrying it are that kind:
pre-``0.15`` pull requests, ``#3`` to ``#28``, with nothing at all between ``#28``
and ``#350``. Three of those seven are distribution rollups (``#25``, ``#26``,
``#28``, each also carrying ``release``); the other four are early
``refactor``/``feat`` work from before the project stabilised. On issues it is sparser still, appearing only from
``#775`` up. So ``breaking``'s absence on an old pull request is weak evidence at
best. The current figures, rather than these:
``refactor``/``feat`` work from before the project stabilised. On issues it is
sparser still, appearing only from ``#775`` up. So ``breaking``'s absence on an old
pull request is weak evidence at best. The current figures, rather than these:

.. code-block:: shell

Expand Down
Loading
Loading