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
42 changes: 38 additions & 4 deletions pcapkit/corekit/infoclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,10 @@ class Info(ReduceAsProtocol2, Mapping[str, VT], Generic[VT], metaclass=InfoMeta)
#: List of names of keys left out of ``repr``, ``str`` and the dumps of
#: :mod:`pcapkit.dumpkit` -- metadata such as ``__next_type__``, which
#: :meth:`to_dict` may still carry, see :attr:`__included__`.
#: Finalisation adds the names in :attr:`__builtin__`.
#: Finalisation adds the names in :attr:`__builtin__`, though ``repr``,
#: ``str`` and the dumps write a field renamed for clashing with one of
#: them, such as a reassembled datagram's ``packet``, under its own name
#: (:issue:`2733`).
__excluded__: 'list[str]' = []
#: List of names of dunder keys that iterating an instance, ``len()`` and
#: :meth:`to_dict` carry -- metadata such as ``__next_type__``, which a
Expand Down Expand Up @@ -771,9 +774,16 @@ def to_dict(self) -> 'OrderedMultiDict[str, VT]':
:attr:`__excluded__`, and records those as its own ``__excluded__``
for the dumpers to leave out (:issue:`2003`).

.. versionchanged:: 1.6.0

The ``__excluded__`` it records leaves out a field renamed for
clashing with a builtin name, such as a reassembled datagram's
``packet``, so the dumpers write that field under its own name
(:issue:`2733`).

"""
dict_ = _OrderedMultiDict() # type: _OrderedMultiDict[str, Any]
dict_.__excluded__ = self.__excluded__
dict_.__excluded__ = _dump_excluded(self)
for (key, value) in self.__items_multi():
out_key = self.__map_reverse__.get(key, key)
if isinstance(value, Info):
Expand Down Expand Up @@ -827,8 +837,9 @@ class _OrderedMultiDict(OrderedMultiDict[KT, VT]):
__ne__ = _ne

#: Names of the keys the dumpers leave out: the :attr:`Info.__excluded__`
#: of the info exported, kept by copies, deep copies and pickles; none for
#: an export of a multi-mapping (:issue:`2003`).
#: of the info exported, less the fields it renamed for clashing with a
#: builtin name (:issue:`2733`), kept by copies, deep copies and pickles;
#: none for an export of a multi-mapping (:issue:`2003`).
__excluded__ = () # type: Collection[str]

def __repr__(self) -> 'str':
Expand Down Expand Up @@ -862,6 +873,29 @@ def _restore_export(cls: 'Type[_OrderedMultiDict]', pairs: 'list[tuple[Any, Any]
return new


def _dump_excluded(info: 'Info') -> 'Collection[str]':
"""Return the names of the keys of ``info`` the dumps leave out.

Args:
info: A field model, i.e. an :class:`Info` that is no multi-mapping.

Returns:
Its :attr:`Info.__excluded__`, less each field it renamed for clashing
with a builtin name. Finalisation adds every such name to
:attr:`Info.__excluded__`, and the dumps see a key under its own name
rather than as renamed, so they would otherwise leave out every renamed
field, a reassembled datagram's ``packet`` among them (:issue:`2733`).
``repr`` and ``str`` see the renamed key, and write the field. A
renamed dunder key stays left out, being metadata.

"""
renamed = info.__map__
if not renamed:
return info.__excluded__
return [name for name in info.__excluded__
if name not in renamed or (name.startswith('__') and name.endswith('__'))]


class _MultiInfo(Info[VT], Generic[KT, VT]):
"""An :class:`Info` that holds its values as a multi-mapping.

Expand Down
51 changes: 38 additions & 13 deletions pcapkit/dumpkit/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
import dictdumper.plist
import dictdumper.tree

from pcapkit.corekit.infoclass import Info
from pcapkit.corekit.infoclass import Info, _dump_excluded
from pcapkit.corekit.multidict import MultiDict, OrderedMultiDict
from pcapkit.corekit.sentinels import NoValueType, NullType
from pcapkit.protocols.protocol import ProtocolBase
from pcapkit.protocols.schema.schema import Schema
from pcapkit.utilities import config
from pcapkit.utilities.exceptions import UnsupportedCall
Expand Down Expand Up @@ -357,6 +358,16 @@ def make_dumper(output: 'Type[ABCDumper]') -> 'Type[ABCDumper]':
kept (:issue:`1257`), and a string carrying a character XML forbids
is written as ``<data>`` (:issue:`1262`).

.. versionchanged:: 1.6.0

A field an :class:`~pcapkit.corekit.infoclass.Info` renamed for
clashing with a builtin name, such as a reassembled datagram's
``packet``, is written under its own name rather than left out, and
reading it resolves a deferred ``packet`` as indexing does. A protocol
object, such as that ``packet``, is written as its
:attr:`~pcapkit.protocols.protocol.Protocol.info`, as a frame is,
rather than by its attributes (:issue:`2733`).

