diff --git a/docs/source/pcapkit/const/ftp.rst b/docs/source/pcapkit/const/ftp.rst index 531655423..3f8e9c4cf 100644 --- a/docs/source/pcapkit/const/ftp.rst +++ b/docs/source/pcapkit/const/ftp.rst @@ -26,6 +26,21 @@ companion :class:`~pcapkit.const.ftp.command.FEATCode` enumeration it also decla the ``FEAT`` response keywords the ``FEAT code`` column of the same IANA registry names, c.f., :rfc:`5797#section-3`. +.. versionchanged:: 1.5.0 + + Neither ``Command._missing_`` nor ``Command.get()`` mints a member + (:issue:`860`): only IANA-registered values are members, and a new one is + created through :meth:`~pcapkit.const.ftp.command.Command.register`, which + can supply the ``feat``, ``desc``, ``type`` and ``conf`` a bare wire command + word carries none of. A minted member had each of them at its default. + + ``Command.get()`` refuses a non-:class:`str` ``key`` with + :exc:`~pcapkit.utilities.exceptions.EnumValueError`, with or without + ``default`` (:issue:`1824`). ``get(42)`` and ``get(None)`` raised + :exc:`AttributeError` from ``key.upper()``, and ``get(b'ABOR')`` returned an + unregistered member whose name and value were both the :class:`bytes` + object. + .. autoclass:: pcapkit.const.ftp.command.Command :members: :undoc-members: diff --git a/docs/source/pcapkit/const/http.rst b/docs/source/pcapkit/const/http.rst index 137ecb85c..144baff69 100644 --- a/docs/source/pcapkit/const/http.rst +++ b/docs/source/pcapkit/const/http.rst @@ -55,6 +55,21 @@ HTTP Method This module contains the constant enumeration for **HTTP Method**, which is automatically generated from :class:`pcapkit.vendor.http.method.Method`. +.. versionchanged:: 1.5.0 + + Neither ``Method._missing_`` nor ``Method.get()`` mints a member + (:issue:`860`): only IANA-registered values are members, and a new one is + created through :meth:`~pcapkit.corekit.enum.EnumRegistry.register`, which + can supply the ``safe`` and ``idempotent`` a bare wire method verb carries + neither of. A member's :class:`str` payload is its value, so + ``Method.GET == 'GET'`` (:issue:`870`). + + ``Method.get()`` matches ``key`` case-sensitively, as :rfc:`9110` makes the + method token (:issue:`896`), and checks it against member values as well as + names (:issue:`908`). A non-:class:`str` ``key`` raises :exc:`ValueError` + (:issue:`908`): ``get(42)`` and ``get(None)`` raised :exc:`AttributeError` + from ``key.upper()``, and ``get(b'GET')`` raised :exc:`TypeError`. + .. autoclass:: pcapkit.const.http.method.Method :members: :undoc-members: diff --git a/docs/source/pcapkit/const/ipv6.rst b/docs/source/pcapkit/const/ipv6.rst index b0db583ca..d25a7d2c8 100644 --- a/docs/source/pcapkit/const/ipv6.rst +++ b/docs/source/pcapkit/const/ipv6.rst @@ -37,6 +37,11 @@ IPv6 Extension Header Types This module contains the constant enumeration for **IPv6 Extension Header Types**, which is automatically generated from :class:`pcapkit.vendor.ipv6.extension_header.ExtensionHeader`. +.. versionchanged:: 1.5.0 + + Generated from IANA's *IPv6 Extension Header Types* registry, where it was + generated from the protocol-numbers registry (:issue:`925`). + .. autoclass:: pcapkit.const.ipv6.extension_header.ExtensionHeader :members: :undoc-members: diff --git a/docs/source/pcapkit/const/pcapng.rst b/docs/source/pcapkit/const/pcapng.rst index 7291f614c..8e8c5b62a 100644 --- a/docs/source/pcapkit/const/pcapng.rst +++ b/docs/source/pcapkit/const/pcapng.rst @@ -61,6 +61,11 @@ Option Types This module contains the constant enumeration for **Option Types**, which is automatically generated from :class:`pcapkit.vendor.pcapng.option_type.OptionType`. +.. versionchanged:: 1.1.0 + + A member converts with :class:`int` to its option code, and compares, + orders and hashes as that code. + .. autoclass:: pcapkit.const.pcapng.option_type.OptionType :members: :undoc-members: @@ -126,6 +131,10 @@ TLS Key Log Labels This module contains the constant enumeration for **TLS Key Log Labels**, which is automatically generated from :class:`pcapkit.vendor.pcapng.tls_key_label.TLSKeyLabel`. +.. versionadded:: 1.5.0 + + Moved here from ``pcapkit.protocols.misc.pcapng`` (:issue:`886`). + .. autoclass:: pcapkit.const.pcapng.tls_key_label.TLSKeyLabel :members: :undoc-members: diff --git a/docs/source/pcapkit/const/reg.rst b/docs/source/pcapkit/const/reg.rst index 66f0be84b..fb2090432 100644 --- a/docs/source/pcapkit/const/reg.rst +++ b/docs/source/pcapkit/const/reg.rst @@ -67,13 +67,38 @@ Numbers**, which are automatically generated from :mod:`pcapkit.vendor.reg.appty IANA keys every assignment on a ``(service, port, transport)`` triple, so the registry is one enumeration per transport protocol sharing a memberless base. +.. versionchanged:: 1.5.0 + + Split by transport protocol (:issue:`732`): :mod:`pcapkit.const.reg.apptype` + is a package, and every assignment is a member of one of the per-transport + registries rather than of :class:`~pcapkit.const.reg.apptype.apptype.AppType`. + ``get()`` raises :exc:`ValueError` for a port outside ``0``-``65535`` + (:issue:`758`), and returns an unregistered member rather than minting one + for a port the registry does not assign (:issue:`860`). + .. module:: pcapkit.const.reg.apptype.apptype +.. versionchanged:: 1.5.0 + + :class:`~pcapkit.const.reg.apptype.apptype.TransportProtocol` is an + ``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`). + .. autoclass:: pcapkit.const.reg.apptype.apptype.TransportProtocol :members: :undoc-members: :show-inheritance: +.. versionchanged:: 1.1.1 + + An :class:`~pcapkit.const.reg.apptype.apptype.AppType` member converts with + :class:`int` to its port number, and compares, orders and hashes as that + port. + .. autoclass:: pcapkit.const.reg.apptype.apptype.AppType :members: :undoc-members: diff --git a/docs/source/pcapkit/const/tcp.rst b/docs/source/pcapkit/const/tcp.rst index 940e7875e..9492a0e38 100644 --- a/docs/source/pcapkit/const/tcp.rst +++ b/docs/source/pcapkit/const/tcp.rst @@ -66,6 +66,11 @@ TCP Header Flags This module contains the constant enumeration for **TCP Header Flags**, which is automatically generated from :class:`pcapkit.vendor.tcp.flags.Flags`. +.. versionchanged:: 1.5.0 + + A value outside the 16-bit field raises :exc:`ValueError`, where + :mod:`aenum` composed a pseudo-member for any integer. + .. autoclass:: pcapkit.const.tcp.flags.Flags :members: :undoc-members: diff --git a/pcapkit/const/ftp/command.py b/pcapkit/const/ftp/command.py index 9c3ac9e77..b4328e6be 100644 --- a/pcapkit/const/ftp/command.py +++ b/pcapkit/const/ftp/command.py @@ -264,20 +264,7 @@ class defines neither ``get`` nor ``_missing_`` of its own to reconcile class Command(EnumRegistry, StrEnum): - """[Command] FTP Command - - .. versionchanged:: 1.5.0 - - Neither ``_missing_`` nor ``get()`` mints a member. The owner ruled on - GitHub issue :issue:`860` that ``get`` should not mint: only IANA-registered - values are legitimate members, a new one is properly created through - :meth:`register`, and ``get`` is not given enough information to create - one. Concretely true here -- a bare wire command word carries no - :attr:`feat`/:attr:`desc`/:attr:`type`/:attr:`conf`, so a minted member - had each of them hollowed out to its default; :meth:`register` is the - path that can actually supply them. - - """ + """[Command] FTP Command""" if TYPE_CHECKING: #: Feature code. Keyword returned in FEAT response line for this command/extension, @@ -589,15 +576,6 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> 'Command': *registered* value. A refused ``key`` therefore raises whatever ``default`` is. - .. versionchanged:: 1.5.0 - - A non-:class:`str` ``key`` raises - :exc:`~pcapkit.utilities.exceptions.EnumValueError`, with or - without ``default`` (:issue:`1824`). ``get(42)`` and ``get(None)`` - raised :exc:`AttributeError` from ``key.upper()``, and - ``get(b'ABOR')`` returned an unregistered member whose name and - value were both the :class:`bytes` object. - Args: key: Key to get enum item. Looked up case-insensitively, as :rfc:`959#section-5` treats FTP command codes, against member diff --git a/pcapkit/const/http/method.py b/pcapkit/const/http/method.py index c6ebd4a04..e6add5407 100644 --- a/pcapkit/const/http/method.py +++ b/pcapkit/const/http/method.py @@ -23,21 +23,7 @@ __all__ = ['Method'] class Method(EnumRegistry, StrEnum): - """[Method] HTTP Method - - .. versionchanged:: 1.5.0 - - Neither ``_missing_`` nor ``get()`` mints a member, per the owner's - ruling on GitHub issue :issue:`860`: only IANA-registered values are - legitimate members, and a new one is created through :meth:`register`, - which can supply :attr:`safe` and :attr:`idempotent` where a bare wire - method verb carries neither. ``get()`` matches ``key`` case-sensitively, - as :rfc:`9110` makes the method token (:issue:`896`), and checks it - against member values as well as names (:issue:`908`). A member's - :class:`str` payload is its value, so ``Method.GET == 'GET'`` - (:issue:`870`). - - """ + """[Method] HTTP Method""" if TYPE_CHECKING: #: Safe method. @@ -246,13 +232,6 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> 'Method': this override's ``default`` supplies the *value* of the unregistered member it builds on a miss. - .. versionchanged:: 1.5.0 - - A non-:class:`str` ``key`` raises :exc:`ValueError` - (:issue:`908`). ``get(42)`` and ``get(None)`` raised - :exc:`AttributeError` from ``key.upper()``, and ``get(b'GET')`` - raised :exc:`TypeError`. - Args: key: Key to get enum item. Looked up case-**sensitively**, per :rfc:`9110#section-9.1` -- the method token is diff --git a/pcapkit/const/ipv6/extension_header.py b/pcapkit/const/ipv6/extension_header.py index af093ad42..6afddb939 100644 --- a/pcapkit/const/ipv6/extension_header.py +++ b/pcapkit/const/ipv6/extension_header.py @@ -24,14 +24,7 @@ class ExtensionHeader(EnumRegistry, IntEnum): - """[ExtensionHeader] IPv6 Extension Header Types - - .. versionchanged:: 1.5.0 - - Generated from IANA's *IPv6 Extension Header Types* registry, where it - was generated from the protocol-numbers registry (:issue:`925`). - - """ + """[ExtensionHeader] IPv6 Extension Header Types""" #: HOPOPT, IPv6 Hop-by-Hop Option [:rfc:`8200`] HOPOPT = 0 diff --git a/pcapkit/const/pcapng/option_type.py b/pcapkit/const/pcapng/option_type.py index af3ccf4d1..ddc2db43b 100644 --- a/pcapkit/const/pcapng/option_type.py +++ b/pcapkit/const/pcapng/option_type.py @@ -25,14 +25,7 @@ class OptionType(EnumRegistry, StrEnum): - """[OptionType] Option Types - - .. versionchanged:: 1.1.0 - - A member converts with :class:`int` to its option code, and compares, - orders and hashes as that code. - - """ + """[OptionType] Option Types""" if TYPE_CHECKING: #: Short name of the option type. diff --git a/pcapkit/const/pcapng/tls_key_label.py b/pcapkit/const/pcapng/tls_key_label.py index 6ce78fbd7..82663e65f 100644 --- a/pcapkit/const/pcapng/tls_key_label.py +++ b/pcapkit/const/pcapng/tls_key_label.py @@ -24,13 +24,7 @@ class TLSKeyLabel(EnumRegistry, StrEnum): - """[TLSKeyLabel] TLS Key Log Labels - - .. versionadded:: 1.5.0 - - Moved here from ``pcapkit.protocols.misc.pcapng`` (:issue:`886`). - - """ + """[TLSKeyLabel] TLS Key Log Labels""" #: NSS-historical: not a registered label of :rfc:`9850#section-4.2`'s "TLS #: SSLKEYLOGFILE Labels" registry. Defined by the Mozilla NSS diff --git a/pcapkit/const/reg/apptype/apptype.py b/pcapkit/const/reg/apptype/apptype.py index 38c1d58da..99981c4f7 100644 --- a/pcapkit/const/reg/apptype/apptype.py +++ b/pcapkit/const/reg/apptype/apptype.py @@ -28,18 +28,7 @@ class TransportProtocol(EnumLookup, IntEnum): - """Transport layer protocol. - - .. versionchanged:: 1.5.0 - - An ``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`). :meth:`get` refuses a name no member - declares, with :exc:`~pcapkit.utilities.exceptions.EnumKeyError`, - rather than minting a member for it (:issue:`808`, :issue:`923`). - - """ + """Transport layer protocol.""" # mypy has no aenum plugin, so this class is a plain class to it: every # member below is cast rather than left as a bare auto()-valued one -- @@ -234,20 +223,6 @@ class AppType(EnumRegistry, StrEnum): makes it subclassable at all, since :mod:`aenum` refuses to extend an enumeration that already has members. - .. versionchanged:: 1.1.1 - - A member converts with :class:`int` to its port number, and compares, - orders and hashes as that port. - - .. versionchanged:: 1.5.0 - - Split by transport protocol (:issue:`732`): :mod:`pcapkit.const.reg.apptype` - is a package, and every assignment is a member of one of the per-transport - registries rather than of this class. ``get()`` raises - :exc:`ValueError` for a port outside ``0``-``65535`` (:issue:`758`), and - returns an unregistered member rather than minting one for a port the - registry does not assign (:issue:`860`). - Note: The rows below are the registry entries whose **transport protocol column is empty**. A blank transport is not an assignment to anything, so there diff --git a/pcapkit/const/tcp/flags.py b/pcapkit/const/tcp/flags.py index 5dfa4b369..e0a7f583d 100644 --- a/pcapkit/const/tcp/flags.py +++ b/pcapkit/const/tcp/flags.py @@ -23,14 +23,7 @@ __all__ = ['Flags'] class Flags(EnumRegistry, IntFlag): - """[Flags] TCP Header Flags - - .. versionchanged:: 1.5.0 - - A value outside the 16-bit field raises :exc:`ValueError`, where - :mod:`aenum` composed a pseudo-member for any integer. - - """ + """[Flags] TCP Header Flags""" #: Reserved for future use [:rfc:`9293`] Reserved_4 = 1 << 4 diff --git a/pcapkit/vendor/ftp/command.py b/pcapkit/vendor/ftp/command.py index 83cbefc1d..745ccdc78 100644 --- a/pcapkit/vendor/ftp/command.py +++ b/pcapkit/vendor/ftp/command.py @@ -276,20 +276,7 @@ class defines neither ``get`` nor ``_missing_`` of its own to reconcile class {NAME}(EnumRegistry, StrEnum): - """[{NAME}] {DOCS} - - .. versionchanged:: 1.5.0 - - Neither ``_missing_`` nor ``get()`` mints a member. The owner ruled on - GitHub issue :issue:`860` that ``get`` should not mint: only IANA-registered - values are legitimate members, a new one is properly created through - :meth:`register`, and ``get`` is not given enough information to create - one. Concretely true here -- a bare wire command word carries no - :attr:`feat`/:attr:`desc`/:attr:`type`/:attr:`conf`, so a minted member - had each of them hollowed out to its default; :meth:`register` is the - path that can actually supply them. - - """ + """[{NAME}] {DOCS}""" if TYPE_CHECKING: #: Feature code. Keyword returned in FEAT response line for this command/extension, @@ -423,15 +410,6 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}': *registered* value. A refused ``key`` therefore raises whatever ``default`` is. - .. versionchanged:: 1.5.0 - - A non-:class:`str` ``key`` raises - :exc:`~pcapkit.utilities.exceptions.EnumValueError`, with or - without ``default`` (:issue:`1824`). ``get(42)`` and ``get(None)`` - raised :exc:`AttributeError` from ``key.upper()``, and - ``get(b'ABOR')`` returned an unregistered member whose name and - value were both the :class:`bytes` object. - Args: key: Key to get enum item. Looked up case-insensitively, as :rfc:`959#section-5` treats FTP command codes, against member diff --git a/pcapkit/vendor/http/method.py b/pcapkit/vendor/http/method.py index ba63c0042..5d66030f2 100644 --- a/pcapkit/vendor/http/method.py +++ b/pcapkit/vendor/http/method.py @@ -47,21 +47,7 @@ __all__ = ['{NAME}'] class {NAME}(EnumRegistry, StrEnum): - """[{NAME}] {DOCS} - - .. versionchanged:: 1.5.0 - - Neither ``_missing_`` nor ``get()`` mints a member, per the owner's - ruling on GitHub issue :issue:`860`: only IANA-registered values are - legitimate members, and a new one is created through :meth:`register`, - which can supply :attr:`safe` and :attr:`idempotent` where a bare wire - method verb carries neither. ``get()`` matches ``key`` case-sensitively, - as :rfc:`9110` makes the method token (:issue:`896`), and checks it - against member values as well as names (:issue:`908`). A member's - :class:`str` payload is its value, so ``Method.GET == 'GET'`` - (:issue:`870`). - - """ + """[{NAME}] {DOCS}""" if TYPE_CHECKING: #: Safe method. @@ -151,13 +137,6 @@ def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}': this override's ``default`` supplies the *value* of the unregistered member it builds on a miss. - .. versionchanged:: 1.5.0 - - A non-:class:`str` ``key`` raises :exc:`ValueError` - (:issue:`908`). ``get(42)`` and ``get(None)`` raised - :exc:`AttributeError` from ``key.upper()``, and ``get(b'GET')`` - raised :exc:`TypeError`. - Args: key: Key to get enum item. Looked up case-**sensitively**, per :rfc:`9110#section-9.1` -- the method token is diff --git a/pcapkit/vendor/ipv6/extension_header.py b/pcapkit/vendor/ipv6/extension_header.py index 15c0e6eec..0308c532e 100644 --- a/pcapkit/vendor/ipv6/extension_header.py +++ b/pcapkit/vendor/ipv6/extension_header.py @@ -50,14 +50,7 @@ class {NAME}(EnumRegistry, IntEnum): - """[{NAME}] {DOCS} - - .. versionchanged:: 1.5.0 - - Generated from IANA's *IPv6 Extension Header Types* registry, where it - was generated from the protocol-numbers registry (:issue:`925`). - - """ + """[{NAME}] {DOCS}""" {ENUM} diff --git a/pcapkit/vendor/pcapng/option_type.py b/pcapkit/vendor/pcapng/option_type.py index fcf1dd67a..cf8af1945 100644 --- a/pcapkit/vendor/pcapng/option_type.py +++ b/pcapkit/vendor/pcapng/option_type.py @@ -53,14 +53,7 @@ class {NAME}(EnumRegistry, StrEnum): - """[{NAME}] {DOCS} - - .. versionchanged:: 1.1.0 - - A member converts with :class:`int` to its option code, and compares, - orders and hashes as that code. - - """ + """[{NAME}] {DOCS}""" if TYPE_CHECKING: #: Short name of the option type. diff --git a/pcapkit/vendor/pcapng/tls_key_label.py b/pcapkit/vendor/pcapng/tls_key_label.py index 7c04fa748..916194cd9 100644 --- a/pcapkit/vendor/pcapng/tls_key_label.py +++ b/pcapkit/vendor/pcapng/tls_key_label.py @@ -64,13 +64,7 @@ class {NAME}(EnumRegistry, StrEnum): - """[{NAME}] {DOCS} - - .. versionadded:: 1.5.0 - - Moved here from ``pcapkit.protocols.misc.pcapng`` (:issue:`886`). - - """ + """[{NAME}] {DOCS}""" {ENUM} diff --git a/pcapkit/vendor/reg/apptype/apptype.py b/pcapkit/vendor/reg/apptype/apptype.py index 22d5e2fd3..d8f035c11 100644 --- a/pcapkit/vendor/reg/apptype/apptype.py +++ b/pcapkit/vendor/reg/apptype/apptype.py @@ -120,18 +120,7 @@ class TransportProtocol(EnumLookup, IntEnum): - """Transport layer protocol. - - .. versionchanged:: 1.5.0 - - An ``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`). :meth:`get` refuses a name no member - declares, with :exc:`~pcapkit.utilities.exceptions.EnumKeyError`, - rather than minting a member for it (:issue:`808`, :issue:`923`). - - """ + """Transport layer protocol.""" # mypy has no aenum plugin, so this class is a plain class to it: every # member below is cast rather than left as a bare auto()-valued one -- @@ -326,20 +315,6 @@ class {NAME}(EnumRegistry, StrEnum): makes it subclassable at all, since :mod:`aenum` refuses to extend an enumeration that already has members. - .. versionchanged:: 1.1.1 - - A member converts with :class:`int` to its port number, and compares, - orders and hashes as that port. - - .. versionchanged:: 1.5.0 - - Split by transport protocol (:issue:`732`): :mod:`pcapkit.const.reg.apptype` - is a package, and every assignment is a member of one of the per-transport - registries rather than of this class. ``get()`` raises - :exc:`ValueError` for a port outside ``0``-``65535`` (:issue:`758`), and - returns an unregistered member rather than minting one for a port the - registry does not assign (:issue:`860`). - Note: The rows below are the registry entries whose **transport protocol column is empty**. A blank transport is not an assignment to anything, so there diff --git a/pcapkit/vendor/tcp/flags.py b/pcapkit/vendor/tcp/flags.py index f3c7e6fe5..2901081db 100644 --- a/pcapkit/vendor/tcp/flags.py +++ b/pcapkit/vendor/tcp/flags.py @@ -60,14 +60,7 @@ __all__ = ['{NAME}'] class {NAME}(EnumRegistry, IntFlag): - """[{NAME}] {DOCS} - - .. versionchanged:: 1.5.0 - - A value outside the 16-bit field raises :exc:`ValueError`, where - :mod:`aenum` composed a pseudo-member for any integer. - - """ + """[{NAME}] {DOCS}""" {ENUM} diff --git a/tests/const/test_const_ftp_command_get_key_type_1824_unit.py b/tests/const/test_const_ftp_command_get_key_type_1824_unit.py index 8869e0f4b..92e3bb663 100644 --- a/tests/const/test_const_ftp_command_get_key_type_1824_unit.py +++ b/tests/const/test_const_ftp_command_get_key_type_1824_unit.py @@ -145,9 +145,13 @@ def test_the_docstring_records_the_change(self) -> 'None': from pcapkit.const.ftp.command import Command doc = inspect.getdoc(Command.get) or '' - self.assertIn('.. versionchanged:: 1.5.0', doc) self.assertRegex(doc, r'Raises:\n\s+EnumValueError: If ``key`` is not a :class:`str`') self.assertIn(':issue:`1824`', doc) + # The module is generated, so the version note is on the docs page + # rather than in the docstring, which + # tests/project/test_no_version_directives_in_generated_tables_1884_unit.py + # checks (#1884). + self.assertNotIn('.. versionchanged::', doc) def test_the_crawler_template_carries_the_same_get(self) -> 'None': """The fix lives in :data:`pcapkit.vendor.ftp.command.LINE`, so a diff --git a/tests/project/test_no_version_directives_in_generated_tables_1884_unit.py b/tests/project/test_no_version_directives_in_generated_tables_1884_unit.py new file mode 100644 index 000000000..9b06bec50 --- /dev/null +++ b/tests/project/test_no_version_directives_in_generated_tables_1884_unit.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +"""Generated tables and their templates carry no version directive (#1884). + +The owner ruled on #1872 that the modules under :mod:`pcapkit.const`, and the +template text under :mod:`pcapkit.vendor` that generates them, take no +``versionadded``, ``versionchanged`` or ``versionremoved`` directive, because +the crawlers regenerate them. A change users need to know about is noted on the +hand-written page under :file:`docs/source/pcapkit/const/` instead. + +Template text is what a crawler writes into the module it generates: every +string literal in a crawler module that is not a docstring, plus the docstring +of each :class:`~pcapkit.vendor.default.Vendor` subclass, which +``Vendor.__init__`` reads as ``DOCS``. The +crawler framework's own docstrings and ``#:`` comments, and the docstrings of a +crawler's hand-written methods, document that code rather than a generated +table, so they keep their directives. + +* :class:`TestGeneratedModules` scans every module under :mod:`pcapkit.const`. +* :class:`TestVendorTemplates` scans the template text of every crawler, and + checks on a sample module which literals count as template text. +* :class:`TestDocsPages` pins the notes #1884 moved onto the docs pages. + +This module is unit-tier and reads no capture. + +""" +from __future__ import annotations + +import ast +import pathlib +import re +import textwrap +import unittest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Iterable + +#: Repository root. +ROOT = pathlib.Path(__file__).resolve().parents[2] + +#: A version directive, as Sphinx spells it. +DIRECTIVE = re.compile(r'\.\. version(?:added|changed|removed)::.*') + +#: The crawler modules whose templates carried a directive before #1884. +ISSUE_VENDORS = ( + 'ftp/command.py', + 'http/method.py', + 'ipv6/extension_header.py', + 'pcapng/option_type.py', + 'pcapng/tls_key_label.py', + 'reg/apptype/apptype.py', + 'tcp/flags.py', +) + +#: The notes #1884 moved from a template onto a docs page, as +#: ``(page, directive, text the directive's body must hold)``. +MOVED_NOTES = ( + ('ftp.rst', 'versionchanged:: 1.5.0', ':issue:`860`'), + ('ftp.rst', 'versionchanged:: 1.5.0', ':issue:`1824`'), + ('http.rst', 'versionchanged:: 1.5.0', ':issue:`860`'), + ('http.rst', 'versionchanged:: 1.5.0', ':issue:`908`'), + ('ipv6.rst', 'versionchanged:: 1.5.0', ':issue:`925`'), + ('pcapng.rst', 'versionchanged:: 1.1.0', 'option code'), + ('pcapng.rst', 'versionadded:: 1.5.0', ':issue:`886`'), + ('reg.rst', 'versionchanged:: 1.1.1', 'port number'), + ('reg.rst', 'versionchanged:: 1.5.0', ':issue:`732`'), + ('reg.rst', 'versionchanged:: 1.5.0', ':issue:`808`'), + ('tcp.rst', 'versionchanged:: 1.5.0', '16-bit'), +) + + +def _base_name(node: 'ast.expr') -> 'str': + """The last component of a base class expression, e.g. ``Vendor``.""" + if isinstance(node, ast.Attribute): + return node.attr + if isinstance(node, ast.Name): + return node.id + return '' + + +def vendor_classes(trees: 'Iterable[ast.Module]') -> 'frozenset[str]': + """Names of the classes that subclass ``Vendor``, directly or not. + + Resolved across all of ``trees`` together, since a crawler may subclass one + defined in another module, as :mod:`pcapkit.vendor.reg.apptype.tcp` does. + + """ + classes = [node for tree in trees for node in tree.body if isinstance(node, ast.ClassDef)] + names = {'Vendor'} + while True: + found = {node.name for node in classes if any(_base_name(base) in names for base in node.bases)} + if found <= names: + return frozenset(names) + names |= found + + +def _docstring(node: 'ast.AST') -> 'ast.Constant | None': + """The docstring literal of a module, class or function, if it has one.""" + body = getattr(node, 'body', None) + if isinstance(body, list) and body and isinstance(body[0], ast.Expr) \ + and isinstance(body[0].value, ast.Constant) and isinstance(body[0].value.value, str): + return body[0].value + return None + + +def template_literals(tree: 'ast.Module', crawlers: 'frozenset[str]') -> 'list[ast.Constant]': + """The string literals of a crawler module a generated module is made from. + + Every string literal, including each literal part of an f-string, except a + docstring -- but with the docstring of each crawler class put back, as + ``DOCS`` interpolates it into the generated module. + + """ + docstrings = set() + for node in ast.walk(tree): + if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + docstring = _docstring(node) + if docstring is not None: + docstrings.add(id(docstring)) + literals = [node for node in ast.walk(tree) if isinstance(node, ast.Constant) + and isinstance(node.value, str) and id(node) not in docstrings] + for node in tree.body: + if isinstance(node, ast.ClassDef) and any(_base_name(base) in crawlers for base in node.bases): + docstring = _docstring(node) + if docstring is not None: + literals.append(docstring) + 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)``.""" + return [(node.lineno, match.group().strip()) + for node in template_literals(tree, crawlers) + for match in DIRECTIVE.finditer(node.value)] + + +def _vendor_trees() -> 'dict[pathlib.Path, ast.Module]': + """Every crawler module under :mod:`pcapkit.vendor`, parsed.""" + return {path: ast.parse(path.read_text(encoding='utf-8'), filename=str(path)) + for path in sorted((ROOT / 'pcapkit' / 'vendor').rglob('*.py'))} + + +class TestGeneratedModules(unittest.TestCase): + """No module under :mod:`pcapkit.const` carries a version directive.""" + + def test_no_const_module_carries_a_directive(self) -> 'None': + paths = sorted((ROOT / 'pcapkit' / 'const').rglob('*.py')) + self.assertTrue(paths, 'found no module under pcapkit/const') + found = [] # type: list[str] + for path in paths: + for lineno, line in enumerate(path.read_text(encoding='utf-8').splitlines(), start=1): + match = DIRECTIVE.search(line) + if match is not None: + found.append(f'{path.relative_to(ROOT)}:{lineno}: {match.group().strip()}') + self.assertEqual(found, []) + + +class TestVendorTemplates(unittest.TestCase): + """No crawler's template text carries a version directive.""" + + def test_no_template_carries_a_directive(self) -> 'None': + trees = _vendor_trees() + crawlers = vendor_classes(trees.values()) + found = [f'{path.relative_to(ROOT)}:{lineno}: {directive}' + for path, tree in trees.items() + for lineno, directive in template_directives(tree, crawlers)] + self.assertEqual(found, []) + + def test_the_issue_templates_are_read(self) -> 'None': + # Guards the scan above against passing because it read nothing. + trees = _vendor_trees() + crawlers = vendor_classes(trees.values()) + for name in ISSUE_VENDORS: + with self.subTest(vendor=name): + tree = trees[ROOT / 'pcapkit' / 'vendor' / name] + text = ''.join(node.value for node in template_literals(tree, crawlers)) + self.assertIn('class ', text) + self.assertIn('EnumRegistry', text) + + def test_only_template_text_is_scanned(self) -> 'None': + # A distinct version marks each site, so the result names the ones read. + sample = textwrap.dedent('''\ + """Crawler framework module. + + .. versionchanged:: 1.0.1 + """ + from pcapkit.vendor.default import Vendor + + #: .. versionchanged:: 1.0.2 + LIMIT = 1 + + LINE = lambda NAME: f"class {NAME}:\\n .. versionadded:: 1.0.3\\n" + + + def helper(): + """Hand-written helper. + + .. versionchanged:: 1.0.4 + """ + + + class Base(Vendor): + """Crawler, whose docstring is emitted as DOCS. + + .. versionchanged:: 1.0.5 + """ + + def method(self): + """Hand-written method. + + .. versionremoved:: 1.0.6 + """ + return ".. versionchanged:: 1.0.7" + + + class Child(Base): + """Crawler subclassing a crawler. + + .. versionadded:: 1.0.8 + """ + + + class Helper: + """Hand-written class. + + .. versionadded:: 1.0.9 + """ + ''') + tree = ast.parse(sample) + crawlers = vendor_classes([tree]) + self.assertEqual(crawlers, frozenset({'Vendor', 'Base', 'Child'})) + found = sorted(directive for _, directive in template_directives(tree, crawlers)) + self.assertEqual(found, ['.. versionadded:: 1.0.3', '.. versionadded:: 1.0.8', + '.. versionchanged:: 1.0.5', '.. versionchanged:: 1.0.7']) + + +class TestDocsPages(unittest.TestCase): + """The notes moved off the templates are on the hand-written docs pages.""" + + def test_each_moved_note_is_on_its_page(self) -> 'None': + for page, directive, text in MOVED_NOTES: + with self.subTest(page=page, directive=directive, text=text): + path = ROOT / 'docs' / 'source' / 'pcapkit' / 'const' / page + blocks = re.findall(rf'(?ms)^\.\. {re.escape(directive)}\n\n(.*?)(?=^\S|\Z)', + path.read_text(encoding='utf-8')) + self.assertTrue(any(text in block for block in blocks), + f'no {directive} block on {page} holds {text!r}') + + +if __name__ == '__main__': + unittest.main()