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
56 changes: 56 additions & 0 deletions docs/source/ext.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,62 @@ The following code snippet shows how to create a new protocol class:
# register protocol class
register_ethertype(EtherType.Internet_Protocol_version_4, MyIPv4)

.. important::

**Declare every construction keyword your protocol accepts.** Since #617,
building a protocol *through its constructor* with a keyword that no signature
declares raises :exc:`~pcapkit.utilities.exceptions.UnsupportedCall` rather than
discarding it, so a misspelling costs an exception instead of a silently wrong
field. The accepted set is read from :func:`inspect.signature` -- the union of every
keyword-taking parameter of ``make``, ``read``, ``pack``, ``unpack``,
``__post_init__`` and ``__init__`` anywhere in the class's MRO -- which is
wider than ``make`` alone because :meth:`ProtocolBase.__post_init__
<pcapkit.protocols.protocol.ProtocolBase.__post_init__>` hands the same
``**kwargs`` to the construction and to the parse, so a keyword only ``read``
declares still travels through ``make``.

Two shapes a signature cannot express are declared on the class instead, via
:attr:`ProtocolBase.__keywords__
<pcapkit.protocols.protocol.ProtocolBase.__keywords__>`:

.. code-block:: python

class MyIPv4(Internet[IPv4Data, IPv4Schema],
schema=IPv4Schema, data=IPv4Data):
#: A keyword read out of ``**kwargs`` by name rather than declared as a
#: parameter, as ``ESP.read`` does with ``packet``. Unioned down the MRO.
__keywords__ = frozenset({'my_extra_keyword'})

def read(self, length=None, **kwargs):
extra = kwargs.get('my_extra_keyword')
...

Setting it to :obj:`None` skips the check entirely, and is meant only for a
*dispatcher* whose real signature belongs to a class chosen at call time --
:meth:`HTTP.make <pcapkit.protocols.application.http.HTTP.make>` is the one
such class in the library. Unlike a set, :obj:`None` is not inherited, so a
subclass of a dispatcher is checked normally. Prefer a declared parameter to
either: it is also what documents the keyword to your callers.

Parsing is unaffected, and so is :meth:`ProtocolBase.from_data
<pcapkit.protocols.protocol.ProtocolBase.from_data>`, which warns
:exc:`~pcapkit.utilities.warnings.UnknownFieldWarning` instead -- its keywords
come from your ``_make_data``, so a mismatch there is a disagreement between
two of your own mappings rather than a caller's typo.

.. warning::

The check lives in :meth:`ProtocolBase.__init__
<pcapkit.protocols.protocol.ProtocolBase.__init__>`, where every producer's
keywords converge, so it covers ``SomeProtocol(...)`` and the ``pack`` it
leads to -- but **not a direct ``SomeProtocol.make(...)`` call**, which still
discards an undeclared keyword in silence. ``object.__new__(cls).make(...)``
is the idiom that reaches it, used by this package's own tests and by
:meth:`HTTP.make <pcapkit.protocols.application.http.HTTP.make>` to reach its
versioned implementation. Covering that would mean interposing on every
``make`` in the tree, which is a larger change than #617 and was deliberately
not made. Construct through the constructor to get the check.

.. note::

Registering after the fact, as above, is one option. The other is passing
Expand Down
16 changes: 13 additions & 3 deletions examples/generators/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,19 @@

#: TCP header fields held constant across every case that needs a TCP segment.
#: Only ``dstport``/``payload`` vary.
_TCP_BASE = dict(srcport=50000, seq=1, ack=0, ns=False, cwr=False, ece=False,
urg=False, ack_flag=False, psh=False, rst=False, syn=True,
fin=False, window=8192, checksum=b'\x00\x00', urgent_pointer=0)
#:
#: Every key is a parameter :meth:`TCP.make <pcapkit.protocols.transport.tcp.TCP.make>`
#: declares, which is not a style point: this mapping used to read ``seq=1``,
#: ``ack=0``, ``ack_flag=False`` and ``urgent_pointer=0``, of which only ``ack``
#: was a parameter at all -- and it is the acknowledgement *flag*, so the
#: acknowledgement number was never set while the flag was. The other three were
#: absorbed by ``make``'s trailing ``**kwargs`` and discarded, so every segment
#: built here carried sequence number ``0`` however plainly this said ``1``. That
#: is the same defect as #602, and since #617 an undeclared keyword raises
#: :exc:`~pcapkit.utilities.exceptions.UnsupportedCall` rather than going quiet.
_TCP_BASE = dict(srcport=50000, seq_no=1, ack_no=0, ns=False, cwr=False, ece=False,
urg=False, ack=False, psh=False, rst=False, syn=True,
fin=False, window=8192, checksum=b'\x00\x00', urgent=0)


###############################################################################
Expand Down
12 changes: 12 additions & 0 deletions pcapkit/protocols/application/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ class HTTP(Application[_PT, _ST], Generic[_PT, _ST]):
#: Saved subclass protocol data (only for HTTP base class).
_http: 'HTTP[_PT, _ST]'

#: This class is a version dispatcher rather than a protocol with a header of
#: its own, so its construction keywords cannot be enumerated: :meth:`make`
#: declares only ``version`` and forwards everything else to
#: :meth:`HTTPv1.make <pcapkit.protocols.application.httpv1.HTTP.make>` or
#: :meth:`HTTPv2.make <pcapkit.protocols.application.httpv2.HTTP.make>`
#: according to that value -- so the set of names that is correct here depends
#: on an argument. :obj:`None` therefore opts out of the construction keyword
#: check that :meth:`ProtocolBase.__init__
#: <pcapkit.protocols.protocol.ProtocolBase.__init__>` performs (#617); the
#: two versioned classes are checked normally when constructed directly.
__keywords__ = None

##########################################################################
# Properties.
##########################################################################
Expand Down
Loading
Loading