diff --git a/pcapkit/corekit/infoclass.py b/pcapkit/corekit/infoclass.py index 1654b26a03..8d1dda7f39 100644 --- a/pcapkit/corekit/infoclass.py +++ b/pcapkit/corekit/infoclass.py @@ -836,7 +836,7 @@ class _OrderedMultiDict(OrderedMultiDict[KT, VT]): __ne__ = _ne - #: Names of the keys the dumpers leave out: the :attr:`Info.__excluded__` + #: Names of the keys the dumpers leave out -- the :attr:`Info.__excluded__` #: 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`). diff --git a/pcapkit/foundation/engines/pcap_ct.py b/pcapkit/foundation/engines/pcap_ct.py index 868f6bc9cf..f72f7d796c 100644 --- a/pcapkit/foundation/engines/pcap_ct.py +++ b/pcapkit/foundation/engines/pcap_ct.py @@ -34,7 +34,7 @@ class _PCAPCTPackage(Protocol): @property def pcap(self) -> 'Type[Handle]': ... - #: A `pcap-ct`_ "frame": the ``(timestamp, bytes)`` pair that + #: A `pcap-ct`_ "frame" -- the ``(timestamp, bytes)`` pair that #: ``pcap.pcap`` iteration yields. Deliberately *not* named ``Frame``, #: so that it is not mistaken for #: :class:`pcapkit.protocols.misc.pcap.frame.Frame`, which is what the diff --git a/pcapkit/foundation/engines/pcapng.py b/pcapkit/foundation/engines/pcapng.py index 748462553f..921b25995d 100644 --- a/pcapkit/foundation/engines/pcapng.py +++ b/pcapkit/foundation/engines/pcapng.py @@ -108,7 +108,7 @@ class PCAPNG(EngineBase[P_PCAPNG]): #: across the whole file rather than per section. _block_count: 'collections.Counter[str]' - #: The PCAP-NG magic number this engine claims: the Block Type of a Section + #: The PCAP-NG magic number this engine claims -- the Block Type of a Section #: Header Block, read from its one home, #: :class:`~pcapkit.const.pcapng.block_type.BlockType` (:issue:`1739`). It #: reads the same in either byte order. diff --git a/pcapkit/foundation/engines/pypcap.py b/pcapkit/foundation/engines/pypcap.py index 726d356936..e71216233f 100644 --- a/pcapkit/foundation/engines/pypcap.py +++ b/pcapkit/foundation/engines/pypcap.py @@ -40,7 +40,7 @@ class _PyPCAPPackage(Protocol): @property def pcap(self) -> 'Type[Handle]': ... - #: A PyPCAP "frame": the ``(timestamp, bytes)`` pair that ``pcap.pcap`` + #: A PyPCAP "frame" -- the ``(timestamp, bytes)`` pair that ``pcap.pcap`` #: iteration yields. Deliberately *not* named ``Frame``, so that it is not #: mistaken for :class:`pcapkit.protocols.misc.pcap.frame.Frame`, which is #: what the built-in engines return. diff --git a/pcapkit/foundation/reassembly/dtls.py b/pcapkit/foundation/reassembly/dtls.py index 3a59bf2935..3ae362f84c 100644 --- a/pcapkit/foundation/reassembly/dtls.py +++ b/pcapkit/foundation/reassembly/dtls.py @@ -75,7 +75,7 @@ class DTLS(ReassemblyBase[Packet[_AT], Datagram[_AT], BufferID, Buffer], Generic #: Protocol name of current reassembly object. __protocol_name__ = 'DTLS' - #: Protocol of current reassembly object. An extension point: a protocol + #: Protocol of current reassembly object. An extension point -- a protocol #: named here need only define ``analyze_handshake``, with the signature of #: :meth:`DTLS.analyze_handshake #: `, and is read diff --git a/pcapkit/foundation/traceflow/data/tcp.py b/pcapkit/foundation/traceflow/data/tcp.py index 13492fbcab..72d96fd7e6 100644 --- a/pcapkit/foundation/traceflow/data/tcp.py +++ b/pcapkit/foundation/traceflow/data/tcp.py @@ -61,12 +61,12 @@ class Packet(Info, Generic[_AT]): """ - #: Data link type: the PCAP global header's, or that of the PCAP-NG + #: Data link type -- the PCAP global header's, or that of the PCAP-NG #: interface the packet was captured on. protocol: 'Enum_LinkType' #: Frame number. index: 'int' - #: Extracted frame info: the default engine's frame, a third-party + #: Extracted frame info -- the default engine's frame, a third-party #: engine's :class:`~pcapkit.foundation.traceflow.data.data.FrameRecord`, or #: the :obj:`dict` PyShark's adapter reports, which carries no frame record. frame: 'Data_Frame | FrameRecord | dict[str, Any]' diff --git a/pcapkit/protocols/application/dtls.py b/pcapkit/protocols/application/dtls.py index 3a5347cb88..10c4e25be4 100644 --- a/pcapkit/protocols/application/dtls.py +++ b/pcapkit/protocols/application/dtls.py @@ -132,7 +132,7 @@ __all__ = ['DTLS', 'DTLSContext'] -#: Length of a legacy record header: content type, legacy version, epoch, +#: Length of a legacy record header -- content type, legacy version, epoch, #: sequence number and length (:rfc:`9147#section-4`). _LEGACY_HEADER_LENGTH = 13 @@ -141,18 +141,18 @@ #: Legacy record version DTLS 1.2 carries, and the value DTLS 1.3 freezes its #: own legacy-format records at (:rfc:`9147#section-4`). _DTLSV1_2_VERSION = 0xFEFD -#: Legacy record versions a DTLS 1.3 record may carry: DTLS 1.2's always, and +#: Legacy record versions a DTLS 1.3 record may carry -- DTLS 1.2's always, and #: DTLS 1.0's on an initial ``ClientHello`` (:rfc:`9147#section-4`). _DTLS13_LEGACY_VERSIONS = frozenset({_DTLSV1_0_VERSION, _DTLSV1_2_VERSION}) #: Top three bits of a unified-header first octet, ``001`` (:rfc:`9147#section-4.1`). _UNIFIED_PREFIX = 0b001 -#: Shortest ciphertext a unified-header record may carry: the record-number +#: Shortest ciphertext a unified-header record may carry. The record-number #: sample is taken from its first 16 octets (:rfc:`9147#section-4.2.3`). _CIPHERTEXT_MIN_LENGTH = 16 #: Offset of a ``tls12_cid`` record's connection ID, after its content type, #: legacy version, epoch and sequence number (:rfc:`9146#section-4`). _CID_OFFSET = 11 -#: Longest connection ID: its length is one octet (:rfc:`9146#section-3`). +#: Longest connection ID. Its length is one octet (:rfc:`9146#section-3`). _CID_MAX_LENGTH = 255 diff --git a/pcapkit/protocols/application/httpv2.py b/pcapkit/protocols/application/httpv2.py index 3c875827af..db5fe16517 100644 --- a/pcapkit/protocols/application/httpv2.py +++ b/pcapkit/protocols/application/httpv2.py @@ -265,7 +265,7 @@ def alias(self) -> 'Literal["HTTP/2"]': @property def length(self) -> 'int': - """Header length of current protocol: the 9-octet frame header, after the + """Header length of current protocol -- the 9-octet frame header, after the 24-octet connection preface when one opens the payload.""" return 9 + self._preface_length diff --git a/pcapkit/protocols/application/ssl.py b/pcapkit/protocols/application/ssl.py index 9e5fa1e69f..e2a448a02d 100644 --- a/pcapkit/protocols/application/ssl.py +++ b/pcapkit/protocols/application/ssl.py @@ -115,7 +115,7 @@ #: The ``supported_versions`` value for DTLS 1.3 (:rfc:`9147#section-5.3`). DTLS13_VERSION = 0xFEFC -#: The ``random`` that makes a ``ServerHello`` a ``HelloRetryRequest``: the +#: The ``random`` that makes a ``ServerHello`` a ``HelloRetryRequest`` -- the #: SHA-256 of ``"HelloRetryRequest"`` (:rfc:`9846#section-4.2.3`). _HRR_RANDOM = bytes.fromhex('cf21ad74e59a6111be1d8c021e65b891c2a211167abb8c5e079e09e2c8a8339c') diff --git a/pcapkit/protocols/application/tls.py b/pcapkit/protocols/application/tls.py index 6d9cdb4453..5019c77a16 100644 --- a/pcapkit/protocols/application/tls.py +++ b/pcapkit/protocols/application/tls.py @@ -90,7 +90,7 @@ __all__ = ['TLS'] -#: Length of the record header: content type, legacy version and length +#: Length of the record header -- content type, legacy version and length #: (:rfc:`9846#section-5.1`). _RECORD_HEADER_LENGTH = 5 @@ -106,7 +106,7 @@ 0x0303: 'TLS 1.2', } # type: dict[int, Literal["SSLv3", "TLS 1.0", "TLS 1.1", "TLS 1.2"]] -#: Legacy record versions a TLS 1.3 record may carry: ``0x0303`` always, and +#: Legacy record versions a TLS 1.3 record may carry -- ``0x0303`` always, and #: ``0x0301`` on an initial ``ClientHello`` (:rfc:`9846#section-5.1`). _TLS13_LEGACY_VERSIONS = frozenset({0x0301, 0x0303}) diff --git a/pcapkit/protocols/data/application/tls.py b/pcapkit/protocols/data/application/tls.py index d840773f8d..dcb2db95e6 100644 --- a/pcapkit/protocols/data/application/tls.py +++ b/pcapkit/protocols/data/application/tls.py @@ -53,7 +53,7 @@ class Extension(Data): #: Extension type. type: 'Enum_ExtensionType' - #: Extension data: decoded, or the raw octets. + #: Extension data -- decoded, or the raw octets. data: 'Union[ExtensionData, bytes]' if TYPE_CHECKING: diff --git a/pcapkit/protocols/data/internet/hopopt.py b/pcapkit/protocols/data/internet/hopopt.py index 4c569f874b..45ffc79f9f 100644 --- a/pcapkit/protocols/data/internet/hopopt.py +++ b/pcapkit/protocols/data/internet/hopopt.py @@ -137,7 +137,7 @@ class CALIPSOOption(Option): #: CALIPSO domain of interpretation. domain: 'int' - #: Compartment bitmap length, in octets: four times the ``Cmpt Length`` + #: Compartment bitmap length, in octets -- four times the ``Cmpt Length`` #: field, which counts 32-bit words. cmpt_len: 'int' #: Sensitivity level. @@ -167,7 +167,7 @@ class SMFIdentificationBasedDPDOption(SMFDPDOption): #: TaggerID type. tid_type: 'TaggerID' - #: ``TidLen`` field as on the wire: the TaggerID length in octets minus + #: ``TidLen`` field as on the wire -- the TaggerID length in octets minus #: one, or ``0`` when there is no TaggerID [:rfc:`6621#section-6.1.1`]. tid_len: 'int' #: TaggerID. @@ -222,7 +222,7 @@ class QuickStartOption(Option): #: QS function. func: 'QSFunction' - #: Rate request/report, in kbps: ``40 * 2 ** N`` for the Rate Request or + #: Rate request/report, in kbps -- ``40 * 2 ** N`` for the Rate Request or #: Rate Report field ``N``, or ``0`` if ``N`` is ``0`` (:rfc:`4782` #: Section 3.1). rate: 'int' diff --git a/pcapkit/protocols/data/internet/igmp.py b/pcapkit/protocols/data/internet/igmp.py index b977b76b47..92aef28d25 100644 --- a/pcapkit/protocols/data/internet/igmp.py +++ b/pcapkit/protocols/data/internet/igmp.py @@ -30,7 +30,7 @@ class IGMP(Protocol): """ - #: Version of IGMP the message was read as, ``1`` or ``2``: the + #: Version of IGMP the message was read as, ``1`` or ``2`` -- the #: :attr:`~pcapkit.protocols.internet.igmp.IGMP.version` of the class that #: read or built it. It is on no octet, since the versions share this #: layout, and :meth:`IGMP.from_data @@ -46,7 +46,7 @@ class IGMP(Protocol): max_resp_time: 'int' #: Checksum. checksum: 'bytes' - #: Group address: the IPv4 multicast group being queried, reported or + #: Group address -- the IPv4 multicast group being queried, reported or #: left, or ``0.0.0.0`` on a General Query. group: 'IPv4Address' diff --git a/pcapkit/protocols/data/internet/igmpv3.py b/pcapkit/protocols/data/internet/igmpv3.py index 72d7fa2a92..74fa0f9a89 100644 --- a/pcapkit/protocols/data/internet/igmpv3.py +++ b/pcapkit/protocols/data/internet/igmpv3.py @@ -30,7 +30,7 @@ class IGMPv3(Protocol): """ - #: Version of IGMP, always ``3``. It is on no octet: it names the class + #: Version of IGMP, always ``3``. It is on no octet; it names the class #: that read or built the message, as on the IGMPv1/v2 data model, and #: :meth:`IGMP.from_data ` #: dispatches on it (:issue:`2574`). @@ -71,7 +71,7 @@ class Query(IGMPv3): max_resp_time: 'int' #: Checksum. checksum: 'bytes' - #: Group address: the IPv4 multicast group being queried, or ``0.0.0.0`` + #: Group address -- the IPv4 multicast group being queried, or ``0.0.0.0`` #: on a General Query. group: 'IPv4Address' #: Resv, the 4 reserved high-order bits of the flags octet diff --git a/pcapkit/protocols/data/internet/ipv4.py b/pcapkit/protocols/data/internet/ipv4.py index 47d1194a07..6c18fa6f28 100644 --- a/pcapkit/protocols/data/internet/ipv4.py +++ b/pcapkit/protocols/data/internet/ipv4.py @@ -423,7 +423,7 @@ class QSOption(Option): #: QS function. func: 'QSFunction' - #: Rate request/report, in kbps: ``40 * 2 ** N`` for the Rate Request or + #: Rate request/report, in kbps -- ``40 * 2 ** N`` for the Rate Request or #: Rate Report field ``N``, or ``0`` if ``N`` is ``0`` (:rfc:`4782` #: Section 3.1). rate: 'int' diff --git a/pcapkit/protocols/data/internet/ipv6_opts.py b/pcapkit/protocols/data/internet/ipv6_opts.py index 0060af563a..e7480fc70e 100644 --- a/pcapkit/protocols/data/internet/ipv6_opts.py +++ b/pcapkit/protocols/data/internet/ipv6_opts.py @@ -142,7 +142,7 @@ class CALIPSOOption(Option): #: CALIPSO domain of interpretation. domain: 'int' - #: Compartment bitmap length, in octets: four times the ``Cmpt Length`` + #: Compartment bitmap length, in octets -- four times the ``Cmpt Length`` #: field, which counts 32-bit words. cmpt_len: 'int' #: Sensitivity level. @@ -173,7 +173,7 @@ class SMFIdentificationBasedDPDOption(SMFDPDOption): #: TaggerID type. tid_type: 'TaggerID' - #: ``TidLen`` field as on the wire: the TaggerID length in octets minus + #: ``TidLen`` field as on the wire -- the TaggerID length in octets minus #: one, or ``0`` when there is no TaggerID [:rfc:`6621#section-6.1.1`]. tid_len: 'int' #: TaggerID. @@ -231,7 +231,7 @@ class QuickStartOption(Option): #: QS function. func: 'QSFunction' - #: Rate request/report, in kbps: ``40 * 2 ** N`` for the Rate Request or + #: Rate request/report, in kbps -- ``40 * 2 ** N`` for the Rate Request or #: Rate Report field ``N``, or ``0`` if ``N`` is ``0`` (:rfc:`4782` #: Section 3.1). rate: 'int' diff --git a/pcapkit/protocols/data/link/gre.py b/pcapkit/protocols/data/link/gre.py index 21af917237..a98ef70c83 100644 --- a/pcapkit/protocols/data/link/gre.py +++ b/pcapkit/protocols/data/link/gre.py @@ -33,7 +33,7 @@ class GRE(Protocol): reserved: 'int' #: Version Number; :rfc:`2784` requires this to be ``0``. version: 'int' - #: Protocol Type: the EtherType of the encapsulated payload. + #: Protocol Type -- the EtherType of the encapsulated payload. protocol_type: 'EtherType' #: Checksum, present only when :attr:`checksum_present` is set. checksum: 'Optional[int]' diff --git a/pcapkit/protocols/data/misc/raw.py b/pcapkit/protocols/data/misc/raw.py index fbb928f559..ae6a2fac23 100644 --- a/pcapkit/protocols/data/misc/raw.py +++ b/pcapkit/protocols/data/misc/raw.py @@ -27,7 +27,7 @@ class Raw(Protocol): #: Original enumeration of this protocol. protocol: 'Optional[int]' - #: Message saying why the packet data is kept raw, if any: that of the error its + #: Message saying why the packet data is kept raw, if any -- that of the error its #: parse raised, or the dissection limit it is past. error: 'Optional[str]' diff --git a/pcapkit/protocols/data/transport/tcp.py b/pcapkit/protocols/data/transport/tcp.py index c4c3bf8de4..3bbcb1d73e 100644 --- a/pcapkit/protocols/data/transport/tcp.py +++ b/pcapkit/protocols/data/transport/tcp.py @@ -355,7 +355,7 @@ class QuickStartResponse(Option): """ - #: Rate request, in kbps: ``40 * 2 ** N`` for the Rate Request field + #: Rate request, in kbps -- ``40 * 2 ** N`` for the Rate Request field #: ``N``, or ``0`` if ``N`` is ``0`` (:rfc:`4782` Section 3.1). req_rate: 'int' #: TTL difference. diff --git a/pcapkit/protocols/internet/igmp.py b/pcapkit/protocols/internet/igmp.py index 9e774490f1..0d0b9b9d7e 100644 --- a/pcapkit/protocols/internet/igmp.py +++ b/pcapkit/protocols/internet/igmp.py @@ -83,7 +83,7 @@ def name(self) -> 'Literal["Internet Group Management Protocol"]': @property @abc.abstractmethod def version(self) -> 'Literal[1, 2, 3]': - """IGMP version this class reads: ``1``, ``2`` or ``3``.""" + """IGMP version this class reads, ``1``, ``2`` or ``3``.""" @property def alias(self) -> 'str': diff --git a/pcapkit/protocols/internet/igmpv1.py b/pcapkit/protocols/internet/igmpv1.py index dfc8572434..e21c2c39a9 100644 --- a/pcapkit/protocols/internet/igmpv1.py +++ b/pcapkit/protocols/internet/igmpv1.py @@ -35,7 +35,7 @@ class IGMPv1(IGMPBase[Data_IGMP, Schema_IGMP], @property def version(self) -> 'Literal[1]': - """IGMP version this class reads: ``1``.""" + """IGMP version this class reads, ``1``.""" return 1 @property diff --git a/pcapkit/protocols/internet/igmpv2.py b/pcapkit/protocols/internet/igmpv2.py index 50d42b5d4d..8c9d4c56d9 100644 --- a/pcapkit/protocols/internet/igmpv2.py +++ b/pcapkit/protocols/internet/igmpv2.py @@ -40,7 +40,7 @@ def name(self) -> 'Literal["Internet Group Management Protocol version 2"]': # @property def version(self) -> 'Literal[2]': - """IGMP version this class reads: ``2``.""" + """IGMP version this class reads, ``2``.""" return 2 @property diff --git a/pcapkit/protocols/internet/igmpv3.py b/pcapkit/protocols/internet/igmpv3.py index 54ebceedeb..23506bd401 100644 --- a/pcapkit/protocols/internet/igmpv3.py +++ b/pcapkit/protocols/internet/igmpv3.py @@ -186,7 +186,7 @@ def name(self) -> 'Literal["Internet Group Management Protocol version 3"]': # @property def version(self) -> 'Literal[3]': - """IGMP version this class reads: ``3``.""" + """IGMP version this class reads, ``3``.""" return 3 @property diff --git a/pcapkit/protocols/internet/internet.py b/pcapkit/protocols/internet/internet.py index 844703060d..ec8d52607b 100644 --- a/pcapkit/protocols/internet/internet.py +++ b/pcapkit/protocols/internet/internet.py @@ -160,7 +160,7 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab }, ) - #: What :attr:`__proto__` is keyed by: the enum a key must be a member of, + #: What :attr:`__proto__` is keyed by -- the enum a key must be a member of, #: and whether a bare :class:`int` is a valid key there too. Declared by the #: class *owning* the registry, and read from it by #: :func:`~pcapkit.foundation.registry.protocols.protocol.register_protocol_code` so a diff --git a/pcapkit/protocols/link/link.py b/pcapkit/protocols/link/link.py index 8efaa95b96..04ed2f1db8 100644 --- a/pcapkit/protocols/link/link.py +++ b/pcapkit/protocols/link/link.py @@ -104,7 +104,7 @@ class Link(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=abstra }, ) # type: DefaultDict[int | Enum_EtherType, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] - #: What :attr:`__proto__` is keyed by: the enum a key must be a member of, + #: What :attr:`__proto__` is keyed by -- the enum a key must be a member of, #: and whether a bare :class:`int` is a valid key there too. Declared by the #: class *owning* the registry, and read from it by #: :func:`~pcapkit.foundation.registry.protocols.protocol.register_protocol_code` so a diff --git a/pcapkit/protocols/link/radiotap.py b/pcapkit/protocols/link/radiotap.py index bed23f36a9..bd09db9e8b 100644 --- a/pcapkit/protocols/link/radiotap.py +++ b/pcapkit/protocols/link/radiotap.py @@ -224,18 +224,18 @@ from pcapkit.corekit.fields.numbers import BitmapField from pcapkit.protocols.data.link.radiotap import RadiotapField as Data_Field - #: A field given to :meth:`Radiotap.make`: a data model, or a ``(type, mapping)`` pair. + #: A field given to :meth:`Radiotap.make` -- a data model, or a ``(type, mapping)`` pair. FieldEntry = Union[Data_Field, Tuple[Union[Enum_FieldType, int], Mapping[str, Any]]] - #: Parser of a field type registered as a callable: the type and the + #: Parser of a field type registered as a callable -- the type and the #: parsed schema, to the data model. FieldParser = Callable[[Enum_FieldType, Schema_Field], Data_Field] - #: Constructor of a field type registered as a callable: the type and a + #: Constructor of a field type registered as a callable -- the type and a #: data model or a mapping of its subfields, to the schema. FieldConstructor = Callable[[Enum_FieldType, Union[Data_Field, Mapping[str, Any]]], Schema_Field] __all__ = ['Radiotap'] -#: Bit 31 of an ``it_present`` word: set, another present-flags word +#: Bit 31 of an ``it_present`` word -- set, another present-flags word #: follows; clear, the field-data region starts right after this word. _IT_PRESENT_EXTENDED = 1 << 31 diff --git a/pcapkit/protocols/misc/checksum/ip_pseudo.py b/pcapkit/protocols/misc/checksum/ip_pseudo.py index a34904457b..f022a3d178 100644 --- a/pcapkit/protocols/misc/checksum/ip_pseudo.py +++ b/pcapkit/protocols/misc/checksum/ip_pseudo.py @@ -82,7 +82,7 @@ __all__ = ['IP_Pseudo', 'pseudo_header', 'verify_upper'] -#: Extension headers that end the chain, as the upper layer: the Mobility Header +#: Extension headers that end the chain, as the upper layer -- the Mobility Header #: (:rfc:`6275#section-6.1.1`) and HIP (:rfc:`7401#section-5.1.1`). _TERMINAL_HEADERS = (Enum_ExtensionHeader.Mobility_Header, Enum_ExtensionHeader.HIP) diff --git a/pcapkit/protocols/misc/pcap/frame.py b/pcapkit/protocols/misc/pcap/frame.py index 1cd21a8c8c..2c9add896b 100644 --- a/pcapkit/protocols/misc/pcap/frame.py +++ b/pcapkit/protocols/misc/pcap/frame.py @@ -132,7 +132,7 @@ class Frame(ProtocolBase[Data_Frame, Schema_Frame], }, ) # type: DefaultDict[Enum_LinkType | int, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] - #: What :attr:`__proto__` is keyed by: the enum a key must be a member of, + #: What :attr:`__proto__` is keyed by -- the enum a key must be a member of, #: and whether a bare :class:`int` is a valid key there too. Declared by the #: class *owning* the registry, and read from it by #: :func:`~pcapkit.foundation.registry.protocols.protocol.register_protocol_code` so a diff --git a/pcapkit/protocols/misc/pcapng.py b/pcapkit/protocols/misc/pcapng.py index c076c208d6..45c6536630 100644 --- a/pcapkit/protocols/misc/pcapng.py +++ b/pcapkit/protocols/misc/pcapng.py @@ -817,7 +817,7 @@ class PCAPNG(ProtocolBase[Data_PCAPNG, Schema_PCAPNG], }, ) # type: DefaultDict[Enum_LinkType | int, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] - #: What :attr:`__proto__` is keyed by: the enum a key must be a member of, + #: What :attr:`__proto__` is keyed by -- the enum a key must be a member of, #: and whether a bare :class:`int` is a valid key there too. Declared by the #: class *owning* the registry, and read from it by #: :func:`~pcapkit.foundation.registry.protocols.protocol.register_protocol_code` so a diff --git a/pcapkit/protocols/schema/application/dtls.py b/pcapkit/protocols/schema/application/dtls.py index 0bbe69f353..fba5012f90 100644 --- a/pcapkit/protocols/schema/application/dtls.py +++ b/pcapkit/protocols/schema/application/dtls.py @@ -71,7 +71,7 @@ class DTLSCiphertext(Schema): """ - #: Unified-header first octet: fixed prefix, ``C``, ``S``, ``L`` and ``EE``. + #: Unified-header first octet -- fixed prefix, ``C``, ``S``, ``L`` and ``EE``. flags: 'dict[str, int]' = BitField(length=1, namespace={ 'fixed': (0, 3), 'connection_id': (3, 1), diff --git a/pcapkit/protocols/schema/internet/igmpv3.py b/pcapkit/protocols/schema/internet/igmpv3.py index 9840dd0596..637b6cfaf1 100644 --- a/pcapkit/protocols/schema/internet/igmpv3.py +++ b/pcapkit/protocols/schema/internet/igmpv3.py @@ -116,7 +116,7 @@ class QueryExtension(Schema): length=lambda pkt: pkt['num_sources'] * 4, item_type=IPv4AddressField(), ) - #: Additional data (:rfc:`3376#section-4.1.10`): octets past the source + #: Additional data (:rfc:`3376#section-4.1.10`) -- octets past the source #: list that a received Query may carry and that must be kept (to verify #: the checksum) but not otherwise acted on. Empty for a #: standards-conformant Query, since :meth:`IGMPv3.make @@ -143,11 +143,11 @@ class Query(Schema): """ - #: Max response code; the raw octet :meth:`IGMPv3.read - #: ` decodes per + #: Max response code; the raw octet + #: :meth:`IGMPv3.read ` decodes per #: :rfc:`3376#section-4.1.1`. max_resp_code: 'int' = UInt8Field() - #: Checksum: the 16-bit one's complement of the one's complement sum of + #: Checksum. The 16-bit one's complement of the one's complement sum of #: the message, with the checksum field itself taken as zero and no #: pseudo-header involved -- the whole IP payload (:rfc:`3376#section-4.1.2`). checksum: 'bytes' = BytesField(length=2) diff --git a/pcapkit/protocols/schema/internet/mh.py b/pcapkit/protocols/schema/internet/mh.py index bd6533783d..f500f03504 100644 --- a/pcapkit/protocols/schema/internet/mh.py +++ b/pcapkit/protocols/schema/internet/mh.py @@ -150,7 +150,7 @@ class ANSIKeyLengthTest(TypedDict): """ - #: First length octet: the length itself, or ``0x80`` plus the number of + #: First length octet -- the length itself, or ``0x80`` plus the number of #: length octets that follow. len: int #: The two octets after it, which hold a long-form length; absent from a diff --git a/pcapkit/protocols/schema/link/gre.py b/pcapkit/protocols/schema/link/gre.py index eb3cd17dd3..6c0f4ef1f6 100644 --- a/pcapkit/protocols/schema/link/gre.py +++ b/pcapkit/protocols/schema/link/gre.py @@ -58,7 +58,7 @@ class GRE(Schema): 'reserved_2': (4, 9), 'version': (13, 3), }) - #: Protocol Type: the EtherType of the encapsulated payload. + #: Protocol Type -- the EtherType of the encapsulated payload. protocol_type: 'Enum_EtherType' = EnumField(length=2, namespace=Enum_EtherType) #: Checksum, present only when ``flags.checksum_present`` is set. checksum: 'Optional[int]' = ConditionalField( diff --git a/pcapkit/protocols/schema/misc/checksum/ip_pseudo.py b/pcapkit/protocols/schema/misc/checksum/ip_pseudo.py index 72a0a1e099..8588b3dc28 100644 --- a/pcapkit/protocols/schema/misc/checksum/ip_pseudo.py +++ b/pcapkit/protocols/schema/misc/checksum/ip_pseudo.py @@ -20,13 +20,13 @@ from pcapkit.corekit.fields.field import FieldBase as Field -#: Length of the IPv4 pseudo-header, :rfc:`793#section-3.1`: source, destination, +#: Length of the IPv4 pseudo-header, :rfc:`793#section-3.1` -- source, destination, #: zero, protocol and length. #: #: .. versionadded:: 1.6.0 IPV4_PSEUDO_LEN = 2 * (ipaddress.IPV4LENGTH // 8) + 1 + 1 + 2 -#: Length of the IPv6 pseudo-header, :rfc:`8200#section-8.1`: source, destination, +#: Length of the IPv6 pseudo-header, :rfc:`8200#section-8.1` -- source, destination, #: length, zero and next header. #: #: .. versionadded:: 1.6.0 diff --git a/pcapkit/protocols/schema/misc/pcapng.py b/pcapkit/protocols/schema/misc/pcapng.py index 2c33b7b7f5..a0f94830fb 100644 --- a/pcapkit/protocols/schema/misc/pcapng.py +++ b/pcapkit/protocols/schema/misc/pcapng.py @@ -1503,7 +1503,7 @@ def __init__(self, type: 'Enum_OptionType', length: 'int', speed: 'int') -> 'Non class IF_TSResolOption(_IF_Option, code=Enum_OptionType.if_tsresol): """Header schema for PCAP-NG file ``if_tsresol`` options.""" - #: Interface timestamp resolution, as the option encodes it: the base flag + #: Interface timestamp resolution, as the option encodes it -- the base flag #: and the exponent, from which :attr:`resolution` is computed. tsresol: 'ResolutionData' = BitField(length=1, namespace={ 'flag': (0, 1), diff --git a/pcapkit/protocols/schema/transport/dccp.py b/pcapkit/protocols/schema/transport/dccp.py index ead4e47d6b..37acf1779c 100644 --- a/pcapkit/protocols/schema/transport/dccp.py +++ b/pcapkit/protocols/schema/transport/dccp.py @@ -41,7 +41,7 @@ #: :rfc:`4340#section-5.8`. DCCP_SINGLE_OCTET_OPTION_MAX = 31 -#: Packet types carrying an Acknowledgement Number Subheader: every one +#: Packet types carrying an Acknowledgement Number Subheader -- every one #: :rfc:`4340#section-5` defines but DCCP-Request and DCCP-Data. DCCP-Listen #: (:rfc:`5596#section-2.2.1`) carries none either. ACK_TYPES = frozenset({ @@ -50,7 +50,7 @@ Enum_PacketType.DCCP_Sync, Enum_PacketType.DCCP_SyncAck, }) -#: Packet types carrying a Service Code: DCCP-Request, DCCP-Response and +#: Packet types carrying a Service Code -- DCCP-Request, DCCP-Response and #: DCCP-Listen (:rfc:`4340#section-5.2`, :rfc:`5596#section-2.2.1`). SERVICE_TYPES = frozenset({ Enum_PacketType.DCCP_Request, Enum_PacketType.DCCP_Response, Enum_PacketType.DCCP_Listen, @@ -360,8 +360,8 @@ class DCCP(Schema): BytesField(length=3), lambda pkt: pkt['type']['type'] == RESET_TYPE, ) - #: Options, cut short where the data is; :meth:`DCCP.read - #: ` refuses such a header. + #: Options, cut short where the data is; + #: :meth:`DCCP.read ` refuses such a header. options: 'list[Option]' = OptionField( length=lambda pkt: max(min(pkt['offset'] * 4 - header_length(pkt['type']), pkt['__length__']), 0), base_schema=Option, diff --git a/pcapkit/protocols/transport/dccp.py b/pcapkit/protocols/transport/dccp.py index 88a9114ab2..2a571e05a3 100644 --- a/pcapkit/protocols/transport/dccp.py +++ b/pcapkit/protocols/transport/dccp.py @@ -113,7 +113,7 @@ __all__ = ['DCCP'] -#: Longest a DCCP header can be: a Data Offset of 255 words, :rfc:`4340#section-5`. +#: Longest a DCCP header can be -- a Data Offset of 255 words, :rfc:`4340#section-5`. DCCP_MAX_HEADER_LEN = 1020 @@ -208,7 +208,7 @@ class DCCP(Transport[Data_DCCP, Schema_DCCP], lambda: ModuleDescriptor('pcapkit.protocols.misc.raw', 'Raw'), ) # type: DefaultDict[int, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] - #: What :attr:`__proto__` is keyed by: the enum a key must be a member of, + #: What :attr:`__proto__` is keyed by -- the enum a key must be a member of, #: and whether a bare :class:`int` is a valid key there too, as for #: :attr:`TCP.__proto_key__ `. __proto_key__: 'ClassVar[tuple[Type[EnumRegistry], bool]]' = (Enum_AppType, True) diff --git a/pcapkit/protocols/transport/sctp.py b/pcapkit/protocols/transport/sctp.py index 31fbaa66fd..4f934ee4a2 100644 --- a/pcapkit/protocols/transport/sctp.py +++ b/pcapkit/protocols/transport/sctp.py @@ -506,7 +506,7 @@ class SCTP(Transport[Data_SCTP, Schema_SCTP], }, ) # type: DefaultDict[int, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] - #: What :attr:`__proto__` is keyed by: the enum a key must be a member of, + #: What :attr:`__proto__` is keyed by -- the enum a key must be a member of, #: and whether a bare :class:`int` is a valid key there too. Declared by the #: class *owning* the registry, and read from it by #: :func:`~pcapkit.foundation.registry.protocols.protocol.register_protocol_code` so a diff --git a/pcapkit/protocols/transport/tcp.py b/pcapkit/protocols/transport/tcp.py index cdb8ab8cb9..4b47d0eb88 100644 --- a/pcapkit/protocols/transport/tcp.py +++ b/pcapkit/protocols/transport/tcp.py @@ -423,7 +423,7 @@ class TCP(Transport[Data_TCP, Schema_TCP], }, ) - #: What :attr:`__proto__` is keyed by: the enum a key must be a member of, + #: What :attr:`__proto__` is keyed by -- the enum a key must be a member of, #: and whether a bare :class:`int` is a valid key there too. Declared by the #: class *owning* the registry, and read from it by #: :func:`~pcapkit.foundation.registry.protocols.protocol.register_protocol_code` so a diff --git a/pcapkit/protocols/transport/udp.py b/pcapkit/protocols/transport/udp.py index 5b9b476b82..0eec33e816 100644 --- a/pcapkit/protocols/transport/udp.py +++ b/pcapkit/protocols/transport/udp.py @@ -142,7 +142,7 @@ class UDP(Transport[Data_UDP, Schema_UDP], }, ) - #: What :attr:`__proto__` is keyed by: the enum a key must be a member of, + #: What :attr:`__proto__` is keyed by -- the enum a key must be a member of, #: and whether a bare :class:`int` is a valid key there too. Declared by the #: class *owning* the registry, and read from it by #: :func:`~pcapkit.foundation.registry.protocols.protocol.register_protocol_code` so a diff --git a/pcapkit/utilities/warnings.py b/pcapkit/utilities/warnings.py index c5fa2c12d5..a065063af1 100644 --- a/pcapkit/utilities/warnings.py +++ b/pcapkit/utilities/warnings.py @@ -77,7 +77,7 @@ from typing_extensions import Self - #: A warning held back by :class:`hold_warnings`: its message, its category, + #: A warning held back by :class:`hold_warnings` -- its message, its category, #: and the frame it is attributed to, if that frame could be found. HeldWarning = Tuple[Union[str, Warning], Type[Warning], Optional[FrameType]] diff --git a/pcapkit/vendor/l2tp/type.py b/pcapkit/vendor/l2tp/type.py index 603ba89bf9..e99a2bb347 100644 --- a/pcapkit/vendor/l2tp/type.py +++ b/pcapkit/vendor/l2tp/type.py @@ -20,7 +20,7 @@ __all__ = ['Type'] -#: L2TP packet types, i.e. the ``T`` bit: :rfc:`2661` §3.1 sets it "to 0 for +#: L2TP packet types, i.e. the ``T`` bit. :rfc:`2661` §3.1 sets it "to 0 for #: a data message and 1 for a control message". DATA = { 0: 'Data', diff --git a/pcapkit/vendor/radiotap/field_type.py b/pcapkit/vendor/radiotap/field_type.py index 34337aa6d9..0c3dad015e 100644 --- a/pcapkit/vendor/radiotap/field_type.py +++ b/pcapkit/vendor/radiotap/field_type.py @@ -47,7 +47,7 @@ __all__ = ['FieldType'] -#: Link to radiotap.org's *Unofficially Used Fields* table: the bit number, +#: Link to radiotap.org's *Unofficially Used Fields* table -- the bit number, #: the field, the systems that generate it and those that parse it. UNOFFICIAL_LINK = 'https://www.radiotap.org/fields/unofficial' diff --git a/tests/project/test_doc_comment_first_line_2745_unit.py b/tests/project/test_doc_comment_first_line_2745_unit.py new file mode 100644 index 0000000000..f60ac7f69a --- /dev/null +++ b/tests/project/test_doc_comment_first_line_2745_unit.py @@ -0,0 +1,410 @@ +# -*- coding: utf-8 -*- +"""Pins that no doc comment's first line reads to napoleon as an inline type (#2745). + +Napoleon takes the first line of an attribute's documentation -- a ``#:`` doc +comment, the docstring under an assignment, or a property's docstring -- as +Google's inline ``type: description`` form wherever a colon outside a role or a +literal has text after it. The text before the colon then renders as a *Type* +field and drops out of the prose, and the build says nothing unless that text +happens to name several classes (#2743). + +* :class:`FirstLineTests` -- runs napoleon's own ``autodoc-process-docstring`` + handler, configured as :file:`docs/source/conf.py` configures it, over every + such documentation under :file:`pcapkit/`, and fails on a type napoleon reads + off a first line that is prose rather than a type expression. +* :class:`ReaderTests` -- the readers that verdict rests on. + +An inline type is kept where it is meant: ``#: int: Frame counter.`` renders +*Type:* ``int`` on purpose. A first line counts as one when the text before its +colon parses as a type expression -- a name, a dotted name, a union, a subscript +or a quoted forward reference -- and a bare name in it is one the module binds or +a builtin, since a single word of prose parses as a name too. An enum member is +left out, since ``restore_enum_member_docstring`` in :file:`conf.py` undoes any +type napoleon reads off one (#1775). + +The sweep reads the sources with :mod:`ast` and Sphinx's own attribute-comment +parser, and imports nothing it scans. It needs Sphinx for napoleon, and skips +without it. + +""" + +from __future__ import annotations + +import ast +import builtins +import functools +import importlib.util +import pathlib +import re +import sys +import types +import unittest +from typing import TYPE_CHECKING, NamedTuple + +if TYPE_CHECKING: + from typing import Any, Optional + +#: Test tier (#1958): reads the repository's sources, and imports nothing it scans. +__tier__ = 'LATEST' + +ROOT = pathlib.Path(__file__).resolve().parents[2] + +#: The package whose documentation is swept. +PACKAGE = ROOT / 'pcapkit' + +#: The configuration whose napoleon settings the sweep runs under. +CONF = ROOT / 'docs' / 'source' / 'conf.py' + +#: Napoleon ships with Sphinx; without it there is nothing to run. +HAS_SPHINX = importlib.util.find_spec('sphinx') is not None + +#: The modules whose classes are enumerations, wherever a base resolves to one. +ENUM_MODULES = frozenset({'enum', 'aenum'}) + +#: The decorators that make autodoc document a function as a property. +PROPERTY_DECORATORS = frozenset({'property', 'cached_property'}) + +#: Top-level names a fully qualified type may start from without the module binding them. +QUALIFIED_ROOTS = frozenset(getattr(sys, 'stdlib_module_names', ())) | {'pcapkit'} + +#: The ``~`` that shortens a cross-reference's displayed name, as in ``~typing.Type``. +TILDE = re.compile(r'~(?=[A-Za-z_])') + + +class Documentation(NamedTuple): + """One attribute's documentation, as autodoc hands it to napoleon.""" + + #: The module's path, relative to :data:`ROOT`. + path: 'str' + #: The attribute's name, qualified by its class where it has one. + qualname: 'str' + #: What autodoc documents it as: ``attribute``, ``data`` or ``property``. + what: 'str' + #: Its lines. + lines: 'list[str]' + + @property + def key(self) -> 'str': + """How a failure names it.""" + return f'{self.path}: {self.qualname}' + + +def napoleon_config() -> 'Any': + """Napoleon's configuration, from the ``napoleon_*`` assignments in :data:`CONF`.""" + from sphinx.ext.napoleon import Config # pylint: disable=import-outside-toplevel + + settings = {} # type: dict[str, Any] + for node in ast.parse(CONF.read_text(encoding='utf-8')).body: + if (isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) + and node.targets[0].id.startswith('napoleon_')): + settings[node.targets[0].id] = ast.literal_eval(node.value) + return Config(**settings) + + +def inline_type(documentation: 'Documentation', config: 'Any') -> 'Optional[str]': + """The inline type napoleon reads off ``documentation``, or :data:`None` where it reads none. + + Runs the handler napoleon connects to ``autodoc-process-docstring`` itself, + so the reading is napoleon's rather than a copy of its rule. A ``:type:`` + field the documentation writes for itself is no reading. + """ + from sphinx.ext.napoleon import _process_docstring # pylint: disable=import-outside-toplevel + + if any(line.startswith(':type:') for line in documentation.lines): + return None + result = list(documentation.lines) + _process_docstring(types.SimpleNamespace(config=config), documentation.what, # type: ignore[arg-type] + documentation.qualname, None, None, result) + return next((line[len(':type:'):].strip() for line in result if line.startswith(':type:')), None) + + +def module_name(path: 'pathlib.Path') -> 'str': + """The dotted name of the module at ``path``.""" + parts = path.relative_to(ROOT).with_suffix('').parts + return '.'.join(parts[:-1] if parts[-1] == '__init__' else parts) + + +class Module: + """What the sweep needs of one module: its tree, its bindings and its classes.""" + + def __init__(self, path: 'pathlib.Path') -> 'None': + self.path = path + self.name = module_name(path) + self.source = path.read_text(encoding='utf-8') + self.tree = ast.parse(self.source, str(path)) + package = self.name if path.name == '__init__.py' else self.name.rpartition('.')[0] + + #: Each name an import binds, to the dotted name it binds. + self.imports = {} # type: dict[str, str] + #: Every name the module binds anywhere, imported or defined. + self.bound = set() # type: set[str] + for node in ast.walk(self.tree): + if isinstance(node, ast.Import): + for alias in node.names: + local = alias.asname or alias.name.partition('.')[0] + self.imports[local] = alias.name if alias.asname else local + elif isinstance(node, ast.ImportFrom): + base = node.module or '' + if node.level: + anchor = package.split('.')[:len(package.split('.')) - node.level + 1] + base = '.'.join(anchor + ([base] if base else [])) + for alias in node.names: + self.imports[alias.asname or alias.name] = f'{base}.{alias.name}' + elif isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + self.bound.add(node.name) + elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store): + self.bound.add(node.id) + self.bound.update(self.imports) + + #: Every class, by its qualified name within the module. + self.classes = {} # type: dict[str, ast.ClassDef] + todo = [('', self.tree)] # type: list[tuple[str, ast.AST]] + while todo: + prefix, parent = todo.pop() + for node in ast.iter_child_nodes(parent): + if isinstance(node, ast.ClassDef): + self.classes[prefix + node.name] = node + todo.append((f'{prefix}{node.name}.', node)) + elif not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + todo.append((prefix, node)) + + @functools.cached_property + def documentation(self) -> 'list[Documentation]': + """Every attribute and property documentation autodoc would read from the module.""" + from sphinx.pycode import ModuleAnalyzer # pylint: disable=import-outside-toplevel + + path = self.path.relative_to(ROOT).as_posix() + found = [Documentation(path, f'{namespace}.{name}' if namespace else name, + 'attribute' if namespace else 'data', list(lines)) + for (namespace, name), lines + in ModuleAnalyzer.for_string(self.source, self.name).find_attr_docs().items()] + for qualname, cls in self.classes.items(): + for node in cls.body: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and is_property(node): + docstring = ast.get_docstring(node) + if docstring: + found.append(Documentation(path, f'{qualname}.{node.name}', 'property', + docstring.splitlines())) + return found + + +def is_property(node: 'ast.FunctionDef | ast.AsyncFunctionDef') -> 'bool': + """Whether ``node`` is decorated as a property; its setter and deleter are not.""" + for decorator in node.decorator_list: + name = decorator.attr if isinstance(decorator, ast.Attribute) else getattr(decorator, 'id', None) + if name in PROPERTY_DECORATORS: + return True + return False + + +def dotted(node: 'ast.expr') -> 'Optional[str]': + """The dotted name ``node`` spells, through a subscript, or :data:`None`.""" + if isinstance(node, ast.Subscript): + return dotted(node.value) + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + head = dotted(node.value) + return None if head is None else f'{head}.{node.attr}' + return None + + +class Package: + """Every module under :data:`PACKAGE`, and which of their classes are enumerations.""" + + def __init__(self) -> 'None': + self.modules = {} # type: dict[str, Module] + for path in sorted(PACKAGE.rglob('*.py')): + module = Module(path) + self.modules[module.name] = module + + def is_enum(self, module: 'str', qualname: 'str', seen: 'Optional[set[tuple[str, str]]]' = None) -> 'bool': + """Whether the class ``qualname`` of ``module`` has an enumeration among its bases.""" + seen = set() if seen is None else seen + if (module, qualname) in seen: + return False + seen.add((module, qualname)) + node = self.modules[module].classes.get(qualname) if module in self.modules else None + return node is not None and any(self.names_enum(module, name, seen) + for name in map(dotted, node.bases) if name is not None) + + def names_enum(self, module: 'str', name: 'str', seen: 'set[tuple[str, str]]') -> 'bool': + """Whether ``name``, as ``module`` spells it, is an enumeration class.""" + if name in self.modules[module].classes: + return self.is_enum(module, name, seen) + head, _, rest = name.partition('.') + target = self.modules[module].imports.get(head) + if target is None: + return False + target = f'{target}.{rest}' if rest else target + if target.partition('.')[0] in ENUM_MODULES: + return True + parts = target.split('.') + for split in range(len(parts) - 1, 0, -1): + owner = '.'.join(parts[:split]) + if owner in self.modules: + return self.names_enum(owner, '.'.join(parts[split:]), seen) + return False + + def is_enum_member(self, module: 'str', documentation: 'Documentation') -> 'bool': + """Whether ``documentation`` is an enum member's, which :file:`conf.py` restores.""" + owner, _, member = documentation.qualname.rpartition('.') + if documentation.what != 'attribute' or member.startswith('__') or ( + member.startswith('_') and member.endswith('_')): + return False + return self.is_enum(module, owner) + + +def is_type_expression(text: 'str', bound: 'set[str]') -> 'bool': + """Whether ``text`` is a type expression, its bare names held by ``bound`` or the builtins.""" + try: + tree = ast.parse(TILDE.sub('', text.strip()), mode='eval').body + except SyntaxError: + return False + if isinstance(tree, ast.Constant) and isinstance(tree.value, str): + return is_type_expression(tree.value, bound) + return _is_type(tree, bound) + + +def _is_type(node: 'ast.expr', bound: 'Optional[set[str]]') -> 'bool': + """Whether ``node`` is a name, a dotted name, a union or a subscript of a type. + + A bare name, and a dotted name's root, is one that ``bound`` or the builtins + hold, or for a dotted name one of :data:`QUALIFIED_ROOTS`, since a single word + of prose parses as a name too. Prose never reaches a subscript, so a + subscript's names need no binding: there ``bound`` is :data:`None`. + """ + if isinstance(node, ast.Name): + return bound is None or node.id in bound or hasattr(builtins, node.id) + if isinstance(node, ast.Attribute): + name = dotted(node) + roots = None if bound is None else bound | QUALIFIED_ROOTS + return name is not None and _is_type(ast.Name(id=name.partition('.')[0]), roots) + if isinstance(node, ast.Subscript): + inner = node.slice + items = inner.elts if isinstance(inner, ast.Tuple) else [inner] + return dotted(node.value) is not None and all(_is_argument(item) for item in items) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): + return _is_type(node.left, bound) and _is_type(node.right, bound) + return isinstance(node, ast.Constant) and node.value is None + + +def _is_argument(node: 'ast.expr') -> 'bool': + """Whether ``node`` can stand in a type's subscript: a type, a literal or a list of either.""" + if isinstance(node, ast.Constant): + return True + if isinstance(node, ast.List): + return all(_is_argument(item) for item in node.elts) + return _is_type(node, None) + + +def inline_types(package: 'Package', config: 'Any') -> 'tuple[dict[str, str], dict[str, str]]': + """Every inline type napoleon reads off a first line, by :attr:`Documentation.key`. + + Returns: + The types that are prose, and the types that are type expressions. An enum + member's are in neither. + + """ + misread, typed = {}, {} # type: dict[str, str], dict[str, str] + for name, module in package.modules.items(): + for documentation in module.documentation: + text = inline_type(documentation, config) + if text is None or package.is_enum_member(name, documentation): + continue + (typed if is_type_expression(text, module.bound) else misread)[documentation.key] = text + return misread, typed + + +@unittest.skipUnless(HAS_SPHINX, 'Sphinx is not installed') +class FirstLineTests(unittest.TestCase): + """No first line of an attribute's documentation renders as a *Type* field.""" + + maxDiff = None + package = None # type: Optional[Package] + found = {} # type: dict[str, str] + typed = {} # type: dict[str, str] + + @classmethod + def setUpClass(cls) -> 'None': + cls.package = Package() + cls.found, cls.typed = inline_types(cls.package, napoleon_config()) + + def test_the_sweep_reads_documentation(self) -> 'None': + """The sweep reads ``#:`` comments and properties, and napoleon types some on purpose.""" + assert self.package is not None + kinds = {documentation.what for module in self.package.modules.values() + for documentation in module.documentation} + self.assertEqual(kinds, {'attribute', 'data', 'property'}) + self.assertTrue(self.typed, 'napoleon read no inline type at all, so the sweep reads nothing') + + def test_enum_classes(self) -> 'None': + """A class is an enumeration through :mod:`enum`, ``aenum`` or a module re-exporting one.""" + assert self.package is not None + self.assertTrue(self.package.is_enum('pcapkit.corekit.checksum.internet', 'ChecksumStatus')) + self.assertTrue(self.package.is_enum('pcapkit.foundation.reassembly.data.data', 'Completion')) + self.assertTrue(self.package.is_enum('pcapkit.const.sctp.payload_protocol_identifier', + 'PayloadProtocolIdentifier')) + self.assertFalse(self.package.is_enum('pcapkit.protocols.data.internet.igmp', 'IGMP')) + + def test_no_first_line_reads_as_a_type(self) -> 'None': + """A misread first line names its documentation and the *Type* it renders.""" + off = sorted(f'{key}: renders Type: {text!r}' for key, text in self.found.items()) + self.assertEqual(off, [], 'keep the colon off the first line, e.g. "Checksum. The 16-bit ..."') + + +@unittest.skipUnless(HAS_SPHINX, 'Sphinx is not installed') +class ReaderTests(unittest.TestCase): + """The readers :class:`FirstLineTests` rests on.""" + + config = napoleon_config() if HAS_SPHINX else None + + def read(self, first: 'str', what: 'str' = 'attribute') -> 'Optional[str]': + """The inline type napoleon reads off a documentation opening on ``first``.""" + return inline_type(Documentation('pcapkit/x.py', 'X.x', what, [first, 'the rest of it.', '']), self.config) + + def test_napoleon_reads_a_first_line_colon_as_a_type(self) -> 'None': + """The text before the colon is the type, for every kind of documentation.""" + for what in ('attribute', 'data', 'property'): + self.assertEqual(self.read('Group address: the IPv4 multicast group', what), 'Group address') + self.assertEqual(self.read('int: Frame counter.'), 'int') + + def test_napoleon_reads_no_type_off_these(self) -> 'None': + """A colon on a later line, at the end, doubled or inside a role or literal is no type.""" + self.assertIsNone(self.read('Checksum. The 16-bit one\'s complement')) + self.assertIsNone(self.read('Group address -- the IPv4 multicast group')) + self.assertIsNone(self.read('The fields, in order:')) + self.assertIsNone(self.read('See :rfc:`3376#section-4.1.2` for the sum')) + self.assertIsNone(self.read('The ``a:b`` pair')) + self.assertIsNone(self.read('A scope :: of its own')) + + def test_napoleon_reads_a_role_split_across_lines(self) -> 'None': + """A role whose closing backtick is on the next line is no role on the first.""" + self.assertEqual(self.read('Max response code; the raw octet :meth:`IGMPv3.read'), + 'Max response code; the raw octet') + + def test_type_expressions(self) -> 'None': + """Bound names, builtins, qualified names, unions, subscripts and forward references are types.""" + bound = {'Optional', 'Sentinel', 'logging'} + for text in ('int', 'logging.Logger', 'Optional[str]', "Literal['little', 'big']", + '~typing.Type[Dumper]', "'Sentinel | None'", 'dict[str, ~pcapkit.protocols.Protocol]', + 'Optional[int] | str', 'tuple[int, ...]', 'Callable[[int], object]', 'Pattern[str]', + 're.Pattern', 'Sentinel | None'): + self.assertTrue(is_type_expression(text, bound), text) + + def test_prose_is_no_type_expression(self) -> 'None': + """Prose, an unbound word and a dotted name rooted nowhere are not types.""" + for text in ('Group address', 'Checksum', 'Version of IGMP, always ``3``. It is on no octet', + 'Max response code; the raw octet', 'i.e', 'Additional data (:rfc:`3376`)', + 'Checksum | None', 'Data link type'): + self.assertFalse(is_type_expression(text, {'Optional'}), text) + + def test_conf_py_restores_enum_members(self) -> 'None': + """The enum-member exclusion stands only while :file:`conf.py` undoes napoleon there.""" + text = CONF.read_text(encoding='utf-8') + self.assertIn("'sphinx.ext.napoleon'", text) + self.assertIn("app.connect('autodoc-process-docstring', restore_enum_member_docstring", text) + + +if __name__ == '__main__': + unittest.main()