"""
# NOTE: :class:`~dictdumper.plist.PLIST` -- which is also what
# :attr:`Extractor.__output__ <pcapkit.foundation.extraction.Extractor.__output__>`
Expand Down Expand Up @@ -416,21 +427,27 @@ def excluded(info: 'Info | MultiDict[Any, Any]') -> 'Collection[Any]':
The :attr:`~pcapkit.corekit.infoclass.Info.__excluded__` of a field
model, or of the field model an export was made from -- metadata
such as ``__next_type__``, which only a rebuild reads
(:issue:`2003`). Empty for an option list, whose keys are its
(:issue:`2003`) -- less the fields it renamed for clashing with a
builtin name, which are written under their own names
(:issue:`2733`). Empty for an option list, whose keys are its
options.

"""
if isinstance(info, Info) and isinstance(info, MultiDict):
return ()
if isinstance(info, Info):
if isinstance(info, MultiDict):
return ()
return _dump_excluded(info)
return getattr(info, '__excluded__', ())

def fields(info: 'Info | MultiDict[Any, Any]') -> 'dict[Any, Any]':
def fields(info: 'Info | MultiDict[Any, Any] | ProtocolBase') -> 'dict[Any, Any]':
"""Return the fields of ``info`` as the :class:`dict` the writer is handed.

Args:
info: The fields, as an :class:`~pcapkit.corekit.infoclass.Info` or
as the :class:`~pcapkit.corekit.multidict.OrderedMultiDict` its
:meth:`~pcapkit.corekit.infoclass.Info.to_dict` returns.
:meth:`~pcapkit.corekit.infoclass.Info.to_dict` returns, or a
protocol object, whose fields are those of its
:attr:`~pcapkit.protocols.protocol.Protocol.info`.

Returns:
One entry per field, in order, with its key unchanged -- the writer
Expand All @@ -443,8 +460,11 @@ def fields(info: 'Info | MultiDict[Any, Any]') -> 'dict[Any, Any]':
written. The keys :func:`excluded` names are left out.

"""
skip = excluded(info)
pairs = [(key, val) for key, val in info.items(multi=True) if key not in skip]
# NOTE: a protocol object, as a reassembled datagram's ``packet``, is
# written as its info, as a frame is, never by its attributes (#2733).
model = info.info if isinstance(info, ProtocolBase) else info # type: Info | MultiDict[Any, Any]
skip = excluded(model)
pairs = [(key, val) for key, val in model.items(multi=True) if key not in skip]
counts = collections.Counter(key for key, _ in pairs)
result = {} # type: dict[Any, Any]
for key, val in pairs:
Expand All @@ -454,7 +474,7 @@ def fields(info: 'Info | MultiDict[Any, Any]') -> 'dict[Any, Any]':
# NOTE: read by indexing, so that a model overriding it is
# honoured -- the traceflow and reassembly models resolve a
# deferred ``packet`` there.
result[key] = info[key]
result[key] = model[key]
return result

#: Whether the writer is handed the fields of an
Expand Down Expand Up @@ -484,12 +504,13 @@ def __call__(self, value: 'Any', name: 'Optional[str]' = None) -> 'Any':
:class:`~pcapkit.corekit.multidict.MultiDict` its
:meth:`~pcapkit.corekit.infoclass.Info.to_dict` returns, is
written as the :class:`dict` :func:`fields` makes of it, so
that a repeated field keeps every value. The writer walks
that a repeated field keeps every value; a protocol object
is written as that of its info. The writer walks
the block it is handed without calling
:meth:`object_hook` on it, only on what it holds.

"""
if isinstance(value, (Info, MultiDict)):
if isinstance(value, (Info, MultiDict, ProtocolBase)):
value = fields(value)
return super().__call__(value, name)

Expand Down Expand Up @@ -526,14 +547,18 @@ def object_hook(self, o: 'Any') -> 'Any':
the option lists an :class:`~pcapkit.corekit.infoclass.Info`
holds. An :class:`~pcapkit.corekit.infoclass.Info` itself
becomes the :class:`dict` of its fields that :func:`fields`
returns, holding every value of a repeated field.
returns, holding every value of a repeated field, and so does a
protocol object, as its
:attr:`~pcapkit.protocols.protocol.Protocol.info`, rather
than by its attributes, which hold such per-process detail as
the address of its file object (:issue:`2733`).

"""
if isinstance(o, decimal.Decimal):
result = str(o) # type: Any
elif isinstance(o, datetime.timedelta):
result = o.total_seconds()
elif isinstance(o, Info) and not isinstance(o, MultiDict):
elif isinstance(o, ProtocolBase) or (isinstance(o, Info) and not isinstance(o, MultiDict)):
# NOTE: a MultiInfo or OrderedMultiInfo, though an Info, is
# an option list, and is written by the branches below.
result = fields(o)
Expand Down
Loading
Loading