From 562e5c19b5416ed82908513854abb194f8104df4 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Tue, 3 Nov 2026 01:10:49 -0500 Subject: [PATCH] perf(const): import the const enum packages lazily (#2729) Every pcapkit.const package imported all of its enumeration modules with itself, and pcapkit.const star-imported every package, so touching one enumeration loaded them all. Only the TCP and UDP app-type registries loaded on first use (#1538). - utilities.lazy: lazy_exports builds a package's PEP 562 __getattr__ and __dir__ from an exports table, or from the __all__ of subpackages it re-exports; it generalises the #1538 hooks. - const: each package names its exports under TYPE_CHECKING and binds the hooks, and pcapkit.const re-exports its subpackages through them, so importing it loads no enumeration module. - const.reg.apptype: SCTP and DCCP load on first use as TCP and UDP do. _Registries is an immutable Info keyed by TransportProtocol rather than a dict subclass (#2726); AppType.__registries__ is typed as a Mapping, in the crawler template too. - tests: conftest warms every const module; the #1538 tests cover SCTP, DCCP, the Info registries and a bare import of pcapkit.const; new sweeps hold every const package's __all__, dir() and member pickles to the eager behaviour; the #2656 test reads the lazy_exports call. - docs: a page for pcapkit.utilities.lazy, and the ruling on const-vendor-pairing. The new and changed laziness tests fail on 468f9c219 without the pcapkit/ change; the const, vendor, utilities, corekit, protocols root and schema, and project modules pass, as do the const and utilities unittest legs. --- .../conventions/const-vendor-pairing.rst | 21 + .../source/contributing/conventions/index.rst | 5 +- docs/source/pcapkit/utilities/index.rst | 4 +- docs/source/pcapkit/utilities/lazy.rst | 15 + pcapkit/const/__init__.py | 76 +-- pcapkit/const/arp/__init__.py | 14 +- pcapkit/const/dccp/__init__.py | 26 +- pcapkit/const/esp/__init__.py | 14 +- pcapkit/const/ftp/__init__.py | 14 +- pcapkit/const/hip/__init__.py | 63 ++- pcapkit/const/http/__init__.py | 23 +- pcapkit/const/icmp/__init__.py | 24 +- pcapkit/const/icmpv6/__init__.py | 24 +- pcapkit/const/ieee802_11/__init__.py | 27 +- pcapkit/const/igmp/__init__.py | 14 +- pcapkit/const/ipv4/__init__.py | 46 +- pcapkit/const/ipv6/__init__.py | 35 +- pcapkit/const/ipx/__init__.py | 14 +- pcapkit/const/l2tp/__init__.py | 11 +- pcapkit/const/linux/__init__.py | 11 +- pcapkit/const/mh/__init__.py | 145 ++++-- pcapkit/const/ngap/__init__.py | 14 +- pcapkit/const/ospf/__init__.py | 14 +- pcapkit/const/pcapng/__init__.py | 32 +- pcapkit/const/radiotap/__init__.py | 72 ++- pcapkit/const/reg/__init__.py | 20 +- pcapkit/const/reg/apptype/__init__.py | 215 ++++---- pcapkit/const/reg/apptype/apptype.py | 14 +- pcapkit/const/sctp/__init__.py | 22 +- pcapkit/const/sslv2/__init__.py | 14 +- pcapkit/const/tcp/__init__.py | 20 +- pcapkit/const/tls/__init__.py | 17 +- pcapkit/const/vlan/__init__.py | 11 +- pcapkit/utilities/__init__.py | 4 +- pcapkit/utilities/lazy.py | 99 ++++ pcapkit/vendor/reg/apptype/apptype.py | 14 +- tests/conftest.py | 24 +- .../test_const_apptype_lazy_1538_unit.py | 480 ++++++++++++------ .../test_const_lazy_exports_2729_unit.py | 169 ++++++ .../test_const_package_exports_2656_unit.py | 26 +- .../utilities/test_lazy_exports_2729_unit.py | 193 +++++++ 41 files changed, 1597 insertions(+), 503 deletions(-) create mode 100644 docs/source/pcapkit/utilities/lazy.rst create mode 100644 pcapkit/utilities/lazy.py create mode 100644 tests/const/test_const_lazy_exports_2729_unit.py create mode 100644 tests/utilities/test_lazy_exports_2729_unit.py diff --git a/docs/source/contributing/conventions/const-vendor-pairing.rst b/docs/source/contributing/conventions/const-vendor-pairing.rst index e3a9b79111..677e0f309e 100644 --- a/docs/source/contributing/conventions/const-vendor-pairing.rst +++ b/docs/source/contributing/conventions/const-vendor-pairing.rst @@ -91,3 +91,24 @@ record-layer header citations, the Unassigned/Reserved band rationale, the docstrings and comments, the way :mod:`pcapkit.vendor.linux.arphrd` already does for ARPHRD; the generated ``const`` modules carry only the plain, templated docstring every other generated module does. + +Lazy Package Imports +~~~~~~~~~~~~~~~~~~~~ + +**A** :mod:`pcapkit.const` **package imports its enumerations on first use**, not with +itself. Ruled by the owner in session and recorded on :issue:`2729`. Its +:file:`__init__.py` names each export in an import under ``if TYPE_CHECKING:``, for +the type checkers, and binds the ``__getattr__`` and ``__dir__`` that +:func:`~pcapkit.utilities.lazy.lazy_exports` returns; :mod:`pcapkit.const` re-exports +its subpackages' ``__all__`` the same way. The one real import is of +:class:`~pcapkit.const.reg.apptype.apptype.AppType` and +:class:`~pcapkit.const.reg.apptype.apptype.TransportProtocol`, which +:mod:`pcapkit.const.reg.apptype` builds +:attr:`AppType.__registries__ ` +on. A crawler writes the enumeration module and never a package's +:file:`__init__.py`, so regenerating keeps the laziness, and a module a crawler adds +takes its export there by hand, in both places. +:file:`tests/const/test_const_lazy_exports_2729_unit.py` fails on an export imported +outside ``TYPE_CHECKING``, or one that does not resolve to what its import names, and +:file:`tests/const/test_const_apptype_lazy_1538_unit.py` on a package that loads an +enumeration with itself. diff --git a/docs/source/contributing/conventions/index.rst b/docs/source/contributing/conventions/index.rst index 8febc6895d..3511272398 100644 --- a/docs/source/contributing/conventions/index.rst +++ b/docs/source/contributing/conventions/index.rst @@ -35,8 +35,9 @@ House Conventions * :ref:`reassembly-integration` -- :mod:`pcapkit.foundation.reassembly` and what a new engine there needs from :mod:`pcapkit.toolkit` and :mod:`pcapkit.foundation.analysis`; - * :ref:`const-vendor-pairing` -- :mod:`pcapkit.const` and the - :mod:`pcapkit.vendor` crawler each module needs. + * :ref:`const-vendor-pairing` -- :mod:`pcapkit.const`, the + :mod:`pcapkit.vendor` crawler each module needs, and how a package of it + imports its modules. **A ruling that stays in its thread is a ruling that gets rediscovered.** So when a question is answered in a way the code cannot express on its own -- a diff --git a/docs/source/pcapkit/utilities/index.rst b/docs/source/pcapkit/utilities/index.rst index d81bfee922..1589b74b65 100644 --- a/docs/source/pcapkit/utilities/index.rst +++ b/docs/source/pcapkit/utilities/index.rst @@ -8,7 +8,8 @@ Module Utilities built with: user-refined exceptions and warnings, logging, the tunable limits of :mod:`~pcapkit.utilities.config`, a compatibility layer, timestamp conversion, pickle reductions, the -:mod:`context variables ` its modules share, and +:mod:`context variables ` its modules share, +:mod:`lazy imports ` for a package's exports, and wrappers around third-party libraries. It imports nothing from :mod:`pcapkit` outside itself; see :ref:`utilities-or-corekit`. @@ -23,6 +24,7 @@ outside itself; see :ref:`utilities-or-corekit`. datetime pickle contextvars + lazy Version Compatibility ===================== diff --git a/docs/source/pcapkit/utilities/lazy.rst b/docs/source/pcapkit/utilities/lazy.rst new file mode 100644 index 0000000000..9d88815078 --- /dev/null +++ b/docs/source/pcapkit/utilities/lazy.rst @@ -0,0 +1,15 @@ +Lazy Imports +============ + +.. module:: pcapkit.utilities.lazy + +:mod:`pcapkit.utilities.lazy` holds :func:`lazy_exports`, which gives a package the +:pep:`562` ``__getattr__`` and ``__dir__`` that import each name it exports the first +time the name is reached, rather than with the package itself. Every package under +:mod:`pcapkit.const` imports its enumerations that way (:issue:`2729`), so a caller +pays only for the enumerations it uses. This module imports nothing from +:mod:`pcapkit`; see :ref:`utilities-or-corekit`. + +.. versionadded:: 1.6.0 + +.. autofunction:: pcapkit.utilities.lazy.lazy_exports diff --git a/pcapkit/const/__init__.py b/pcapkit/const/__init__.py index bfe312953e..3010ce5149 100644 --- a/pcapkit/const/__init__.py +++ b/pcapkit/const/__init__.py @@ -6,39 +6,49 @@ This module contains all constant enumerations of :mod:`pcapkit`, which are automatically generated from the :mod:`pcapkit.vendor` module. +Each enumeration is imported the first time it is reached -- as an attribute +of this package or of the subpackage holding it -- rather than with the +package, through :func:`~pcapkit.utilities.lazy.lazy_exports` +(:issue:`2729`). The subpackages themselves are imported with it, and import +none of their enumerations. + """ +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports -# IANA registration -from pcapkit.const.reg import * +if TYPE_CHECKING: + # IANA registration + from pcapkit.const.reg import * -# Miscellaneous -from pcapkit.const.pcapng import * + # Miscellaneous + from pcapkit.const.pcapng import * -# per protocol -from pcapkit.const.arp import * -from pcapkit.const.dccp import * -from pcapkit.const.esp import * -from pcapkit.const.ftp import * -from pcapkit.const.hip import * -from pcapkit.const.http import * -from pcapkit.const.icmp import * -from pcapkit.const.icmpv6 import * -from pcapkit.const.ieee802_11 import * -from pcapkit.const.igmp import * -from pcapkit.const.ipv4 import * -from pcapkit.const.ipv6 import * -from pcapkit.const.ipx import * -from pcapkit.const.l2tp import * -from pcapkit.const.linux import * -from pcapkit.const.mh import * -from pcapkit.const.ngap import * -from pcapkit.const.ospf import * -from pcapkit.const.radiotap import * -from pcapkit.const.sctp import * -from pcapkit.const.sslv2 import * -from pcapkit.const.tcp import * -from pcapkit.const.tls import * -from pcapkit.const.vlan import * + # per protocol + from pcapkit.const.arp import * + from pcapkit.const.dccp import * + from pcapkit.const.esp import * + from pcapkit.const.ftp import * + from pcapkit.const.hip import * + from pcapkit.const.http import * + from pcapkit.const.icmp import * + from pcapkit.const.icmpv6 import * + from pcapkit.const.ieee802_11 import * + from pcapkit.const.igmp import * + from pcapkit.const.ipv4 import * + from pcapkit.const.ipv6 import * + from pcapkit.const.ipx import * + from pcapkit.const.l2tp import * + from pcapkit.const.linux import * + from pcapkit.const.mh import * + from pcapkit.const.ngap import * + from pcapkit.const.ospf import * + from pcapkit.const.radiotap import * + from pcapkit.const.sctp import * + from pcapkit.const.sslv2 import * + from pcapkit.const.tcp import * + from pcapkit.const.tls import * + from pcapkit.const.vlan import * __all__ = [ # Protocol Registration @@ -123,3 +133,11 @@ 'PCAPNG_VerdictType', 'PCAPNG_RecordType', 'PCAPNG_SecretsType', 'PCAPNG_FilterType', 'PCAPNG_TLSKeyLabel', ] + +# NOTE: the subpackages whose ``__all__`` this package re-exports, which the +# type checkers star-import above. +__getattr__, __dir__ = lazy_exports(__name__, globals(), packages=( + 'reg', 'pcapng', 'arp', 'dccp', 'esp', 'ftp', 'hip', 'http', 'icmp', 'icmpv6', + 'ieee802_11', 'igmp', 'ipv4', 'ipv6', 'ipx', 'l2tp', 'linux', 'mh', 'ngap', 'ospf', + 'radiotap', 'sctp', 'sslv2', 'tcp', 'tls', 'vlan', +)) diff --git a/pcapkit/const/arp/__init__.py b/pcapkit/const/arp/__init__.py index 5206737ca2..76dfdab977 100644 --- a/pcapkit/const/arp/__init__.py +++ b/pcapkit/const/arp/__init__.py @@ -19,7 +19,17 @@ """ -from pcapkit.const.arp.hardware import Hardware as ARP_Hardware -from pcapkit.const.arp.operation import Operation as ARP_Operation +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.arp.hardware import Hardware as ARP_Hardware + from pcapkit.const.arp.operation import Operation as ARP_Operation __all__ = ['ARP_Hardware', 'ARP_Operation'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'ARP_Hardware': ('hardware', 'Hardware'), + 'ARP_Operation': ('operation', 'Operation'), +}) diff --git a/pcapkit/const/dccp/__init__.py b/pcapkit/const/dccp/__init__.py index 91c5ce32a8..f7d8db5d41 100644 --- a/pcapkit/const/dccp/__init__.py +++ b/pcapkit/const/dccp/__init__.py @@ -33,12 +33,26 @@ """ -from pcapkit.const.dccp.ack_vector_state import AckVectorState as DCCP_AckVectorState -from pcapkit.const.dccp.drop_code import DropCode as DCCP_DropCode -from pcapkit.const.dccp.feature import Feature as DCCP_Feature -from pcapkit.const.dccp.option import Option as DCCP_Option -from pcapkit.const.dccp.packet_type import PacketType as DCCP_PacketType -from pcapkit.const.dccp.reset_code import ResetCode as DCCP_ResetCode +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.dccp.ack_vector_state import AckVectorState as DCCP_AckVectorState + from pcapkit.const.dccp.drop_code import DropCode as DCCP_DropCode + from pcapkit.const.dccp.feature import Feature as DCCP_Feature + from pcapkit.const.dccp.option import Option as DCCP_Option + from pcapkit.const.dccp.packet_type import PacketType as DCCP_PacketType + from pcapkit.const.dccp.reset_code import ResetCode as DCCP_ResetCode __all__ = ['DCCP_PacketType', 'DCCP_ResetCode', 'DCCP_Option', 'DCCP_Feature', 'DCCP_AckVectorState', 'DCCP_DropCode'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'DCCP_PacketType': ('packet_type', 'PacketType'), + 'DCCP_ResetCode': ('reset_code', 'ResetCode'), + 'DCCP_Option': ('option', 'Option'), + 'DCCP_Feature': ('feature', 'Feature'), + 'DCCP_AckVectorState': ('ack_vector_state', 'AckVectorState'), + 'DCCP_DropCode': ('drop_code', 'DropCode'), +}) diff --git a/pcapkit/const/esp/__init__.py b/pcapkit/const/esp/__init__.py index ba499d3f28..b1f9d281df 100644 --- a/pcapkit/const/esp/__init__.py +++ b/pcapkit/const/esp/__init__.py @@ -33,7 +33,17 @@ """ -from pcapkit.const.esp.cipher import Cipher as ESP_Cipher -from pcapkit.const.esp.integrity import Integrity as ESP_Integrity +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.esp.cipher import Cipher as ESP_Cipher + from pcapkit.const.esp.integrity import Integrity as ESP_Integrity __all__ = ['ESP_Cipher', 'ESP_Integrity'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'ESP_Cipher': ('cipher', 'Cipher'), + 'ESP_Integrity': ('integrity', 'Integrity'), +}) diff --git a/pcapkit/const/ftp/__init__.py b/pcapkit/const/ftp/__init__.py index ec338e9d7e..c7384b7e93 100644 --- a/pcapkit/const/ftp/__init__.py +++ b/pcapkit/const/ftp/__init__.py @@ -19,7 +19,17 @@ """ -from pcapkit.const.ftp.command import Command as FTP_Command -from pcapkit.const.ftp.return_code import ReturnCode as FTP_ReturnCode +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.ftp.command import Command as FTP_Command + from pcapkit.const.ftp.return_code import ReturnCode as FTP_ReturnCode __all__ = ['FTP_Command', 'FTP_ReturnCode'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'FTP_Command': ('command', 'Command'), + 'FTP_ReturnCode': ('return_code', 'ReturnCode'), +}) diff --git a/pcapkit/const/hip/__init__.py b/pcapkit/const/hip/__init__.py index 9cbca443b9..33b77ae313 100644 --- a/pcapkit/const/hip/__init__.py +++ b/pcapkit/const/hip/__init__.py @@ -69,26 +69,53 @@ """ -from pcapkit.const.hip.certificate import Certificate as HIP_Certificate -from pcapkit.const.hip.cipher import Cipher as HIP_Cipher -from pcapkit.const.hip.di import DITypes as HIP_DITypes -from pcapkit.const.hip.ecdsa_curve import ECDSACurve as HIP_ECDSACurve -from pcapkit.const.hip.ecdsa_low_curve import ECDSALowCurve as HIP_ECDSALowCurve -from pcapkit.const.hip.eddsa_curve import EdDSACurve as HIP_EdDSACurve -from pcapkit.const.hip.esp_transform_suite import ESPTransformSuite as HIP_ESPTransformSuite -from pcapkit.const.hip.group import Group as HIP_Group -from pcapkit.const.hip.hi_algorithm import HIAlgorithm as HIP_HIAlgorithm -from pcapkit.const.hip.hit_suite import HITSuite as HIP_HITSuite -from pcapkit.const.hip.nat_traversal import NATTraversal as HIP_NATTraversal -from pcapkit.const.hip.notify_message import NotifyMessage as HIP_NotifyMessage -from pcapkit.const.hip.packet import Packet as HIP_Packet -from pcapkit.const.hip.parameter import Parameter as HIP_Parameter -from pcapkit.const.hip.registration import Registration as HIP_Registration -from pcapkit.const.hip.registration_failure import RegistrationFailure as HIP_RegistrationFailure -from pcapkit.const.hip.suite import Suite as HIP_Suite -from pcapkit.const.hip.transport import Transport as HIP_Transport +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.hip.certificate import Certificate as HIP_Certificate + from pcapkit.const.hip.cipher import Cipher as HIP_Cipher + from pcapkit.const.hip.di import DITypes as HIP_DITypes + from pcapkit.const.hip.ecdsa_curve import ECDSACurve as HIP_ECDSACurve + from pcapkit.const.hip.ecdsa_low_curve import ECDSALowCurve as HIP_ECDSALowCurve + from pcapkit.const.hip.eddsa_curve import EdDSACurve as HIP_EdDSACurve + from pcapkit.const.hip.esp_transform_suite import ESPTransformSuite as HIP_ESPTransformSuite + from pcapkit.const.hip.group import Group as HIP_Group + from pcapkit.const.hip.hi_algorithm import HIAlgorithm as HIP_HIAlgorithm + from pcapkit.const.hip.hit_suite import HITSuite as HIP_HITSuite + from pcapkit.const.hip.nat_traversal import NATTraversal as HIP_NATTraversal + from pcapkit.const.hip.notify_message import NotifyMessage as HIP_NotifyMessage + from pcapkit.const.hip.packet import Packet as HIP_Packet + from pcapkit.const.hip.parameter import Parameter as HIP_Parameter + from pcapkit.const.hip.registration import Registration as HIP_Registration + from pcapkit.const.hip.registration_failure import \ + RegistrationFailure as HIP_RegistrationFailure + from pcapkit.const.hip.suite import Suite as HIP_Suite + from pcapkit.const.hip.transport import Transport as HIP_Transport __all__ = ['HIP_Certificate', 'HIP_Cipher', 'HIP_DITypes', 'HIP_ECDSACurve', 'HIP_ECDSALowCurve', 'HIP_ESPTransformSuite', 'HIP_Group', 'HIP_HIAlgorithm', 'HIP_HITSuite', 'HIP_NATTraversal', 'HIP_NotifyMessage', 'HIP_Packet', 'HIP_Parameter', 'HIP_Registration', 'HIP_RegistrationFailure', 'HIP_Suite', 'HIP_Transport', 'HIP_EdDSACurve'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'HIP_Certificate': ('certificate', 'Certificate'), + 'HIP_Cipher': ('cipher', 'Cipher'), + 'HIP_DITypes': ('di', 'DITypes'), + 'HIP_ECDSACurve': ('ecdsa_curve', 'ECDSACurve'), + 'HIP_ECDSALowCurve': ('ecdsa_low_curve', 'ECDSALowCurve'), + 'HIP_ESPTransformSuite': ('esp_transform_suite', 'ESPTransformSuite'), + 'HIP_Group': ('group', 'Group'), + 'HIP_HIAlgorithm': ('hi_algorithm', 'HIAlgorithm'), + 'HIP_HITSuite': ('hit_suite', 'HITSuite'), + 'HIP_NATTraversal': ('nat_traversal', 'NATTraversal'), + 'HIP_NotifyMessage': ('notify_message', 'NotifyMessage'), + 'HIP_Packet': ('packet', 'Packet'), + 'HIP_Parameter': ('parameter', 'Parameter'), + 'HIP_Registration': ('registration', 'Registration'), + 'HIP_RegistrationFailure': ('registration_failure', 'RegistrationFailure'), + 'HIP_Suite': ('suite', 'Suite'), + 'HIP_Transport': ('transport', 'Transport'), + 'HIP_EdDSACurve': ('eddsa_curve', 'EdDSACurve'), +}) diff --git a/pcapkit/const/http/__init__.py b/pcapkit/const/http/__init__.py index ee7b79e654..f52a9b28ca 100644 --- a/pcapkit/const/http/__init__.py +++ b/pcapkit/const/http/__init__.py @@ -30,10 +30,23 @@ """ -from pcapkit.const.http.error_code import ErrorCode as HTTP_ErrorCode -from pcapkit.const.http.frame import Frame as HTTP_Frame -from pcapkit.const.http.method import Method as HTTP_Method -from pcapkit.const.http.setting import Setting as HTTP_Setting -from pcapkit.const.http.status_code import StatusCode as HTTP_Status +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.http.error_code import ErrorCode as HTTP_ErrorCode + from pcapkit.const.http.frame import Frame as HTTP_Frame + from pcapkit.const.http.method import Method as HTTP_Method + from pcapkit.const.http.setting import Setting as HTTP_Setting + from pcapkit.const.http.status_code import StatusCode as HTTP_Status __all__ = ['HTTP_ErrorCode', 'HTTP_Frame', 'HTTP_Method', 'HTTP_Setting', 'HTTP_Status'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'HTTP_ErrorCode': ('error_code', 'ErrorCode'), + 'HTTP_Frame': ('frame', 'Frame'), + 'HTTP_Method': ('method', 'Method'), + 'HTTP_Setting': ('setting', 'Setting'), + 'HTTP_Status': ('status_code', 'StatusCode'), +}) diff --git a/pcapkit/const/icmp/__init__.py b/pcapkit/const/icmp/__init__.py index d020e548f4..4f5b2ff174 100644 --- a/pcapkit/const/icmp/__init__.py +++ b/pcapkit/const/icmp/__init__.py @@ -27,14 +27,26 @@ """ -from pcapkit.const.icmp.destination_unreachable_code import \ - DestinationUnreachableCode as ICMP_DestinationUnreachableCode -from pcapkit.const.icmp.parameter_problem_code import \ - ParameterProblemCode as ICMP_ParameterProblemCode -from pcapkit.const.icmp.time_exceeded_code import TimeExceededCode as ICMP_TimeExceededCode -from pcapkit.const.icmp.type import Type as ICMP_Type +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.icmp.destination_unreachable_code import \ + DestinationUnreachableCode as ICMP_DestinationUnreachableCode + from pcapkit.const.icmp.parameter_problem_code import \ + ParameterProblemCode as ICMP_ParameterProblemCode + from pcapkit.const.icmp.time_exceeded_code import TimeExceededCode as ICMP_TimeExceededCode + from pcapkit.const.icmp.type import Type as ICMP_Type __all__ = [ 'ICMP_Type', 'ICMP_DestinationUnreachableCode', 'ICMP_TimeExceededCode', 'ICMP_ParameterProblemCode', ] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'ICMP_Type': ('type', 'Type'), + 'ICMP_DestinationUnreachableCode': ('destination_unreachable_code', 'DestinationUnreachableCode'), + 'ICMP_TimeExceededCode': ('time_exceeded_code', 'TimeExceededCode'), + 'ICMP_ParameterProblemCode': ('parameter_problem_code', 'ParameterProblemCode'), +}) diff --git a/pcapkit/const/icmpv6/__init__.py b/pcapkit/const/icmpv6/__init__.py index 34019c7edb..1bcbd0651e 100644 --- a/pcapkit/const/icmpv6/__init__.py +++ b/pcapkit/const/icmpv6/__init__.py @@ -27,14 +27,26 @@ """ -from pcapkit.const.icmpv6.destination_unreachable_code import \ - DestinationUnreachableCode as ICMPv6_DestinationUnreachableCode -from pcapkit.const.icmpv6.parameter_problem_code import \ - ParameterProblemCode as ICMPv6_ParameterProblemCode -from pcapkit.const.icmpv6.time_exceeded_code import TimeExceededCode as ICMPv6_TimeExceededCode -from pcapkit.const.icmpv6.type import Type as ICMPv6_Type +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.icmpv6.destination_unreachable_code import \ + DestinationUnreachableCode as ICMPv6_DestinationUnreachableCode + from pcapkit.const.icmpv6.parameter_problem_code import \ + ParameterProblemCode as ICMPv6_ParameterProblemCode + from pcapkit.const.icmpv6.time_exceeded_code import TimeExceededCode as ICMPv6_TimeExceededCode + from pcapkit.const.icmpv6.type import Type as ICMPv6_Type __all__ = [ 'ICMPv6_Type', 'ICMPv6_DestinationUnreachableCode', 'ICMPv6_TimeExceededCode', 'ICMPv6_ParameterProblemCode', ] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'ICMPv6_Type': ('type', 'Type'), + 'ICMPv6_DestinationUnreachableCode': ('destination_unreachable_code', 'DestinationUnreachableCode'), + 'ICMPv6_TimeExceededCode': ('time_exceeded_code', 'TimeExceededCode'), + 'ICMPv6_ParameterProblemCode': ('parameter_problem_code', 'ParameterProblemCode'), +}) diff --git a/pcapkit/const/ieee802_11/__init__.py b/pcapkit/const/ieee802_11/__init__.py index f9d0b78b5c..db34d0e7f4 100644 --- a/pcapkit/const/ieee802_11/__init__.py +++ b/pcapkit/const/ieee802_11/__init__.py @@ -25,15 +25,28 @@ """ -from pcapkit.const.ieee802_11.control_subtype import ControlSubtype as IEEE802_11_ControlSubtype -from pcapkit.const.ieee802_11.data_subtype import DataSubtype as IEEE802_11_DataSubtype -from pcapkit.const.ieee802_11.extension_subtype import \ - ExtensionSubtype as IEEE802_11_ExtensionSubtype -from pcapkit.const.ieee802_11.management_subtype import \ - ManagementSubtype as IEEE802_11_ManagementSubtype -from pcapkit.const.ieee802_11.type import Type as IEEE802_11_Type +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.ieee802_11.control_subtype import ControlSubtype as IEEE802_11_ControlSubtype + from pcapkit.const.ieee802_11.data_subtype import DataSubtype as IEEE802_11_DataSubtype + from pcapkit.const.ieee802_11.extension_subtype import \ + ExtensionSubtype as IEEE802_11_ExtensionSubtype + from pcapkit.const.ieee802_11.management_subtype import \ + ManagementSubtype as IEEE802_11_ManagementSubtype + from pcapkit.const.ieee802_11.type import Type as IEEE802_11_Type __all__ = [ 'IEEE802_11_Type', 'IEEE802_11_ManagementSubtype', 'IEEE802_11_ControlSubtype', 'IEEE802_11_DataSubtype', 'IEEE802_11_ExtensionSubtype', ] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'IEEE802_11_Type': ('type', 'Type'), + 'IEEE802_11_ManagementSubtype': ('management_subtype', 'ManagementSubtype'), + 'IEEE802_11_ControlSubtype': ('control_subtype', 'ControlSubtype'), + 'IEEE802_11_DataSubtype': ('data_subtype', 'DataSubtype'), + 'IEEE802_11_ExtensionSubtype': ('extension_subtype', 'ExtensionSubtype'), +}) diff --git a/pcapkit/const/igmp/__init__.py b/pcapkit/const/igmp/__init__.py index f75f28898e..e5059ad54a 100644 --- a/pcapkit/const/igmp/__init__.py +++ b/pcapkit/const/igmp/__init__.py @@ -21,7 +21,17 @@ """ -from pcapkit.const.igmp.record_type import RecordType as IGMP_RecordType -from pcapkit.const.igmp.type import Type as IGMP_Type +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.igmp.record_type import RecordType as IGMP_RecordType + from pcapkit.const.igmp.type import Type as IGMP_Type __all__ = ['IGMP_Type', 'IGMP_RecordType'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'IGMP_Type': ('type', 'Type'), + 'IGMP_RecordType': ('record_type', 'RecordType'), +}) diff --git a/pcapkit/const/ipv4/__init__.py b/pcapkit/const/ipv4/__init__.py index f47d8918a2..c87560419c 100644 --- a/pcapkit/const/ipv4/__init__.py +++ b/pcapkit/const/ipv4/__init__.py @@ -39,19 +39,41 @@ """ -from pcapkit.const.ipv4.classification_level import ClassificationLevel as IPv4_ClassificationLevel -from pcapkit.const.ipv4.option_class import OptionClass as IPv4_OptionClass -from pcapkit.const.ipv4.option_number import OptionNumber as IPv4_OptionNumber -from pcapkit.const.ipv4.protection_authority import ProtectionAuthority as IPv4_ProtectionAuthority -from pcapkit.const.ipv4.qs_function import QSFunction as IPv4_QSFunction -from pcapkit.const.ipv4.router_alert import RouterAlert as IPv4_RouterAlert -from pcapkit.const.ipv4.tos_del import ToSDelay as IPv4_ToSDelay -from pcapkit.const.ipv4.tos_ecn import ToSECN as IPv4_ToSECN -from pcapkit.const.ipv4.tos_pre import ToSPrecedence as IPv4_ToSPrecedence -from pcapkit.const.ipv4.tos_rel import ToSReliability as IPv4_ToSReliability -from pcapkit.const.ipv4.tos_thr import ToSThroughput as IPv4_ToSThroughput -from pcapkit.const.ipv4.ts_flag import TSFlag as IPv4_TSFlag +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.ipv4.classification_level import \ + ClassificationLevel as IPv4_ClassificationLevel + from pcapkit.const.ipv4.option_class import OptionClass as IPv4_OptionClass + from pcapkit.const.ipv4.option_number import OptionNumber as IPv4_OptionNumber + from pcapkit.const.ipv4.protection_authority import \ + ProtectionAuthority as IPv4_ProtectionAuthority + from pcapkit.const.ipv4.qs_function import QSFunction as IPv4_QSFunction + from pcapkit.const.ipv4.router_alert import RouterAlert as IPv4_RouterAlert + from pcapkit.const.ipv4.tos_del import ToSDelay as IPv4_ToSDelay + from pcapkit.const.ipv4.tos_ecn import ToSECN as IPv4_ToSECN + from pcapkit.const.ipv4.tos_pre import ToSPrecedence as IPv4_ToSPrecedence + from pcapkit.const.ipv4.tos_rel import ToSReliability as IPv4_ToSReliability + from pcapkit.const.ipv4.tos_thr import ToSThroughput as IPv4_ToSThroughput + from pcapkit.const.ipv4.ts_flag import TSFlag as IPv4_TSFlag __all__ = ['IPv4_ClassificationLevel', 'IPv4_OptionClass', 'IPv4_OptionNumber', 'IPv4_ProtectionAuthority', 'IPv4_QSFunction', 'IPv4_RouterAlert', 'IPv4_ToSDelay', 'IPv4_ToSECN', 'IPv4_ToSPrecedence', 'IPv4_ToSReliability', 'IPv4_ToSThroughput', 'IPv4_TSFlag'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'IPv4_ClassificationLevel': ('classification_level', 'ClassificationLevel'), + 'IPv4_OptionClass': ('option_class', 'OptionClass'), + 'IPv4_OptionNumber': ('option_number', 'OptionNumber'), + 'IPv4_ProtectionAuthority': ('protection_authority', 'ProtectionAuthority'), + 'IPv4_QSFunction': ('qs_function', 'QSFunction'), + 'IPv4_RouterAlert': ('router_alert', 'RouterAlert'), + 'IPv4_ToSDelay': ('tos_del', 'ToSDelay'), + 'IPv4_ToSECN': ('tos_ecn', 'ToSECN'), + 'IPv4_ToSPrecedence': ('tos_pre', 'ToSPrecedence'), + 'IPv4_ToSReliability': ('tos_rel', 'ToSReliability'), + 'IPv4_ToSThroughput': ('tos_thr', 'ToSThroughput'), + 'IPv4_TSFlag': ('ts_flag', 'TSFlag'), +}) diff --git a/pcapkit/const/ipv6/__init__.py b/pcapkit/const/ipv6/__init__.py index 9bd7945ff7..d5f75a0fff 100644 --- a/pcapkit/const/ipv6/__init__.py +++ b/pcapkit/const/ipv6/__init__.py @@ -39,15 +39,32 @@ """ -from pcapkit.const.ipv6.extension_header import ExtensionHeader as IPv6_ExtensionHeader -from pcapkit.const.ipv6.option import Option as IPv6_Option -from pcapkit.const.ipv6.option_action import OptionAction as IPv6_OptionAction -from pcapkit.const.ipv6.qs_function import QSFunction as IPv6_QSFunction -from pcapkit.const.ipv6.router_alert import RouterAlert as IPv6_RouterAlert -from pcapkit.const.ipv6.routing import Routing as IPv6_Routing -from pcapkit.const.ipv6.seed_id import SeedID as IPv6_SeedID -from pcapkit.const.ipv6.smf_dpd_mode import SMFDPDMode as IPv6_SMFDPDMode -from pcapkit.const.ipv6.tagger_id import TaggerID as IPv6_TaggerID +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.ipv6.extension_header import ExtensionHeader as IPv6_ExtensionHeader + from pcapkit.const.ipv6.option import Option as IPv6_Option + from pcapkit.const.ipv6.option_action import OptionAction as IPv6_OptionAction + from pcapkit.const.ipv6.qs_function import QSFunction as IPv6_QSFunction + from pcapkit.const.ipv6.router_alert import RouterAlert as IPv6_RouterAlert + from pcapkit.const.ipv6.routing import Routing as IPv6_Routing + from pcapkit.const.ipv6.seed_id import SeedID as IPv6_SeedID + from pcapkit.const.ipv6.smf_dpd_mode import SMFDPDMode as IPv6_SMFDPDMode + from pcapkit.const.ipv6.tagger_id import TaggerID as IPv6_TaggerID __all__ = ['IPv6_ExtensionHeader', 'IPv6_Option', 'IPv6_QSFunction', 'IPv6_RouterAlert', 'IPv6_Routing', 'IPv6_SeedID', 'IPv6_SMFDPDMode', 'IPv6_TaggerID', 'IPv6_OptionAction'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'IPv6_ExtensionHeader': ('extension_header', 'ExtensionHeader'), + 'IPv6_Option': ('option', 'Option'), + 'IPv6_QSFunction': ('qs_function', 'QSFunction'), + 'IPv6_RouterAlert': ('router_alert', 'RouterAlert'), + 'IPv6_Routing': ('routing', 'Routing'), + 'IPv6_SeedID': ('seed_id', 'SeedID'), + 'IPv6_SMFDPDMode': ('smf_dpd_mode', 'SMFDPDMode'), + 'IPv6_TaggerID': ('tagger_id', 'TaggerID'), + 'IPv6_OptionAction': ('option_action', 'OptionAction'), +}) diff --git a/pcapkit/const/ipx/__init__.py b/pcapkit/const/ipx/__init__.py index 6767fb2217..3d77fee9dc 100644 --- a/pcapkit/const/ipx/__init__.py +++ b/pcapkit/const/ipx/__init__.py @@ -21,7 +21,17 @@ """ -from pcapkit.const.ipx.packet import Packet as IPX_Packet -from pcapkit.const.ipx.socket import Socket as IPX_Socket +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.ipx.packet import Packet as IPX_Packet + from pcapkit.const.ipx.socket import Socket as IPX_Socket __all__ = ['IPX_Packet', 'IPX_Socket'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'IPX_Packet': ('packet', 'Packet'), + 'IPX_Socket': ('socket', 'Socket'), +}) diff --git a/pcapkit/const/l2tp/__init__.py b/pcapkit/const/l2tp/__init__.py index efb669bd88..dac025f72e 100644 --- a/pcapkit/const/l2tp/__init__.py +++ b/pcapkit/const/l2tp/__init__.py @@ -16,6 +16,15 @@ """ -from pcapkit.const.l2tp.type import Type as L2TP_Type +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.l2tp.type import Type as L2TP_Type __all__ = ['L2TP_Type'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'L2TP_Type': ('type', 'Type'), +}) diff --git a/pcapkit/const/linux/__init__.py b/pcapkit/const/linux/__init__.py index 52f5b90571..8741702dd6 100644 --- a/pcapkit/const/linux/__init__.py +++ b/pcapkit/const/linux/__init__.py @@ -14,6 +14,15 @@ """ -from pcapkit.const.linux.arphrd import ARPHRD as Linux_ARPHRD +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.linux.arphrd import ARPHRD as Linux_ARPHRD __all__ = ['Linux_ARPHRD'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'Linux_ARPHRD': ('arphrd', 'ARPHRD'), +}) diff --git a/pcapkit/const/mh/__init__.py b/pcapkit/const/mh/__init__.py index a89d91dd77..11a36b5232 100644 --- a/pcapkit/const/mh/__init__.py +++ b/pcapkit/const/mh/__init__.py @@ -141,52 +141,60 @@ """ -from pcapkit.const.mh.access_type import AccessType as MH_AccessType -from pcapkit.const.mh.ack_status_code import ACKStatusCode as MH_ACKStatusCode -from pcapkit.const.mh.ani_suboption import ANISuboption as MH_ANISuboption -from pcapkit.const.mh.auth_subtype import AuthSubtype as MH_AuthSubtype -from pcapkit.const.mh.binding_ack_flag import BindingACKFlag as MH_BindingACKFlag -from pcapkit.const.mh.binding_error import BindingError as MH_BindingError -from pcapkit.const.mh.binding_revocation import BindingRevocation as MH_BindingRevocation -from pcapkit.const.mh.binding_update_flag import BindingUpdateFlag as MH_BindingUpdateFlag -from pcapkit.const.mh.cga_extension import CGAExtension as MH_CGAExtension -from pcapkit.const.mh.cga_sec import CGASec as MH_CGASec -from pcapkit.const.mh.cga_type import CGAType as MH_CGAType -from pcapkit.const.mh.dhcp_support_mode import DHCPSupportMode as MH_DHCPSupportMode -from pcapkit.const.mh.dns_status_code import DNSStatusCode as MH_DNSStatusCode -from pcapkit.const.mh.dsmip6_tls_packet import DSMIP6TLSPacket as MH_DSMIP6TLSPacket -from pcapkit.const.mh.dsmipv6_home_address import DSMIPv6HomeAddress as MH_DSMIPv6HomeAddress -from pcapkit.const.mh.enumerating_algorithm import EnumeratingAlgorithm as MH_EnumeratingAlgorithm -from pcapkit.const.mh.fb_ack_status import FlowBindingACKStatus as MH_FlowBindingACKStatus -from pcapkit.const.mh.fb_action import FlowBindingAction as MH_FlowBindingAction -from pcapkit.const.mh.fb_indication_trigger import \ - FlowBindingIndicationTrigger as MH_FlowBindingIndicationTrigger -from pcapkit.const.mh.fb_type import FlowBindingType as MH_FlowBindingType -from pcapkit.const.mh.flow_id_status import FlowIDStatus as MH_FlowIDStatus -from pcapkit.const.mh.flow_id_suboption import FlowIDSuboption as MH_FlowIDSuboption -from pcapkit.const.mh.handoff_type import HandoffType as MH_HandoffType -from pcapkit.const.mh.handover_ack_flag import HandoverACKFlag as MH_HandoverACKFlag -from pcapkit.const.mh.handover_ack_status import HandoverACKStatus as MH_HandoverACKStatus -from pcapkit.const.mh.handover_initiate_flag import HandoverInitiateFlag as MH_HandoverInitiateFlag -from pcapkit.const.mh.handover_initiate_status import \ - HandoverInitiateStatus as MH_HandoverInitiateStatus -from pcapkit.const.mh.home_address_reply import HomeAddressReply as MH_HomeAddressReply -from pcapkit.const.mh.lla_code import LLACode as MH_LLACode -from pcapkit.const.mh.lma_mag_suboption import \ - LMAControlledMAGSuboption as MH_LMAControlledMAGSuboption -from pcapkit.const.mh.mn_group_id import MNGroupID as MH_MNGroupID -from pcapkit.const.mh.mn_id_subtype import MNIDSubtype as MH_MNIDSubtype -from pcapkit.const.mh.operator_id import OperatorID as MH_OperatorID -from pcapkit.const.mh.option import Option as MH_Option -from pcapkit.const.mh.packet import Packet as MH_Packet -from pcapkit.const.mh.qos_attribute import QoSAttribute as MH_QoSAttribute -from pcapkit.const.mh.revocation_status_code import RevocationStatusCode as MH_RevocationStatusCode -from pcapkit.const.mh.revocation_trigger import RevocationTrigger as MH_RevocationTrigger -from pcapkit.const.mh.status_code import StatusCode as MH_StatusCode -from pcapkit.const.mh.traffic_selector import TrafficSelector as MH_TrafficSelector -from pcapkit.const.mh.upa_status import \ - UpdateNotificationACKStatus as MH_UpdateNotificationACKStatus -from pcapkit.const.mh.upn_reason import UpdateNotificationReason as MH_UpdateNotificationReason +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.mh.access_type import AccessType as MH_AccessType + from pcapkit.const.mh.ack_status_code import ACKStatusCode as MH_ACKStatusCode + from pcapkit.const.mh.ani_suboption import ANISuboption as MH_ANISuboption + from pcapkit.const.mh.auth_subtype import AuthSubtype as MH_AuthSubtype + from pcapkit.const.mh.binding_ack_flag import BindingACKFlag as MH_BindingACKFlag + from pcapkit.const.mh.binding_error import BindingError as MH_BindingError + from pcapkit.const.mh.binding_revocation import BindingRevocation as MH_BindingRevocation + from pcapkit.const.mh.binding_update_flag import BindingUpdateFlag as MH_BindingUpdateFlag + from pcapkit.const.mh.cga_extension import CGAExtension as MH_CGAExtension + from pcapkit.const.mh.cga_sec import CGASec as MH_CGASec + from pcapkit.const.mh.cga_type import CGAType as MH_CGAType + from pcapkit.const.mh.dhcp_support_mode import DHCPSupportMode as MH_DHCPSupportMode + from pcapkit.const.mh.dns_status_code import DNSStatusCode as MH_DNSStatusCode + from pcapkit.const.mh.dsmip6_tls_packet import DSMIP6TLSPacket as MH_DSMIP6TLSPacket + from pcapkit.const.mh.dsmipv6_home_address import DSMIPv6HomeAddress as MH_DSMIPv6HomeAddress + from pcapkit.const.mh.enumerating_algorithm import \ + EnumeratingAlgorithm as MH_EnumeratingAlgorithm + from pcapkit.const.mh.fb_ack_status import FlowBindingACKStatus as MH_FlowBindingACKStatus + from pcapkit.const.mh.fb_action import FlowBindingAction as MH_FlowBindingAction + from pcapkit.const.mh.fb_indication_trigger import \ + FlowBindingIndicationTrigger as MH_FlowBindingIndicationTrigger + from pcapkit.const.mh.fb_type import FlowBindingType as MH_FlowBindingType + from pcapkit.const.mh.flow_id_status import FlowIDStatus as MH_FlowIDStatus + from pcapkit.const.mh.flow_id_suboption import FlowIDSuboption as MH_FlowIDSuboption + from pcapkit.const.mh.handoff_type import HandoffType as MH_HandoffType + from pcapkit.const.mh.handover_ack_flag import HandoverACKFlag as MH_HandoverACKFlag + from pcapkit.const.mh.handover_ack_status import HandoverACKStatus as MH_HandoverACKStatus + from pcapkit.const.mh.handover_initiate_flag import \ + HandoverInitiateFlag as MH_HandoverInitiateFlag + from pcapkit.const.mh.handover_initiate_status import \ + HandoverInitiateStatus as MH_HandoverInitiateStatus + from pcapkit.const.mh.home_address_reply import HomeAddressReply as MH_HomeAddressReply + from pcapkit.const.mh.lla_code import LLACode as MH_LLACode + from pcapkit.const.mh.lma_mag_suboption import \ + LMAControlledMAGSuboption as MH_LMAControlledMAGSuboption + from pcapkit.const.mh.mn_group_id import MNGroupID as MH_MNGroupID + from pcapkit.const.mh.mn_id_subtype import MNIDSubtype as MH_MNIDSubtype + from pcapkit.const.mh.operator_id import OperatorID as MH_OperatorID + from pcapkit.const.mh.option import Option as MH_Option + from pcapkit.const.mh.packet import Packet as MH_Packet + from pcapkit.const.mh.qos_attribute import QoSAttribute as MH_QoSAttribute + from pcapkit.const.mh.revocation_status_code import \ + RevocationStatusCode as MH_RevocationStatusCode + from pcapkit.const.mh.revocation_trigger import RevocationTrigger as MH_RevocationTrigger + from pcapkit.const.mh.status_code import StatusCode as MH_StatusCode + from pcapkit.const.mh.traffic_selector import TrafficSelector as MH_TrafficSelector + from pcapkit.const.mh.upa_status import \ + UpdateNotificationACKStatus as MH_UpdateNotificationACKStatus + from pcapkit.const.mh.upn_reason import UpdateNotificationReason as MH_UpdateNotificationReason __all__ = [ 'MH_Packet', 'MH_Option', 'MH_DNSStatusCode', 'MH_ACKStatusCode', @@ -204,3 +212,48 @@ 'MH_LMAControlledMAGSuboption', 'MH_LLACode', 'MH_CGAType', 'MH_CGAExtension', 'MH_CGASec', 'MH_BindingError', ] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'MH_Packet': ('packet', 'Packet'), + 'MH_Option': ('option', 'Option'), + 'MH_DNSStatusCode': ('dns_status_code', 'DNSStatusCode'), + 'MH_ACKStatusCode': ('ack_status_code', 'ACKStatusCode'), + 'MH_MNIDSubtype': ('mn_id_subtype', 'MNIDSubtype'), + 'MH_StatusCode': ('status_code', 'StatusCode'), + 'MH_EnumeratingAlgorithm': ('enumerating_algorithm', 'EnumeratingAlgorithm'), + 'MH_AuthSubtype': ('auth_subtype', 'AuthSubtype'), + 'MH_HandoffType': ('handoff_type', 'HandoffType'), + 'MH_AccessType': ('access_type', 'AccessType'), + 'MH_BindingUpdateFlag': ('binding_update_flag', 'BindingUpdateFlag'), + 'MH_BindingACKFlag': ('binding_ack_flag', 'BindingACKFlag'), + 'MH_DSMIPv6HomeAddress': ('dsmipv6_home_address', 'DSMIPv6HomeAddress'), + 'MH_BindingRevocation': ('binding_revocation', 'BindingRevocation'), + 'MH_RevocationTrigger': ('revocation_trigger', 'RevocationTrigger'), + 'MH_RevocationStatusCode': ('revocation_status_code', 'RevocationStatusCode'), + 'MH_HomeAddressReply': ('home_address_reply', 'HomeAddressReply'), + 'MH_DHCPSupportMode': ('dhcp_support_mode', 'DHCPSupportMode'), + 'MH_HandoverInitiateFlag': ('handover_initiate_flag', 'HandoverInitiateFlag'), + 'MH_HandoverACKFlag': ('handover_ack_flag', 'HandoverACKFlag'), + 'MH_HandoverInitiateStatus': ('handover_initiate_status', 'HandoverInitiateStatus'), + 'MH_HandoverACKStatus': ('handover_ack_status', 'HandoverACKStatus'), + 'MH_FlowIDStatus': ('flow_id_status', 'FlowIDStatus'), + 'MH_FlowIDSuboption': ('flow_id_suboption', 'FlowIDSuboption'), + 'MH_TrafficSelector': ('traffic_selector', 'TrafficSelector'), + 'MH_MNGroupID': ('mn_group_id', 'MNGroupID'), + 'MH_DSMIP6TLSPacket': ('dsmip6_tls_packet', 'DSMIP6TLSPacket'), + 'MH_ANISuboption': ('ani_suboption', 'ANISuboption'), + 'MH_OperatorID': ('operator_id', 'OperatorID'), + 'MH_UpdateNotificationReason': ('upn_reason', 'UpdateNotificationReason'), + 'MH_UpdateNotificationACKStatus': ('upa_status', 'UpdateNotificationACKStatus'), + 'MH_FlowBindingType': ('fb_type', 'FlowBindingType'), + 'MH_FlowBindingIndicationTrigger': ('fb_indication_trigger', 'FlowBindingIndicationTrigger'), + 'MH_FlowBindingACKStatus': ('fb_ack_status', 'FlowBindingACKStatus'), + 'MH_FlowBindingAction': ('fb_action', 'FlowBindingAction'), + 'MH_QoSAttribute': ('qos_attribute', 'QoSAttribute'), + 'MH_LMAControlledMAGSuboption': ('lma_mag_suboption', 'LMAControlledMAGSuboption'), + 'MH_LLACode': ('lla_code', 'LLACode'), + 'MH_CGAType': ('cga_type', 'CGAType'), + 'MH_CGAExtension': ('cga_extension', 'CGAExtension'), + 'MH_CGASec': ('cga_sec', 'CGASec'), + 'MH_BindingError': ('binding_error', 'BindingError'), +}) diff --git a/pcapkit/const/ngap/__init__.py b/pcapkit/const/ngap/__init__.py index 8691e11703..7d2c089364 100644 --- a/pcapkit/const/ngap/__init__.py +++ b/pcapkit/const/ngap/__init__.py @@ -28,7 +28,17 @@ """ -from pcapkit.const.ngap.procedure_code import ProcedureCode as NGAP_ProcedureCode -from pcapkit.const.ngap.protocol_ie import ProtocolIE as NGAP_ProtocolIE +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.ngap.procedure_code import ProcedureCode as NGAP_ProcedureCode + from pcapkit.const.ngap.protocol_ie import ProtocolIE as NGAP_ProtocolIE __all__ = ['NGAP_ProcedureCode', 'NGAP_ProtocolIE'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'NGAP_ProcedureCode': ('procedure_code', 'ProcedureCode'), + 'NGAP_ProtocolIE': ('protocol_ie', 'ProtocolIE'), +}) diff --git a/pcapkit/const/ospf/__init__.py b/pcapkit/const/ospf/__init__.py index 92796b7d19..8553f78af3 100644 --- a/pcapkit/const/ospf/__init__.py +++ b/pcapkit/const/ospf/__init__.py @@ -21,7 +21,17 @@ """ -from pcapkit.const.ospf.authentication import Authentication as OSPF_Authentication -from pcapkit.const.ospf.packet import Packet as OSPF_Packet +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.ospf.authentication import Authentication as OSPF_Authentication + from pcapkit.const.ospf.packet import Packet as OSPF_Packet __all__ = ['OSPF_Authentication', 'OSPF_Packet'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'OSPF_Authentication': ('authentication', 'Authentication'), + 'OSPF_Packet': ('packet', 'Packet'), +}) diff --git a/pcapkit/const/pcapng/__init__.py b/pcapkit/const/pcapng/__init__.py index d7f2c1921e..4620b333cc 100644 --- a/pcapkit/const/pcapng/__init__.py +++ b/pcapkit/const/pcapng/__init__.py @@ -39,17 +39,33 @@ """ -from pcapkit.const.pcapng.block_type import BlockType as PCAPNG_BlockType -from pcapkit.const.pcapng.filter_type import FilterType as PCAPNG_FilterType -from pcapkit.const.pcapng.hash_algorithm import HashAlgorithm as PCAPNG_HashAlgorithm -from pcapkit.const.pcapng.option_type import OptionType as PCAPNG_OptionType -from pcapkit.const.pcapng.record_type import RecordType as PCAPNG_RecordType -from pcapkit.const.pcapng.secrets_type import SecretsType as PCAPNG_SecretsType -from pcapkit.const.pcapng.tls_key_label import TLSKeyLabel as PCAPNG_TLSKeyLabel -from pcapkit.const.pcapng.verdict_type import VerdictType as PCAPNG_VerdictType +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.pcapng.block_type import BlockType as PCAPNG_BlockType + from pcapkit.const.pcapng.filter_type import FilterType as PCAPNG_FilterType + from pcapkit.const.pcapng.hash_algorithm import HashAlgorithm as PCAPNG_HashAlgorithm + from pcapkit.const.pcapng.option_type import OptionType as PCAPNG_OptionType + from pcapkit.const.pcapng.record_type import RecordType as PCAPNG_RecordType + from pcapkit.const.pcapng.secrets_type import SecretsType as PCAPNG_SecretsType + from pcapkit.const.pcapng.tls_key_label import TLSKeyLabel as PCAPNG_TLSKeyLabel + from pcapkit.const.pcapng.verdict_type import VerdictType as PCAPNG_VerdictType __all__ = [ 'PCAPNG_BlockType', 'PCAPNG_OptionType', 'PCAPNG_HashAlgorithm', 'PCAPNG_VerdictType', 'PCAPNG_RecordType', 'PCAPNG_SecretsType', 'PCAPNG_FilterType', 'PCAPNG_TLSKeyLabel', ] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'PCAPNG_BlockType': ('block_type', 'BlockType'), + 'PCAPNG_OptionType': ('option_type', 'OptionType'), + 'PCAPNG_HashAlgorithm': ('hash_algorithm', 'HashAlgorithm'), + 'PCAPNG_VerdictType': ('verdict_type', 'VerdictType'), + 'PCAPNG_RecordType': ('record_type', 'RecordType'), + 'PCAPNG_SecretsType': ('secrets_type', 'SecretsType'), + 'PCAPNG_FilterType': ('filter_type', 'FilterType'), + 'PCAPNG_TLSKeyLabel': ('tls_key_label', 'TLSKeyLabel'), +}) diff --git a/pcapkit/const/radiotap/__init__.py b/pcapkit/const/radiotap/__init__.py index 453d4d66f3..4e9c4465ce 100644 --- a/pcapkit/const/radiotap/__init__.py +++ b/pcapkit/const/radiotap/__init__.py @@ -54,27 +54,34 @@ """ -from pcapkit.const.radiotap.ampdu_flags import AMPDUFlags as Radiotap_AMPDUFlags -from pcapkit.const.radiotap.channel_flags import ChannelFlags as Radiotap_ChannelFlags -from pcapkit.const.radiotap.eht_known import EHTKnown as Radiotap_EHTKnown -from pcapkit.const.radiotap.field_type import FieldType as Radiotap_FieldType -from pcapkit.const.radiotap.flags import Flags as Radiotap_Flags -from pcapkit.const.radiotap.he_data1_known import HEData1Known as Radiotap_HEData1Known -from pcapkit.const.radiotap.he_data2_known import HEData2Known as Radiotap_HEData2Known -from pcapkit.const.radiotap.he_mu_flags1_known import HEMUFlags1Known as Radiotap_HEMUFlags1Known -from pcapkit.const.radiotap.he_mu_flags2_known import HEMUFlags2Known as Radiotap_HEMUFlags2Known -from pcapkit.const.radiotap.he_mu_other_user_known import \ - HEMUOtherUserKnown as Radiotap_HEMUOtherUserKnown -from pcapkit.const.radiotap.lsig_data1 import LSIGData1 as Radiotap_LSIGData1 -from pcapkit.const.radiotap.mcs_known import MCSKnown as Radiotap_MCSKnown -from pcapkit.const.radiotap.rx_flags import RXFlags as Radiotap_RXFlags -from pcapkit.const.radiotap.s1g_known import S1GKnown as Radiotap_S1GKnown -from pcapkit.const.radiotap.timestamp_flags import TimestampFlags as Radiotap_TimestampFlags -from pcapkit.const.radiotap.tx_flags import TXFlags as Radiotap_TXFlags -from pcapkit.const.radiotap.usig_common_known import USIGCommonKnown as Radiotap_USIGCommonKnown -from pcapkit.const.radiotap.vht_coding import VHTCoding as Radiotap_VHTCoding -from pcapkit.const.radiotap.vht_flags import VHTFlags as Radiotap_VHTFlags -from pcapkit.const.radiotap.vht_known import VHTKnown as Radiotap_VHTKnown +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.radiotap.ampdu_flags import AMPDUFlags as Radiotap_AMPDUFlags + from pcapkit.const.radiotap.channel_flags import ChannelFlags as Radiotap_ChannelFlags + from pcapkit.const.radiotap.eht_known import EHTKnown as Radiotap_EHTKnown + from pcapkit.const.radiotap.field_type import FieldType as Radiotap_FieldType + from pcapkit.const.radiotap.flags import Flags as Radiotap_Flags + from pcapkit.const.radiotap.he_data1_known import HEData1Known as Radiotap_HEData1Known + from pcapkit.const.radiotap.he_data2_known import HEData2Known as Radiotap_HEData2Known + from pcapkit.const.radiotap.he_mu_flags1_known import \ + HEMUFlags1Known as Radiotap_HEMUFlags1Known + from pcapkit.const.radiotap.he_mu_flags2_known import \ + HEMUFlags2Known as Radiotap_HEMUFlags2Known + from pcapkit.const.radiotap.he_mu_other_user_known import \ + HEMUOtherUserKnown as Radiotap_HEMUOtherUserKnown + from pcapkit.const.radiotap.lsig_data1 import LSIGData1 as Radiotap_LSIGData1 + from pcapkit.const.radiotap.mcs_known import MCSKnown as Radiotap_MCSKnown + from pcapkit.const.radiotap.rx_flags import RXFlags as Radiotap_RXFlags + from pcapkit.const.radiotap.s1g_known import S1GKnown as Radiotap_S1GKnown + from pcapkit.const.radiotap.timestamp_flags import TimestampFlags as Radiotap_TimestampFlags + from pcapkit.const.radiotap.tx_flags import TXFlags as Radiotap_TXFlags + from pcapkit.const.radiotap.usig_common_known import USIGCommonKnown as Radiotap_USIGCommonKnown + from pcapkit.const.radiotap.vht_coding import VHTCoding as Radiotap_VHTCoding + from pcapkit.const.radiotap.vht_flags import VHTFlags as Radiotap_VHTFlags + from pcapkit.const.radiotap.vht_known import VHTKnown as Radiotap_VHTKnown __all__ = [ 'Radiotap_FieldType', 'Radiotap_Flags', 'Radiotap_ChannelFlags', 'Radiotap_RXFlags', @@ -84,3 +91,26 @@ 'Radiotap_HEMUOtherUserKnown', 'Radiotap_LSIGData1', 'Radiotap_S1GKnown', 'Radiotap_USIGCommonKnown', 'Radiotap_EHTKnown', ] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'Radiotap_FieldType': ('field_type', 'FieldType'), + 'Radiotap_Flags': ('flags', 'Flags'), + 'Radiotap_ChannelFlags': ('channel_flags', 'ChannelFlags'), + 'Radiotap_RXFlags': ('rx_flags', 'RXFlags'), + 'Radiotap_TXFlags': ('tx_flags', 'TXFlags'), + 'Radiotap_MCSKnown': ('mcs_known', 'MCSKnown'), + 'Radiotap_AMPDUFlags': ('ampdu_flags', 'AMPDUFlags'), + 'Radiotap_VHTKnown': ('vht_known', 'VHTKnown'), + 'Radiotap_VHTFlags': ('vht_flags', 'VHTFlags'), + 'Radiotap_VHTCoding': ('vht_coding', 'VHTCoding'), + 'Radiotap_TimestampFlags': ('timestamp_flags', 'TimestampFlags'), + 'Radiotap_HEData1Known': ('he_data1_known', 'HEData1Known'), + 'Radiotap_HEData2Known': ('he_data2_known', 'HEData2Known'), + 'Radiotap_HEMUFlags1Known': ('he_mu_flags1_known', 'HEMUFlags1Known'), + 'Radiotap_HEMUFlags2Known': ('he_mu_flags2_known', 'HEMUFlags2Known'), + 'Radiotap_HEMUOtherUserKnown': ('he_mu_other_user_known', 'HEMUOtherUserKnown'), + 'Radiotap_LSIGData1': ('lsig_data1', 'LSIGData1'), + 'Radiotap_S1GKnown': ('s1g_known', 'S1GKnown'), + 'Radiotap_USIGCommonKnown': ('usig_common_known', 'USIGCommonKnown'), + 'Radiotap_EHTKnown': ('eht_known', 'EHTKnown'), +}) diff --git a/pcapkit/const/reg/__init__.py b/pcapkit/const/reg/__init__.py index 2d24158544..82342d05a7 100644 --- a/pcapkit/const/reg/__init__.py +++ b/pcapkit/const/reg/__init__.py @@ -34,9 +34,21 @@ """ -from pcapkit.const.reg.apptype import AppType as APPTYPE -from pcapkit.const.reg.ethertype import EtherType as ETHERTYPE -from pcapkit.const.reg.linktype import LinkType as LINKTYPE -from pcapkit.const.reg.transtype import TransType as TRANSTYPE +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.reg.apptype import AppType as APPTYPE + from pcapkit.const.reg.ethertype import EtherType as ETHERTYPE + from pcapkit.const.reg.linktype import LinkType as LINKTYPE + from pcapkit.const.reg.transtype import TransType as TRANSTYPE __all__ = ['ETHERTYPE', 'LINKTYPE', 'TRANSTYPE', 'APPTYPE'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'ETHERTYPE': ('ethertype', 'EtherType'), + 'LINKTYPE': ('linktype', 'LinkType'), + 'TRANSTYPE': ('transtype', 'TransType'), + 'APPTYPE': ('apptype', 'AppType'), +}) diff --git a/pcapkit/const/reg/apptype/__init__.py b/pcapkit/const/reg/apptype/__init__.py index 7bc2d8c749..46ae2b1434 100644 --- a/pcapkit/const/reg/apptype/__init__.py +++ b/pcapkit/const/reg/apptype/__init__.py @@ -32,163 +32,136 @@ right registry through :meth:`AppType.get `. -:class:`~pcapkit.const.reg.apptype.tcp.TCP` and -:class:`~pcapkit.const.reg.apptype.udp.UDP` are imported the first time either -is reached -- as an attribute of this package, or through :meth:`AppType.get -` -- rather than with the package -itself, since they are most of its import time (GitHub issue :issue:`1538`). +Each per-transport registry -- :class:`~pcapkit.const.reg.apptype.tcp.TCP`, +:class:`~pcapkit.const.reg.apptype.udp.UDP`, +:class:`~pcapkit.const.reg.apptype.sctp.SCTP` and +:class:`~pcapkit.const.reg.apptype.dccp.DCCP` -- is imported the first time it +is reached, as an attribute of this package or through :meth:`AppType.get +`, rather than with the package +itself: TCP and UDP for being most of its import time (GitHub issue +:issue:`1538`), and SCTP and DCCP as every :mod:`pcapkit.const` enumeration is +(:issue:`2729`). """ -import importlib -import sys +import itertools from typing import TYPE_CHECKING from pcapkit.const.reg.apptype.apptype import AppType, TransportProtocol -from pcapkit.const.reg.apptype.dccp import DCCP -from pcapkit.const.reg.apptype.sctp import SCTP +from pcapkit.corekit.infoclass import Info, info_final from pcapkit.corekit.module import ModuleDescriptor +from pcapkit.utilities.lazy import lazy_exports if TYPE_CHECKING: - from typing import Any + from typing import Any, ItemsView, Iterable, Iterator, Mapping, Optional, SupportsIndex, Type + from pcapkit.const.reg.apptype.dccp import DCCP + from pcapkit.const.reg.apptype.sctp import SCTP from pcapkit.const.reg.apptype.tcp import TCP from pcapkit.const.reg.apptype.udp import UDP + from pcapkit.corekit.multidict import OrderedMultiDict __all__ = ['AppType', 'TransportProtocol', 'TCP', 'UDP', 'SCTP', 'DCCP'] -#: Registries imported on first use rather than with this package, as submodule -#: name to class name -- GitHub issue :issue:`1538`. Generated from IANA's -#: registry, they hold about 49,700 lines of members, about 30% of -#: ``import pcapkit`` when imported eagerly, while SCTP's and DCCP's cost a few -#: milliseconds and stay eager. -_LAZY = {'tcp': 'TCP', 'udp': 'UDP'} +#: Registries imported on first use rather than with this package, as public +#: name to submodule and class name: every per-transport registry (GitHub issues +#: :issue:`1538` and :issue:`2729`). +_LAZY = {'TCP': ('tcp', 'TCP'), 'UDP': ('udp', 'UDP'), 'SCTP': ('sctp', 'SCTP'), 'DCCP': ('dccp', 'DCCP')} -class _Registries(dict): +@info_final +class _Registries(Info['Type[AppType]']): """:attr:`AppType.__registries__ `, importing a lazy registry the first time its value is read. - Every key is present from the start, in the order - :class:`~pcapkit.const.reg.apptype.apptype.TransportProtocol` declares them, - so :func:`len`, ``in`` and iteration import nothing. A lazy - registry's value is a :class:`~pcapkit.corekit.module.ModuleDescriptor` - until the first read replaces it with the class. Every way of reading a - value is overridden, :meth:`__iter__` included: overriding it is what makes - ``dict(...)`` and ``{**...}`` go through :meth:`__getitem__` instead of - copying the descriptors in C, so no caller ever sees one. + An :class:`~pcapkit.corekit.infoclass.Info` keyed by + :class:`~pcapkit.const.reg.apptype.apptype.TransportProtocol` members, in + the order it is built from. Each registry is held under a name made from its + member's, as an :class:`~pcapkit.corekit.infoclass.Info` holds a key named + like one of its methods, and the ``__map__`` and ``__map_reverse__`` it keeps + for those pair the two: a lookup by member or by bare :class:`int` finds the + registry, and iteration yields the members. :func:`len`, ``in`` and + iteration import nothing. + + A lazy registry is held as a :class:`~pcapkit.corekit.module.ModuleDescriptor` + until the first read of its value replaces it with the class. Every read of a + value -- indexing, ``get()``, ``values()``, ``items()``, ``==``, + :func:`repr`, :meth:`to_dict`, a copy or a pickle -- goes through + :meth:`__getitem__` or imports every registry first, so no caller ever sees a + descriptor. + + It is immutable, as every :class:`~pcapkit.corekit.infoclass.Info` is, and no + :obj:`dict` subclass (GitHub issue :issue:`2726`): it has none of the + :obj:`dict` methods that change a mapping, nor ``copy()`` or the ``|`` + operator. """ - __slots__ = () - - def _resolve(self, key: 'Any', value: 'Any') -> 'Any': - if isinstance(value, ModuleDescriptor): - value = value.klass - dict.__setitem__(self, key, value) - return value - - def _resolve_all(self) -> 'None': - for key, value in list(dict.items(self)): - self._resolve(key, value) - - def __getitem__(self, key: 'Any') -> 'Any': - return self._resolve(key, dict.__getitem__(self, key)) - - def get(self, key: 'Any', default: 'Any' = None) -> 'Any': - value = dict.get(self, key, default) - # NOTE: the second test keeps a caller's own ``default`` from being - # resolved and stored, should it ever be a descriptor itself. - if isinstance(value, ModuleDescriptor) and dict.__contains__(self, key): - value = self._resolve(key, value) - return value - - def __iter__(self) -> 'Any': - return dict.__iter__(self) - - def values(self) -> 'Any': - self._resolve_all() - return dict.values(self) - - def items(self) -> 'Any': - self._resolve_all() - return dict.items(self) - - def copy(self) -> 'dict[Any, Any]': - self._resolve_all() - return dict.copy(self) - - def pop(self, *args: 'Any') -> 'Any': - self._resolve_all() - return dict.pop(self, *args) + if TYPE_CHECKING: + def __init__(self, dict_: 'Mapping[TransportProtocol, Type[AppType] | ModuleDescriptor[AppType]]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long + + def __update__(self, dict_: 'Optional[Mapping[Any, Any] | Iterable[tuple[Any, Any]]]' = None, + **kwargs: 'Type[AppType] | ModuleDescriptor[AppType]') -> 'None': + """Hold each registry under the name made from its transport protocol. + + Args: + dict_: Registries, each keyed by a + :class:`~pcapkit.const.reg.apptype.apptype.TransportProtocol` + member, or by its value or name. + **kwargs: More registries, each keyed by its transport protocol's name. + + """ + for (key, registry) in itertools.chain(dict(dict_ or ()).items(), kwargs.items()): + proto = TransportProtocol.get(key) + name = f'_{type(self).__name__}{proto.name}' + self.__map__[proto] = name + self.__map_reverse__[name] = proto + self.__dict__[name] = registry + + def __getitem__(self, key: 'Any') -> 'Type[AppType]': + # NOTE: ``__map__`` holds the transport protocols alone, so any other + # key, the names of the bookkeeping attributes included, raises + # :exc:`KeyError` as a missing :obj:`dict` key does. + name = self.__map__[key] + registry = self.__dict__[name] + if isinstance(registry, ModuleDescriptor): + registry = registry.klass + self.__dict__[name] = registry + return registry - def popitem(self) -> 'Any': - self._resolve_all() - return dict.popitem(self) + def __contains__(self, key: 'object') -> 'bool': + return key in self.__map__ - def setdefault(self, key: 'Any', default: 'Any' = None) -> 'Any': - self._resolve_all() - return dict.setdefault(self, key, default) + def __repr__(self) -> 'str': + return repr(dict(self.items())) - def __eq__(self, other: 'object') -> 'bool': - self._resolve_all() - return dict.__eq__(self, other) + __str__ = __repr__ - def __ne__(self, other: 'object') -> 'bool': + def __reduce_ex__(self, protocol: 'SupportsIndex') -> 'Any': self._resolve_all() - return dict.__ne__(self, other) - - # NOTE: dict union is :pep:`584`, Python 3.9 and up, so a registry has it - # from 3.9 only, as :obj:`dict` does (GitHub issue #1694): before - # 3.9, ``|`` and ``|=`` raise dict's :exc:`TypeError` and import nothing. - # ``|=`` stays dict's own, since it updates in place and reads no value. - if sys.version_info >= (3, 9): # pragma: no branch -- Python 3.9 and up - def __or__(self, other: 'Any') -> 'Any': - self._resolve_all() - return dict.__or__(self, other) + return super().__reduce_ex__(protocol) - def __ror__(self, other: 'Any') -> 'Any': + def items(self, multi: 'bool' = False) -> 'ItemsView[str, Type[AppType]] | Iterator[tuple[str, Type[AppType]]]': # type: ignore[override] # pylint: disable=line-too-long + if multi: self._resolve_all() - return dict.__ror__(self, other) + return super().items(multi) - def __repr__(self) -> 'str': + def to_dict(self) -> 'OrderedMultiDict[str, Type[AppType]]': self._resolve_all() - return dict.__repr__(self) + return super().to_dict() + + def _resolve_all(self) -> 'None': + """Import every pending registry, as reading each value does.""" + list(self.values()) # NOTE: this is what makes ``AppType.get(port, proto=...)`` work on the base # class, which is where every caller in the library addresses it. It cannot live # in ``apptype.py``: that module is imported *by* every per-transport registry, -# so it cannot import them back. Here is the first point at which all of them -# exist, or, for the lazy ones, where to import them from. -AppType.__registries__ = _Registries({ - TransportProtocol.tcp: ModuleDescriptor(f'{__name__}.tcp', _LAZY['tcp']), - TransportProtocol.udp: ModuleDescriptor(f'{__name__}.udp', _LAZY['udp']), - TransportProtocol.sctp: SCTP, - TransportProtocol.dccp: DCCP, +# so it cannot import them back. Here is where to import each of them from. +AppType.__registries__ = _Registries({ # type: ignore[assignment] # Info types its keys as str + TransportProtocol.get(module): ModuleDescriptor(f'{__name__}.{module}', klass) + for (module, klass) in _LAZY.values() }) - -def __getattr__(name: 'str') -> 'Any': - """Import a :data:`_LAZY` registry, or its submodule, on first access. - - :pep:`562` calls this only for a name missing from this module's globals, - so it runs once per name: importing a submodule binds it here, and the - class is bound here before it is returned. - - """ - if name in _LAZY: - return importlib.import_module(f'{__name__}.{name}') - module = name.lower() - if _LAZY.get(module) == name: - registry = getattr(importlib.import_module(f'{__name__}.{module}'), name) - globals()[name] = registry - return registry - raise AttributeError(f'module {__name__!r} has no attribute {name!r}') - - -def __dir__() -> 'list[str]': - """The names an eager import of every per-transport registry would list.""" - helpers = {'importlib', 'sys', 'TYPE_CHECKING', 'ModuleDescriptor', '_LAZY', '_Registries', - '__getattr__', '__dir__'} - return sorted({*globals(), *_LAZY, *_LAZY.values()} - helpers) +__getattr__, __dir__ = lazy_exports(__name__, globals(), _LAZY) diff --git a/pcapkit/const/reg/apptype/apptype.py b/pcapkit/const/reg/apptype/apptype.py index 4bc93ca316..69dabdcce1 100644 --- a/pcapkit/const/reg/apptype/apptype.py +++ b/pcapkit/const/reg/apptype/apptype.py @@ -22,7 +22,7 @@ __all__ = ['TransportProtocol', 'AppType'] if TYPE_CHECKING: - from typing import Any, Optional, Type + from typing import Any, Mapping, Optional, Type from pcapkit.corekit.multidict import MultiDict @@ -2374,9 +2374,9 @@ class AppType(EnumRegistry, StrEnum): #: Registry class owning each transport protocol, i.e. what #: :meth:`get` delegates to when called on this class rather than on one of - #: them. Populated by :mod:`pcapkit.const.reg.apptype` once every one of - #: them has been imported, since this module cannot import its own subclasses. - __registries__: 'dict[TransportProtocol, Type[AppType]]' = {} + #: them. Populated by :mod:`pcapkit.const.reg.apptype`, which imports each + #: of them on first use, since this module cannot import its own subclasses. + __registries__: 'Mapping[TransportProtocol, Type[AppType]]' = {} #: Canonical service per port, for the ports carrying more than one. The base #: registry has no members and so no collisions. @@ -2562,13 +2562,13 @@ def _dispatch(cls, key: 'int', proto: 'TransportProtocol | str | int') -> 'Type[ if isinstance(proto, str): proto = TransportProtocol.get(proto.lower()) - # NOTE: a direct dict lookup covers every genuine single transport -- + # NOTE: a direct lookup covers every genuine single transport -- # one of the real members, or a bare int equal to one of their # values -- since ``__registries__`` keys compare by the same # int-valued hash/eq every member and bare int alike already use. The - # cast is for mypy alone: ``dict.get`` accepts any hashable key at run + # cast is for mypy alone: ``.get`` accepts any hashable key at run # time regardless of its declared key type, but mypy holds ``.get`` to - # the dict's own ``TransportProtocol`` keys, and does not know ``proto`` + # the mapping's own ``TransportProtocol`` keys, and does not know ``proto`` # can genuinely be a bare :class:`int` here, TransportProtocol not being # an ``IntFlag`` (GitHub issue #808) -- see the annotation on ``proto`` # above. diff --git a/pcapkit/const/sctp/__init__.py b/pcapkit/const/sctp/__init__.py index d4ecb2fbf6..23ce0c4b81 100644 --- a/pcapkit/const/sctp/__init__.py +++ b/pcapkit/const/sctp/__init__.py @@ -27,11 +27,23 @@ """ -from pcapkit.const.sctp.cause_code import CauseCode as SCTP_CauseCode -from pcapkit.const.sctp.chunk import Chunk as SCTP_Chunk -from pcapkit.const.sctp.parameter import Parameter as SCTP_Parameter -from pcapkit.const.sctp.payload_protocol_identifier import \ - PayloadProtocolIdentifier as SCTP_PayloadProtocolIdentifier +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.sctp.cause_code import CauseCode as SCTP_CauseCode + from pcapkit.const.sctp.chunk import Chunk as SCTP_Chunk + from pcapkit.const.sctp.parameter import Parameter as SCTP_Parameter + from pcapkit.const.sctp.payload_protocol_identifier import \ + PayloadProtocolIdentifier as SCTP_PayloadProtocolIdentifier __all__ = ['SCTP_Chunk', 'SCTP_Parameter', 'SCTP_CauseCode', 'SCTP_PayloadProtocolIdentifier'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'SCTP_Chunk': ('chunk', 'Chunk'), + 'SCTP_Parameter': ('parameter', 'Parameter'), + 'SCTP_CauseCode': ('cause_code', 'CauseCode'), + 'SCTP_PayloadProtocolIdentifier': ('payload_protocol_identifier', 'PayloadProtocolIdentifier'), +}) diff --git a/pcapkit/const/sslv2/__init__.py b/pcapkit/const/sslv2/__init__.py index 4efcd0ba41..e272210ead 100644 --- a/pcapkit/const/sslv2/__init__.py +++ b/pcapkit/const/sslv2/__init__.py @@ -21,7 +21,17 @@ """ -from pcapkit.const.sslv2.handshake_type import HandshakeType as SSLv2_HandshakeType -from pcapkit.const.sslv2.version import Version as SSLv2_Version +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.sslv2.handshake_type import HandshakeType as SSLv2_HandshakeType + from pcapkit.const.sslv2.version import Version as SSLv2_Version __all__ = ['SSLv2_HandshakeType', 'SSLv2_Version'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'SSLv2_HandshakeType': ('handshake_type', 'HandshakeType'), + 'SSLv2_Version': ('version', 'Version'), +}) diff --git a/pcapkit/const/tcp/__init__.py b/pcapkit/const/tcp/__init__.py index 8f53aa1733..23ee5d91bf 100644 --- a/pcapkit/const/tcp/__init__.py +++ b/pcapkit/const/tcp/__init__.py @@ -27,9 +27,21 @@ """ -from pcapkit.const.tcp.checksum import Checksum as TCP_Checksum -from pcapkit.const.tcp.flags import Flags as TCP_Flags -from pcapkit.const.tcp.mp_tcp_option import MPTCPOption as TCP_MPTCPOption -from pcapkit.const.tcp.option import Option as TCP_Option +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.tcp.checksum import Checksum as TCP_Checksum + from pcapkit.const.tcp.flags import Flags as TCP_Flags + from pcapkit.const.tcp.mp_tcp_option import MPTCPOption as TCP_MPTCPOption + from pcapkit.const.tcp.option import Option as TCP_Option __all__ = ['TCP_Checksum', 'TCP_Option', 'TCP_MPTCPOption', 'TCP_Flags'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'TCP_Checksum': ('checksum', 'Checksum'), + 'TCP_Option': ('option', 'Option'), + 'TCP_MPTCPOption': ('mp_tcp_option', 'MPTCPOption'), + 'TCP_Flags': ('flags', 'Flags'), +}) diff --git a/pcapkit/const/tls/__init__.py b/pcapkit/const/tls/__init__.py index 612e82260c..a6d30e809e 100644 --- a/pcapkit/const/tls/__init__.py +++ b/pcapkit/const/tls/__init__.py @@ -26,8 +26,19 @@ """ -from pcapkit.const.tls.content_type import ContentType as TLS_ContentType -from pcapkit.const.tls.extension_type import ExtensionType as TLS_ExtensionType -from pcapkit.const.tls.handshake_type import HandshakeType as TLS_HandshakeType +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.tls.content_type import ContentType as TLS_ContentType + from pcapkit.const.tls.extension_type import ExtensionType as TLS_ExtensionType + from pcapkit.const.tls.handshake_type import HandshakeType as TLS_HandshakeType __all__ = ['TLS_ContentType', 'TLS_HandshakeType', 'TLS_ExtensionType'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'TLS_ContentType': ('content_type', 'ContentType'), + 'TLS_HandshakeType': ('handshake_type', 'HandshakeType'), + 'TLS_ExtensionType': ('extension_type', 'ExtensionType'), +}) diff --git a/pcapkit/const/vlan/__init__.py b/pcapkit/const/vlan/__init__.py index e0753d56e4..74917a0ead 100644 --- a/pcapkit/const/vlan/__init__.py +++ b/pcapkit/const/vlan/__init__.py @@ -18,6 +18,15 @@ """ -from pcapkit.const.vlan.priority_level import PriorityLevel as VLAN_PriorityLevel +from typing import TYPE_CHECKING + +from pcapkit.utilities.lazy import lazy_exports + +if TYPE_CHECKING: + from pcapkit.const.vlan.priority_level import PriorityLevel as VLAN_PriorityLevel __all__ = ['VLAN_PriorityLevel'] + +__getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'VLAN_PriorityLevel': ('priority_level', 'PriorityLevel'), +}) diff --git a/pcapkit/utilities/__init__.py b/pcapkit/utilities/__init__.py index ef671ae5bf..b996e6af84 100644 --- a/pcapkit/utilities/__init__.py +++ b/pcapkit/utilities/__init__.py @@ -10,8 +10,8 @@ warnings, logging, the tunable limits of :mod:`~pcapkit.utilities.config`, a compatibility layer, timestamp conversion, pickle reductions, the context -variables its modules share, and wrappers around third-party -libraries. It imports nothing from :mod:`pcapkit` +variables its modules share, lazy imports for a package's +exports, and wrappers around third-party libraries. It imports nothing from :mod:`pcapkit` outside itself (:issue:`1712`). .. versionremoved:: 1.5.0 diff --git a/pcapkit/utilities/lazy.py b/pcapkit/utilities/lazy.py new file mode 100644 index 0000000000..d758b527d5 --- /dev/null +++ b/pcapkit/utilities/lazy.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +"""Lazy Imports +================== + +.. module:: pcapkit.utilities.lazy + +:mod:`pcapkit.utilities.lazy` holds :func:`lazy_exports`, which gives a package +the :pep:`562` ``__getattr__`` and ``__dir__`` that import each name it exports +the first time the name is reached, rather than with the package itself. Every +package under :mod:`pcapkit.const` imports its enumerations that way +(:issue:`2729`), the mechanism :mod:`pcapkit.const.reg.apptype` had for its TCP +and UDP registries (:issue:`1538`) made general. + +.. versionadded:: 1.6.0 + +""" +import importlib +import types +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable, Mapping, Optional + +__all__ = ['lazy_exports'] + + +def lazy_exports(package: 'str', namespace: 'dict[str, Any]', + exports: 'Optional[Mapping[str, tuple[str, str]]]' = None, *, + packages: 'Iterable[str]' = ()) -> 'tuple[Callable[[str], Any], Callable[[], list[str]]]': + """Return the ``__getattr__`` and ``__dir__`` of a package that imports its exports on first use. + + A package binds both at module level, after its ``__all__``, and imports the + exported names only under :data:`~typing.TYPE_CHECKING`, for the type checkers: + + .. code-block:: python + + if TYPE_CHECKING: + from pcapkit.const.tcp.option import Option as TCP_Option + + __all__ = ['TCP_Option'] + + __getattr__, __dir__ = lazy_exports(__name__, globals(), { + 'TCP_Option': ('option', 'Option'), + }) + + :pep:`562` calls ``__getattr__`` only for a name missing from the package's + globals. For an exported name it imports the submodule, reads the attribute + off it and binds it in ``namespace``, so it runs once per name; for the name + of a submodule an export comes from, it imports that submodule, which the + import system binds on the package. So ``from package import Name``, + ``import package.submodule``, attribute access, ``import *`` and the + :mod:`pickle` of an object defined in a submodule all behave as an eager + import would, apart from *when* the import happens. + + ``__dir__`` lists what the eager import would have bound: the names the + import system sets, ``__all__``, every export, every submodule an export + comes from, and any other submodule already imported. The package's own + helpers -- ``TYPE_CHECKING``, this function, the two hooks -- are left out. + + Args: + package: The package's ``__name__``. + namespace: The package's ``globals()``, which ``__getattr__`` binds + each name in once it is imported. + exports: The names imported on first use, each mapped to the submodule + it is defined in, relative to ``package``, and its name there. + packages: Subpackages, relative to ``package``, whose ``__all__`` this + package re-exports. Each is imported by this call -- one using + :func:`lazy_exports` itself imports none of its exports -- and every + name in its ``__all__`` is read off it on first use. + + Returns: + The package's ``__getattr__`` and ``__dir__``. + + """ + table = dict(exports or {}) + for name in packages: + module = importlib.import_module(f'{package}.{name}') + table.update((export, (name, export)) for export in module.__all__) + submodules = {module for module, _ in table.values()} + + def __getattr__(name: 'str') -> 'Any': + if name in table: + module, attribute = table[name] + value = getattr(importlib.import_module(f'{package}.{module}'), attribute) + namespace[name] = value + return value + if name in submodules: + return importlib.import_module(f'{package}.{name}') + raise AttributeError(f'module {package!r} has no attribute {name!r}') + + def __dir__() -> 'list[str]': + names = {name for name in namespace if name.startswith('__') and name.endswith('__')} + names -= {'__getattr__', '__dir__'} + names.update(namespace.get('__all__', ()), table, submodules) + names.update(name for (name, value) in list(namespace.items()) + if isinstance(value, types.ModuleType) and value.__name__ == f'{package}.{name}') + return sorted(names) + + return __getattr__, __dir__ diff --git a/pcapkit/vendor/reg/apptype/apptype.py b/pcapkit/vendor/reg/apptype/apptype.py index 587937453e..70a2c90b84 100644 --- a/pcapkit/vendor/reg/apptype/apptype.py +++ b/pcapkit/vendor/reg/apptype/apptype.py @@ -114,7 +114,7 @@ __all__ = ['TransportProtocol', '{NAME}'] if TYPE_CHECKING: - from typing import Any, Optional, Type + from typing import Any, Mapping, Optional, Type from pcapkit.corekit.multidict import MultiDict @@ -328,9 +328,9 @@ class {NAME}(EnumRegistry, StrEnum): #: Registry class owning each transport protocol, i.e. what #: :meth:`get` delegates to when called on this class rather than on one of - #: them. Populated by :mod:`pcapkit.const.reg.apptype` once every one of - #: them has been imported, since this module cannot import its own subclasses. - __registries__: 'dict[TransportProtocol, Type[{NAME}]]' = {{}} + #: them. Populated by :mod:`pcapkit.const.reg.apptype`, which imports each + #: of them on first use, since this module cannot import its own subclasses. + __registries__: 'Mapping[TransportProtocol, Type[{NAME}]]' = {{}} #: Canonical service per port, for the ports carrying more than one. The base #: registry has no members and so no collisions. @@ -516,13 +516,13 @@ def _dispatch(cls, key: 'int', proto: 'TransportProtocol | str | int') -> 'Type[ if isinstance(proto, str): proto = TransportProtocol.get(proto.lower()) - # NOTE: a direct dict lookup covers every genuine single transport -- + # NOTE: a direct lookup covers every genuine single transport -- # one of the real members, or a bare int equal to one of their # values -- since ``__registries__`` keys compare by the same # int-valued hash/eq every member and bare int alike already use. The - # cast is for mypy alone: ``dict.get`` accepts any hashable key at run + # cast is for mypy alone: ``.get`` accepts any hashable key at run # time regardless of its declared key type, but mypy holds ``.get`` to - # the dict's own ``TransportProtocol`` keys, and does not know ``proto`` + # the mapping's own ``TransportProtocol`` keys, and does not know ``proto`` # can genuinely be a bare :class:`int` here, TransportProtocol not being # an ``IntFlag`` (GitHub issue #808) -- see the annotation on ``proto`` # above. diff --git a/tests/conftest.py b/tests/conftest.py index 0589f7ba48..a3ba04b991 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -76,10 +76,16 @@ #: win. _pinned_snapshot = None # type: Optional[dict[str, types.ModuleType]] -#: What :func:`pytest_sessionstart` imports before the pin: the package, plus the -#: app-type registries it loads on first use rather than with itself (GitHub -#: issue #1538). See that hook for why the registries cannot be left out. -WARM_MODULES = ('pcapkit', 'pcapkit.const.reg.apptype.tcp', 'pcapkit.const.reg.apptype.udp') +#: What :func:`pytest_sessionstart` imports before the pin: the package, plus every +#: module of :mod:`pcapkit.const`, whose packages import their enumerations on first +#: use rather than with themselves -- the app-type registries for GitHub issue +#: #1538, and every enumeration for #2729. Read off the tree rather than listed, +#: so a module the crawlers add is warmed the day it lands. See that hook for why +#: they cannot be left out. +WARM_MODULES = ('pcapkit', *sorted( + '.'.join(path.relative_to(ROOT).with_suffix('').parts[:-1] if path.name == '__init__.py' + else path.relative_to(ROOT).with_suffix('').parts) + for path in (ROOT / 'pcapkit' / 'const').rglob('*.py'))) def _pin_module_snapshot() -> None: @@ -163,15 +169,17 @@ def pytest_sessionstart(session: 'pytest.Session') -> 'None': that imports the library and nothing else leaves the region exactly as it found it, so there is nothing to put back. - The lazily loaded app-type registries in :data:`WARM_MODULES` are + The lazily loaded :mod:`pcapkit.const` modules in :data:`WARM_MODULES` are imported here too, and that is for correctness rather than speed. Left out - of the pin, they were dropped from :data:`sys.modules` after whichever test - first loaded them, while the package kept the classes it had already bound - in ``TCP``/``UDP`` and :attr:`AppType.__registries__ + of the pin, the app-type registries were dropped from :data:`sys.modules` + after whichever test first loaded them, while the package kept the classes it + had already bound in ``TCP``/``UDP`` and :attr:`AppType.__registries__ `. The next direct import of the submodule then built a second ``TCP`` and ``UDP``, and :mod:`tests.project.test_conventions_doc_claims` counted both generations, and its registry count came out wrong on the 3.10-3.13 unit legs of PR #1560. + Every :mod:`pcapkit.const` package binds what it imports on first use the same + way (#2729), so every module of it is warmed. Failure here is not an error. A checkout without the runtime dependencies installed cannot import the package at all -- which is a supported way to run diff --git a/tests/const/test_const_apptype_lazy_1538_unit.py b/tests/const/test_const_apptype_lazy_1538_unit.py index 12b08e2607..7beb4e82a6 100644 --- a/tests/const/test_const_apptype_lazy_1538_unit.py +++ b/tests/const/test_const_apptype_lazy_1538_unit.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""The TCP and UDP app-type registries load on first use, not with ``pcapkit``. +"""The app-type registries, and every const enumeration, load on first use. GitHub issue #1538. :mod:`pcapkit.const.reg.apptype.tcp` and :mod:`~pcapkit.const.reg.apptype.udp` are generated from IANA's registry and @@ -8,25 +8,36 @@ any import path needs them, so :mod:`pcapkit.const.reg.apptype` imports them from a :pep:`562` ``__getattr__`` and from :attr:`AppType.__registries__ ` -the first time either is read. +the first time either is read. GitHub issue #2729 extended that to SCTP and +DCCP, and to every package under :mod:`pcapkit.const`, through +:func:`pcapkit.utilities.lazy.lazy_exports`; #2726 made the registries an +:class:`~pcapkit.corekit.infoclass.Info` rather than a :obj:`dict` subclass. The subprocess checks run in a fresh interpreter, since the question is what a -cold ``import pcapkit`` leaves in :data:`sys.modules` and this process has long -since imported both. Each fails on an eager tree at its first ``loaded()`` -check; what follows it pins that nothing a caller can observe changed apart -from *when* the import happens. :class:`RegistriesMappingTests` and -:class:`PackageGetattrTests` run in-process, on a private import, so that -coverage sees every line. :class:`PinnedSnapshotTests` covers the suite's own -side: :file:`tests/conftest.py` has to pin both registries, or its restore -leaves a second generation of each behind. +cold import leaves in :data:`sys.modules` and this process has long since +imported everything. :data:`PRELUDE` imports :mod:`pcapkit`, whose protocol +modules import the const modules they name at module level -- +:mod:`pcapkit.protocols.application.ngap` imports SCTP's registry, for one, which +#2730 is to move -- so a check of the const packages themselves runs on :data:`BARE`, which +binds :mod:`pcapkit` as a bare package and so runs none of +:file:`pcapkit/__init__.py`. Each check fails on an eager tree at its first +``loaded()`` or ``enumerations()``; what follows it pins that nothing a caller +can observe changed apart from *when* the import happens. +:class:`RegistriesMappingTests` and :class:`PackageGetattrTests` run in-process, +on a private import, so that coverage sees every line. +:class:`PinnedSnapshotTests` covers the suite's own side: :file:`tests/conftest.py` +has to pin every const module, or its restore leaves a second generation of each +behind. """ +import copy import importlib.util import inspect import json import os import pathlib +import pickle import subprocess import sys import textwrap @@ -39,11 +50,14 @@ from types import ModuleType from typing import Any -#: Test tier (#1958): branches on the running interpreter's version or implementation. +#: Test tier (#1958): exercises the import system and pickling, which differ between interpreters. __tier__ = 'ALL' ROOT = pathlib.Path(__file__).resolve().parents[2] +#: The per-transport registries, in :class:`~pcapkit.const.reg.apptype.apptype.TransportProtocol` order. +TRANSPORTS = ('tcp', 'udp', 'sctp', 'dccp') + #: The names ``dir(pcapkit.const.reg.apptype)`` listed on the eager package #: besides those the import system sets. Those are not pinned, since they vary #: by interpreter -- Python 3.15 no longer sets ``__cached__`` (:gh:`97879`, @@ -51,25 +65,11 @@ PACKAGE_NAMES = ['AppType', 'DCCP', 'SCTP', 'TCP', 'TransportProtocol', 'UDP', '__all__', 'apptype', 'dccp', 'sctp', 'tcp', 'udp'] -#: Whether :obj:`dict` has union, :pep:`584`. Before Python 3.9, a registry has -#: none either (GitHub issue #1694), so the union views run from 3.9 only. -UNION = sys.version_info >= (3, 9) - - -def merge(left: 'Any', right: 'Any') -> 'Any': - """``left |= right``, as an expression.""" - left |= right - return left - - -#: Each dict union operator, as a function of its two operands. -UNIONS = {'|': lambda left, right: left | right, '|=': merge} - - -def union_error(op: 'str', left: 'object', right: 'object') -> 'str': - """The :exc:`TypeError` message for ``left right`` where neither supports it.""" - return (f'unsupported operand type(s) for {op}: ' - f'{type(left).__name__!r} and {type(right).__name__!r}') +#: The const modules a package of :mod:`pcapkit.const` imports with itself, and why: +#: the app-type package binds ``__registries__`` on the memberless base, so it +#: needs :class:`~pcapkit.const.reg.apptype.apptype.AppType` and +#: :class:`~pcapkit.const.reg.apptype.apptype.TransportProtocol` at once. +EAGER = {'pcapkit.const.reg.apptype.apptype': 'pcapkit.const.reg.apptype'} def import_system_names(module: 'ModuleType') -> 'list[str]': @@ -80,7 +80,7 @@ def import_system_names(module: 'ModuleType') -> 'list[str]': the body with :func:`exec` in the module's namespace and so adds ``__builtins__``. Both run here on ``module``'s own spec, so the names are the ones this interpreter, and the loader that imported ``module``, set. - :data:`PRELUDE` carries this function's source into each child. + :data:`HELPERS` carries this function's source into each child. """ fresh = importlib.util.module_from_spec(module.__spec__) # type: ignore[arg-type] @@ -95,33 +95,54 @@ def eager_dir(interpreter: 'list[str]') -> 'list[str]': return sorted({*PACKAGE_NAMES, *interpreter}) -PRELUDE = f''' -import importlib.util, json, sys -import pcapkit -assert pcapkit.__file__.startswith({str(ROOT)!r}), pcapkit.__file__ +HELPERS = f''' +import importlib.util, json, pathlib, sys -def loaded(): - return [name for name in ('tcp', 'udp') +def loaded(*names): + return [name for name in (names or {TRANSPORTS!r}) if f'pcapkit.const.reg.apptype.{{name}}' in sys.modules] +def enumerations(): + return sorted(name for name, module in list(sys.modules.items()) + if name.startswith('pcapkit.const.') and not hasattr(module, '__path__')) + def emit(**kwargs): print(json.dumps(kwargs)) ''' + inspect.getsource(import_system_names) +#: A child that imports :mod:`pcapkit`, protocols and all. +PRELUDE = HELPERS + f''' +import pcapkit +assert pcapkit.__file__.startswith({str(ROOT)!r}), pcapkit.__file__ +''' + +#: A child that binds :mod:`pcapkit` as a bare package, so importing a +#: subpackage runs none of :file:`pcapkit/__init__.py` -- nor the protocol +#: modules it imports, which name const modules of their own. +BARE = HELPERS + f''' +_root = pathlib.Path({str(ROOT)!r}) / 'pcapkit' +_spec = importlib.util.spec_from_file_location('pcapkit', _root / '__init__.py', + submodule_search_locations=[str(_root)]) +sys.modules['pcapkit'] = importlib.util.module_from_spec(_spec) +import pcapkit.const +assert pcapkit.const.__file__.startswith({str(ROOT)!r}), pcapkit.const.__file__ +assert 'pcapkit.protocols' not in sys.modules +''' + #: ``repr(AppType.__registries__)`` once populated, eager or lazy. REGISTRIES_REPR = ("{: , : " ", : , " ": }") -def run_child(code: 'str') -> 'dict': - """Run :data:`PRELUDE` then ``code`` in a fresh interpreter; return its last +def run_child(code: 'str', prelude: 'str' = PRELUDE) -> 'dict': + """Run ``prelude`` then ``code`` in a fresh interpreter; return its last :func:`emit` line.""" environ = dict(os.environ) environ['PYTHONPATH'] = os.pathsep.join( [str(ROOT), environ['PYTHONPATH']] if environ.get('PYTHONPATH') else [str(ROOT)]) - result = subprocess.run([sys.executable, '-c', PRELUDE + textwrap.dedent(code)], + result = subprocess.run([sys.executable, '-c', prelude + textwrap.dedent(code)], cwd=str(ROOT), env=environ, capture_output=True, text=True, timeout=300, check=False) if result.returncode != 0: @@ -132,23 +153,48 @@ def run_child(code: 'str') -> 'dict': class LazyAppTypeImportTests(unittest.TestCase): """What a cold import loads, and what loads each registry afterwards.""" - def test_import_pcapkit_leaves_tcp_and_udp_unimported(self) -> 'None': + def test_import_pcapkit_leaves_the_registries_unimported(self) -> 'None': + # NOTE: SCTP's is left out: pcapkit.protocols.application.ngap imports it + # at module level, which LazyConstImportTests accounts for (#2730). out = run_child(''' - emit(loaded=loaded(), - eager=[f'pcapkit.const.reg.apptype.{name}' in sys.modules - for name in ('apptype', 'sctp', 'dccp')]) + emit(loaded=loaded('tcp', 'udp', 'dccp'), + eager='pcapkit.const.reg.apptype.apptype' in sys.modules) ''') self.assertEqual(out['loaded'], []) - self.assertEqual(out['eager'], [True, True, True]) + self.assertTrue(out['eager']) - def test_attribute_access_loads_only_the_registry_it_names(self) -> 'None': + def test_the_package_imports_no_registry(self) -> 'None': out = run_child(''' import pcapkit.const.reg.apptype as pkg + from pcapkit.const.reg.apptype import AppType steps = [loaded()] - tcp = pkg.TCP + sctp = pkg.SCTP steps.append(loaded()) - udp = pkg.udp.UDP + dccp = type(AppType.get(9, proto='dccp')) steps.append(loaded()) + tcp = pkg.tcp.TCP + steps.append(loaded()) + from pcapkit.const.reg.apptype import UDP + steps.append(loaded()) + emit(steps=steps, + same=[sctp is sys.modules['pcapkit.const.reg.apptype.sctp'].SCTP, dccp is pkg.DCCP, + tcp is pkg.TCP, UDP is pkg.udp.UDP, 'SCTP' in vars(pkg)], + members=[isinstance(sctp.discard, AppType), str(AppType.get(9, proto='sctp')), + str(AppType.get(9, proto=pkg.TransportProtocol.dccp))]) + ''', prelude=BARE) + self.assertEqual(out['steps'], [[], ['sctp'], ['sctp', 'dccp'], ['tcp', 'sctp', 'dccp'], + ['tcp', 'udp', 'sctp', 'dccp']]) + self.assertEqual(out['same'], [True] * 5) + self.assertEqual(out['members'], [True, 'discard [9 - sctp]', 'discard [9 - dccp]']) + + def test_attribute_access_loads_only_the_registry_it_names(self) -> 'None': + out = run_child(''' + import pcapkit.const.reg.apptype as pkg + steps = [loaded('tcp', 'udp')] + tcp = pkg.TCP + steps.append(loaded('tcp', 'udp')) + udp = pkg.udp.UDP + steps.append(loaded('tcp', 'udp')) emit(steps=steps, same=[tcp is sys.modules['pcapkit.const.reg.apptype.tcp'].TCP, udp is pkg.UDP, pkg.TCP is pkg.tcp.TCP, 'TCP' in vars(pkg)]) @@ -159,11 +205,11 @@ def test_attribute_access_loads_only_the_registry_it_names(self) -> 'None': def test_port_lookup_loads_only_the_transport_it_asks_for(self) -> 'None': out = run_child(''' from pcapkit.const.reg.apptype import AppType, TransportProtocol - steps = [loaded()] + steps = [loaded('tcp', 'udp')] http = AppType.get(80, proto='tcp') - steps.append(loaded()) + steps.append(loaded('tcp', 'udp')) dns = AppType.get(53, proto=TransportProtocol.udp) - steps.append(loaded()) + steps.append(loaded('tcp', 'udp')) emit(steps=steps, members=[str(http), type(http).__qualname__, str(dns), type(dns).__qualname__]) ''') @@ -185,50 +231,53 @@ def test_dir_and_star_import_are_unchanged(self) -> 'None': interpreter=import_system_names(pkg), star=sorted(name for name in namespace if name != '__builtins__'), resolved=[namespace[name] is getattr(pkg, name) for name in pkg.__all__]) - ''') + ''', prelude=BARE) self.assertEqual(out['dir'], eager_dir(out['interpreter'])) self.assertEqual(out['before'], []) self.assertEqual(out['star'], sorted(out['all'])) self.assertEqual(out['resolved'], [True] * 6) - self.assertEqual(out['after'], ['tcp', 'udp']) + self.assertEqual(out['after'], list(TRANSPORTS)) def test_registries_never_expose_a_pending_registry(self) -> 'None': # NOTE: one interpreter for every view, so each starts from the pending # state again rather than from whatever the previous view loaded. out = run_child(''' - import copy + import copy, pickle from pcapkit.const.reg.apptype import AppType, TransportProtocol + from pcapkit.corekit.infoclass import Info from pcapkit.corekit.module import ModuleDescriptor registries = AppType.__registries__ shape = [len(registries), [int(key) for key in registries], - 1 in registries, TransportProtocol.udp in registries, loaded()] + 1 in registries, TransportProtocol.udp in registries, loaded(), + isinstance(registries, Info), isinstance(registries, dict)] populated = dict(registries) views = { - 'getitem': lambda: registries[TransportProtocol.tcp], - 'get': lambda: registries.get(1), - 'dict': lambda: dict(registries), - 'unpack': lambda: {**registries}, - 'values': lambda: list(registries.values()), - 'items': lambda: dict(registries.items()), - 'copy': registries.copy, - 'copy.copy': lambda: dict(copy.copy(registries)), - 'repr': lambda: registries, - 'eq': lambda: registries == populated, - 'ne': lambda: registries != populated, + 'getitem': lambda: repr(registries[TransportProtocol.tcp]), + 'get': lambda: repr(registries.get(1)), + 'dict': lambda: repr(dict(registries)), + 'unpack': lambda: repr({**registries}), + 'values': lambda: repr(list(registries.values())), + 'items': lambda: repr(dict(registries.items())), + 'items_multi': lambda: repr(dict(registries.items(multi=True))), + 'to_dict': lambda: repr(dict(registries.to_dict())), + 'copy.copy': lambda: repr(dict(copy.copy(registries))), + 'copy.deepcopy': lambda: repr(dict(copy.deepcopy(registries))), + 'pickle': lambda: repr(dict(pickle.loads(pickle.dumps(registries)))), + 'repr': lambda: repr(registries), + 'str': lambda: str(registries), + 'eq': lambda: repr(registries == populated), + 'ne': lambda: repr(registries != populated), } - if sys.version_info >= (3, 9): # dict union, PEP 584 - views.update({'or': lambda: registries | {}, 'ror': lambda: {} | registries}) reprs = {} for name, view in views.items(): - for proto in (TransportProtocol.tcp, TransportProtocol.udp): - dict.__setitem__(registries, proto, ModuleDescriptor( - f'pcapkit.const.reg.apptype.{proto.name}', proto.name.upper())) - reprs[name] = repr(view()) + for proto in list(TransportProtocol)[1:]: + vars(registries)[registries.__map__[proto]] = ModuleDescriptor( + f'pcapkit.const.reg.apptype.{proto.name}', proto.name.upper()) + reprs[name] = view() emit(shape=shape, reprs=reprs, default=registries.get(0, 'fallback')) - ''') - self.assertEqual(out['shape'], [4, [1, 2, 3, 4], True, True, []]) - self.assertEqual(len(out['reprs']), 13 if UNION else 11) - self.assertEqual({'or', 'ror'} <= set(out['reprs']), UNION) + ''', prelude=BARE) + self.assertEqual(out['shape'], [4, [1, 2, 3, 4], True, True, [], True, False]) + self.assertEqual(len(out['reprs']), 15) for name, text in out['reprs'].items(): with self.subTest(view=name): self.assertNotIn('ModuleDescriptor', text) @@ -242,21 +291,27 @@ def test_registries_never_expose_a_pending_registry(self) -> 'None': def test_member_pickled_eagerly_unpickles_after_a_lazy_import(self) -> 'None': payload = run_child(''' import pickle + from pcapkit.const.reg.apptype.dccp import DCCP + from pcapkit.const.reg.apptype.sctp import SCTP from pcapkit.const.reg.apptype.tcp import TCP - emit(payload=pickle.dumps(TCP.http).hex()) + from pcapkit.const.reg.apptype.udp import UDP + emit(payload=pickle.dumps([TCP.http, UDP.domain, SCTP.discard, DCCP.discard]).hex()) ''')['payload'] out = run_child(f''' import copy, pickle before = loaded() - member = pickle.loads(bytes.fromhex({payload!r})) - from pcapkit.const.reg.apptype import TCP + members = pickle.loads(bytes.fromhex({payload!r})) + import pcapkit.const.reg.apptype as pkg emit(before=before, after=loaded(), - same=[member is TCP.http, copy.copy(member) is member, - copy.deepcopy(member) is member]) - ''') + same=[member is getattr(pkg, name).__members__[member.name] + for name, member in zip(('TCP', 'UDP', 'SCTP', 'DCCP'), members)], + copies=[copy.copy(member) is member and copy.deepcopy(member) is member + for member in members]) + ''', prelude=BARE) self.assertEqual(out['before'], []) - self.assertEqual(out['after'], ['tcp']) - self.assertEqual(out['same'], [True, True, True]) + self.assertEqual(out['after'], list(TRANSPORTS)) + self.assertEqual(out['same'], [True] * 4) + self.assertEqual(out['copies'], [True] * 4) def test_port_registered_before_first_use_dispatches_the_same(self) -> 'None': code = ''' @@ -265,7 +320,7 @@ def test_port_registered_before_first_use_dispatches_the_same(self) -> 'None': if {lookup_first}: import pcapkit.const.reg.apptype pcapkit.const.reg.apptype.TCP - at_register = loaded() + at_register = loaded('tcp', 'udp') register_tcp(54321, 'pcapkit.protocols.application.ftp', 'FTP') raw = TCP(srcport=40000, dstport=54321, payload=b'USER anonymous\\r\\n').data parsed = TCP(raw, len(raw)) @@ -283,12 +338,110 @@ def test_port_registered_before_first_use_dispatches_the_same(self) -> 'None': self.assertEqual(before['rebuilt'], [True, True]) +class LazyConstImportTests(unittest.TestCase): + """:mod:`pcapkit.const` imports no enumeration until one is used (GitHub issue #2729).""" + + def test_import_pcapkit_const_loads_no_enumeration(self) -> 'None': + out = run_child(''' + packages = sorted(name for name in sys.modules if name.startswith('pcapkit.const')) + steps = [enumerations()] + option = pcapkit.const.TCP_Option + steps.append(enumerations()) + from pcapkit.const.ipv4 import IPv4_OptionNumber + steps.append(enumerations()) + import pcapkit.const.vlan.priority_level + steps.append(enumerations()) + apptype = pcapkit.const.APPTYPE + steps.append(enumerations()) + namespace = {} + exec('from pcapkit.const import *', namespace) + emit(packages=packages, steps=steps, loaded=loaded(), + same=[option is pcapkit.const.tcp.option.Option, + IPv4_OptionNumber is pcapkit.const.ipv4.option_number.OptionNumber, + pcapkit.const.vlan.priority_level.PriorityLevel is pcapkit.const.VLAN_PriorityLevel, + apptype is pcapkit.const.reg.apptype.apptype.AppType], + star=all(namespace[name] is getattr(pcapkit.const, name) for name in pcapkit.const.__all__), + every=enumerations()) + ''', prelude=BARE) + tree = ROOT / 'pcapkit' / 'const' + packages = sorted('.'.join(path.relative_to(ROOT).parent.parts) for path in tree.rglob('__init__.py')) + modules = sorted('.'.join(path.relative_to(ROOT).with_suffix('').parts) + for path in tree.rglob('*.py') if path.name != '__init__.py') + self.assertEqual(out['packages'], [name for name in packages if name != 'pcapkit.const.reg.apptype']) + self.assertEqual(out['steps'], [ + [], + ['pcapkit.const.tcp.option'], + ['pcapkit.const.ipv4.option_number', 'pcapkit.const.tcp.option'], + ['pcapkit.const.ipv4.option_number', 'pcapkit.const.tcp.option', + 'pcapkit.const.vlan.priority_level'], + ['pcapkit.const.ipv4.option_number', 'pcapkit.const.reg.apptype.apptype', + 'pcapkit.const.tcp.option', 'pcapkit.const.vlan.priority_level'], + ]) + # NOTE: pcapkit.const exports APPTYPE, the memberless base, and none of + # the per-transport registries, so its star import loads every other + # enumeration and none of those. + self.assertEqual(out['loaded'], []) + self.assertEqual(out['same'], [True] * 4) + self.assertTrue(out['star']) + self.assertEqual(out['every'], [name for name in modules if name not in + {f'pcapkit.const.reg.apptype.{name}' for name in TRANSPORTS}]) + + def test_import_pcapkit_loads_only_what_other_modules_import(self) -> 'None': + """Every const module ``import pcapkit`` loads is one a module outside the + const packages imports at module level, or one :data:`EAGER` names. + + Those are the protocol, schema and data modules naming the const module + they read; moving them is GitHub issue #2730. + + """ + out = run_child(''' + import ast + + def imported(body, found): + for node in body: + if isinstance(node, ast.Import): + found.update(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: + found.update(f'{node.module}.{alias.name}' for alias in node.names) + found.add(node.module) + elif isinstance(node, ast.If): + if not (isinstance(node.test, ast.Name) and node.test.id == 'TYPE_CHECKING'): + imported(node.body, found) + imported(node.orelse, found) + elif isinstance(node, ast.Try): + for block in (node.body, *(handler.body for handler in node.handlers), + node.orelse, node.finalbody): + imported(block, found) + return found + + loaded = set(enumerations()) + importers = {} + for name, module in list(sys.modules.items()): + path = getattr(module, '__file__', None) + if not name.startswith('pcapkit') or path is None or ( + name.startswith('pcapkit.const') and hasattr(module, '__path__')): + continue + with open(path, encoding='utf-8') as file: + found = imported(ast.parse(file.read()).body, set()) + for target in found & loaded: + importers.setdefault(target, []).append(name) + emit(loaded=sorted(loaded), explained=sorted(importers), + packages=sorted(name for name in sys.modules + if name.startswith('pcapkit.const') and hasattr(sys.modules[name], '__path__'))) + ''') + unexplained = sorted(set(out['loaded']) - set(out['explained']) - set(EAGER)) + self.assertEqual(unexplained, [], 'a const package imported these with itself') + for module, package in EAGER.items(): + self.assertIn(package, out['packages'], f'{module} names a package import pcapkit does not load') + self.assertNotIn('pcapkit.const.reg.apptype.tcp', out['loaded']) + + class RegistriesMappingTests(unittest.TestCase): """Every method of the lazy mapping, on a private instance in this process.""" def setUp(self) -> 'None': # NOTE: a private import, so that the package starts cold -- the shared - # one has both registries loaded, since tests/conftest.py pins them -- + # one has every registry loaded, since tests/conftest.py pins them -- # and nothing bound here outlives the class. reimport_once_per_class(self, restore=True) self.make() @@ -296,11 +449,14 @@ def setUp(self) -> 'None': def make(self) -> 'None': from pcapkit.const.reg.apptype import _Registries # type: ignore[attr-defined] from pcapkit.const.reg.apptype import DCCP, SCTP, TCP, UDP, TransportProtocol + from pcapkit.corekit.infoclass import Info from pcapkit.corekit.module import ModuleDescriptor + from pcapkit.utilities.exceptions import UnsupportedCall self.proto = TransportProtocol self.classes = [TCP, UDP, SCTP, DCCP] self.descriptor = ModuleDescriptor + self.info, self.unsupported, self.factory = Info, UnsupportedCall, _Registries self.registries = _Registries({ TransportProtocol.tcp: ModuleDescriptor('pcapkit.const.reg.apptype.tcp', 'TCP'), TransportProtocol.udp: ModuleDescriptor('pcapkit.const.reg.apptype.udp', 'UDP'), @@ -308,10 +464,11 @@ def make(self) -> 'None': TransportProtocol.dccp: DCCP, }) - def assert_pending(self, *protos: 'object') -> 'None': - raw = dict.items(self.registries) - self.assertEqual([key for key, value in raw if isinstance(value, self.descriptor)], - list(protos)) + def assert_pending(self, *protos: 'object', registries: 'Any' = None) -> 'None': + registries = self.registries if registries is None else registries + held = vars(registries) + self.assertEqual([proto for name, proto in registries.__map_reverse__.items() + if isinstance(held[name], self.descriptor)], list(protos)) def test_reads_resolve_in_place(self) -> 'None': registries, proto = self.registries, self.proto @@ -329,62 +486,92 @@ def test_a_descriptor_default_is_returned_untouched(self) -> 'None': self.assertIs(self.registries.get(0, default), default) self.assertNotIn(0, self.registries) + def test_only_a_transport_is_a_key(self) -> 'None': + registries = self.registries + for key in ('tcp', '__map__', '__dict__', registries.__map__[self.proto.tcp], self.proto.undefined, 5): + with self.subTest(key=key): + self.assertNotIn(key, registries) + with self.assertRaises(KeyError) as context: + registries[key] # pylint: disable=pointless-statement + self.assertEqual(context.exception.args, (key,)) + with self.assertRaises(TypeError): + [] in registries # pylint: disable=pointless-statement + self.assert_pending(self.proto.tcp, self.proto.udp) + + def test_keys_by_member_value_or_name(self) -> 'None': + proto, (tcp, udp, sctp, _) = self.proto, self.classes + for registries in (self.factory({1: tcp, 'UDP': udp}, sctp=sctp), + self.factory([(proto.tcp, tcp), (2, udp)], sctp=sctp), + self.factory.from_dict({proto.tcp: tcp, 'udp': udp}, SCTP=sctp)): + with self.subTest(registries=registries): + self.assertEqual(list(registries), [proto.tcp, proto.udp, proto.sctp]) + self.assertEqual([registries[key] for key in registries], [tcp, udp, sctp]) + def test_whole_mapping_reads_resolve_everything(self) -> 'None': proto, classes = self.proto, self.classes expected = dict(zip(list(proto)[1:], classes)) views = { 'values': lambda registries: list(registries.values()) == classes, 'items': lambda registries: dict(registries.items()) == expected, - 'copy': lambda registries: registries.copy() == expected, - 'dict': lambda registries: dict.__eq__(dict(registries), expected), + 'items_multi': lambda registries: list(registries.items(multi=True)) == list(expected.items()), + 'to_dict': lambda registries: list(registries.to_dict().items(multi=True)) == list(expected.items()), + 'dict': lambda registries: dict(registries) == expected, 'eq': lambda registries: registries == expected, 'ne': lambda registries: (registries != expected) is False, - 'repr': lambda registries: 'ModuleDescriptor' not in repr(registries), - 'pop': lambda registries: registries.pop(proto.tcp) is classes[0], - 'popitem': lambda registries: registries.popitem() == (proto.dccp, classes[3]), - 'setdefault': lambda registries: registries.setdefault(proto.udp) is classes[1], + 'repr': lambda registries: repr(registries) == repr(expected), + 'str': lambda registries: str(registries) == repr(expected), + 'deepcopy': lambda registries: dict(copy.deepcopy(registries)) == expected, } - if UNION: - views.update({ - 'or': lambda registries: dict.__eq__(registries | {}, expected), - 'ror': lambda registries: dict.__eq__({} | registries, expected), - }) + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + views[f'pickle-{protocol}'] = lambda registries, protocol=protocol: self.round_trips(registries, protocol) for name, view in views.items(): with self.subTest(view=name): self.make() self.assertTrue(view(self.registries)) self.assert_pending() - def test_union_is_dict_s_on_every_python(self) -> 'None': - # NOTE: GitHub issue #1694. Where dict has no union (Python < 3.9), a - # registry raises dict's TypeError, naming its own type, and imports - # nothing; where it does, ``registries |= other`` is dict's in-place - # update, which reads no value and so leaves the registry pending. - proto, expected = self.proto, dict(zip(list(self.proto)[1:], self.classes)) - for op, side in (('|', 'left'), ('|', 'right'), ('|=', 'left'), ('|=', 'right')): - with self.subTest(op=op, side=side): - self.make() - operands = (self.registries, {}) if side == 'left' else ({}, self.registries) - if not UNION: - with self.assertRaises(TypeError) as plain: - UNIONS[op]({}, {}) - with self.assertRaises(TypeError) as registry: - UNIONS[op](*operands) - self.assertEqual(str(plain.exception), union_error(op, {}, {})) - self.assertEqual(str(registry.exception), union_error(op, *operands)) - self.assert_pending(proto.tcp, proto.udp) - elif (op, side) == ('|=', 'left'): - self.assertIs(UNIONS[op](*operands), self.registries) - self.assert_pending(proto.tcp, proto.udp) - self.assertEqual(self.registries, expected) - else: - result = UNIONS[op](*operands) - self.assertIs(type(result), dict) - self.assertTrue(dict.__eq__(result, expected)) - self.assert_pending() - for name in ('__or__', '__ror__'): - self.assertIs(name in vars(type(self.registries)), UNION) - self.assertNotIn('__ior__', vars(type(self.registries))) + def round_trips(self, registries: 'Any', protocol: 'int') -> 'bool': + """Whether ``registries`` comes back from :mod:`pickle` as an equal registry holding no descriptor.""" + copied = pickle.loads(pickle.dumps(registries, protocol)) + self.assertIs(type(copied), self.factory) + self.assertEqual(list(copied), list(registries)) + self.assert_pending(registries=copied) + return copied == registries + + def test_a_copy_resolves_on_its_own(self) -> 'None': + proto, registries = self.proto, self.registries + copied = copy.copy(registries) + self.assertIsNot(copied.__map__, registries.__map__) + self.assert_pending(proto.tcp, proto.udp, registries=copied) + self.assertIs(copied[proto.tcp], self.classes[0]) + self.assert_pending(proto.udp, registries=copied) + self.assert_pending(proto.tcp, proto.udp) + self.assertEqual(copied, registries) + + def test_it_is_an_immutable_info(self) -> 'None': + # NOTE: GitHub issue #2726. As a dict subclass it had every dict method, + # and the | operator from Python 3.9 (#1694); as an Info it has the + # read-only mapping interface, and nothing it does imports a registry. + registries, proto = self.registries, self.proto + self.assertIsInstance(registries, self.info) + self.assertNotIsInstance(registries, dict) + with self.assertRaises(TypeError): + registries | {} # pylint: disable=pointless-statement + with self.assertRaises(TypeError): + {} | registries # pylint: disable=pointless-statement + with self.assertRaises(TypeError): + registries[proto.tcp] = self.classes[0] # type: ignore[index] + with self.assertRaises(TypeError): + del registries[proto.tcp] # type: ignore[attr-defined] + local = registries + with self.assertRaises(TypeError): + local |= {} # type: ignore[operator] + with self.assertRaises(self.unsupported): + registries.tcp = self.classes[0] + for name in ('pop', 'popitem', 'setdefault', 'update', 'clear', 'copy', 'fromkeys'): + with self.subTest(method=name): + self.assertFalse(hasattr(registries, name)) + self.assert_pending(proto.tcp, proto.udp) class PackageGetattrTests(unittest.TestCase): @@ -396,26 +583,31 @@ def setUp(self) -> 'None': def test_hooks(self) -> 'None': import pcapkit.const.reg.apptype as pkg - self.assertNotIn('TCP', vars(pkg)) - self.assertNotIn('pcapkit.const.reg.apptype.tcp', sys.modules) + # NOTE: TCP, UDP and DCCP, since pcapkit.protocols.application.ngap + # imports SCTP's submodule at module level (#2730). + for name in ('TCP', 'UDP', 'DCCP'): + self.assertNotIn(name, vars(pkg)) + self.assertNotIn(f'pcapkit.const.reg.apptype.{name.lower()}', sys.modules) getattr_ = vars(pkg)['__getattr__'] tcp_module = getattr_('tcp') self.assertIs(tcp_module, sys.modules['pcapkit.const.reg.apptype.tcp']) self.assertIs(getattr_('TCP'), tcp_module.TCP) self.assertIs(vars(pkg)['TCP'], tcp_module.TCP) + self.assertIs(getattr_('DCCP'), sys.modules['pcapkit.const.reg.apptype.dccp'].DCCP) for name in ('Tcp', 'tCP', 'sctp_', 'nope'): - with self.subTest(name=name), self.assertRaises(AttributeError): + with self.subTest(name=name), self.assertRaises(AttributeError) as context: getattr_(name) + self.assertEqual(str(context.exception), f"module 'pcapkit.const.reg.apptype' has no attribute {name!r}") self.assertEqual(vars(pkg)['__dir__'](), eager_dir(import_system_names(pkg))) class SharedImportProbe(unittest.TestCase): """Two tests on the shared import, with the conftest restore between them. - The first binds both registries through the package; the second imports + The first binds every registry through the package; the second imports their submodules directly. Unless :data:`tests.conftest.WARM_MODULES` puts them in the pinned snapshot, the restore drops them in between and the - second test builds another ``TCP`` and ``UDP``. :mod:`unittest` runs the + second test builds another generation of each. :mod:`unittest` runs the two in name order, and :class:`PinnedSnapshotTests` runs them in one :program:`pytest` process, so they meet that restore whatever runs this file. @@ -425,19 +617,19 @@ def test_1_bind_through_the_package(self) -> 'None': import pcapkit.const.reg.apptype as pkg from pcapkit.const.reg.apptype import AppType - self.assertIs(type(AppType.get(80, proto='tcp')), pkg.TCP) - self.assertIs(type(AppType.get(53, proto='udp')), pkg.UDP) + for name, port in (('TCP', 80), ('UDP', 53), ('SCTP', 9), ('DCCP', 9)): + with self.subTest(registry=name): + self.assertIs(type(AppType.get(port, proto=name.lower())), getattr(pkg, name)) def test_2_import_the_submodules_directly(self) -> 'None': import pcapkit.const.reg.apptype as pkg from pcapkit.const.reg.apptype import AppType, TransportProtocol - from pcapkit.const.reg.apptype.tcp import TCP - from pcapkit.const.reg.apptype.udp import UDP - self.assertIs(TCP, pkg.TCP) - self.assertIs(UDP, pkg.UDP) - self.assertIs(AppType.__registries__[TransportProtocol.tcp], TCP) - self.assertIs(AppType.__registries__[TransportProtocol.udp], UDP) + for name in TRANSPORTS: + with self.subTest(registry=name): + registry = getattr(importlib.import_module(f'pcapkit.const.reg.apptype.{name}'), name.upper()) + self.assertIs(registry, getattr(pkg, name.upper())) + self.assertIs(AppType.__registries__[TransportProtocol[name]], registry) class PinnedSnapshotTests(unittest.TestCase): diff --git a/tests/const/test_const_lazy_exports_2729_unit.py b/tests/const/test_const_lazy_exports_2729_unit.py new file mode 100644 index 0000000000..beacb593d5 --- /dev/null +++ b/tests/const/test_const_lazy_exports_2729_unit.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +"""Every :mod:`pcapkit.const` package exports what it did eagerly (GitHub issue #2729). + +Each package under :mod:`pcapkit.const` imports its enumerations on first use, +through :func:`pcapkit.utilities.lazy.lazy_exports`, and names them only under +:data:`~typing.TYPE_CHECKING`, for the type checkers. These sweeps read the +packages off the tree, so a package the crawlers add is held to them the day it +lands, and hold every one to the eager behaviour that statement describes: + +* every name in its ``__all__`` is named by an import under ``TYPE_CHECKING``, + bar the ones :data:`EAGER` lists, and resolves to the very object that import + names, or to the subpackage's for a star import; +* ``from import *`` binds every one of them; +* :func:`dir` lists what the eager import bound -- the names the import system + sets, ``__all__``, and the submodules -- and none of the helpers; +* every member of every exported enumeration survives :mod:`pickle` as itself. + +What a cold import loads, and when, is +:mod:`tests.const.test_const_apptype_lazy_1538_unit`'s. + +""" + +import ast +import importlib +import importlib.util +import pathlib +import pickle +import types +import unittest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + +#: Test tier (#1958): exercises the import system and pickling, which differ between interpreters. +__tier__ = 'ALL' + +ROOT = pathlib.Path(__file__).resolve().parents[2] + +#: The names a :mod:`pcapkit.const` package imports with itself, keyed by package, +#: and why: the app-type package binds ``__registries__`` on the memberless base, so +#: it needs both at once. +EAGER = {'pcapkit.const.reg.apptype': {'AppType', 'TransportProtocol'}} + +#: Every package of :mod:`pcapkit.const`, read off the tree. +PACKAGES = sorted('.'.join(path.relative_to(ROOT).parent.parts) + for path in (ROOT / 'pcapkit' / 'const').rglob('__init__.py')) + + +def init_path(package: 'str') -> 'pathlib.Path': + """The ``__init__.py`` of ``package``.""" + return ROOT.joinpath(*package.split('.'), '__init__.py') + + +def declared(package: 'str') -> 'dict[str, tuple[str, str, bool]]': + """What the ``__init__.py`` of ``package`` imports at module level, ``TYPE_CHECKING`` included. + + Each bound name maps to the module it is imported from, its name there, and + whether the import is under ``TYPE_CHECKING``; a star import maps each name + its module's ``__all__`` lists to that module and the same name. + + """ + names = {} # type: dict[str, tuple[str, str, bool]] + + def visit(body: 'list[ast.stmt]', typing_only: 'bool') -> 'None': + for node in body: + if isinstance(node, ast.If): + guarded = isinstance(node.test, ast.Name) and node.test.id == 'TYPE_CHECKING' + visit(node.body, typing_only or guarded) + visit(node.orelse, typing_only) + elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: + for alias in node.names: + if alias.name == '*': + source = importlib.import_module(node.module) + names.update((name, (node.module, name, typing_only)) for name in source.__all__) + else: + names[alias.asname or alias.name] = (node.module, alias.name, typing_only) + + visit(ast.parse(init_path(package).read_text(encoding='utf-8')).body, False) + return names + + +def import_system_names(module: 'types.ModuleType') -> 'set[str]': + """The names importing ``module`` would leave in it, were its body empty.""" + fresh = importlib.util.module_from_spec(module.__spec__) # type: ignore[arg-type] + exec('', vars(fresh)) # pylint: disable=exec-used + return set(dir(fresh)) + + +def exported_enumerations() -> 'list[type]': + """Every class a :mod:`pcapkit.const` package exports, each once.""" + found = {} # type: dict[int, type] + for package in PACKAGES: + module = importlib.import_module(package) + for name in module.__all__: + value = getattr(module, name) + if isinstance(value, type): + found.setdefault(id(value), value) + return list(found.values()) + + +class LazyExportSweepTests(unittest.TestCase): + """Each :mod:`pcapkit.const` package, as an eager import of it would leave it.""" + + def test_the_sweep_found_the_packages(self) -> 'None': + """An empty walk would let every loop below pass without checking a package.""" + self.assertIn('pcapkit.const', PACKAGES) + self.assertIn('pcapkit.const.reg.apptype', PACKAGES) + self.assertIn('pcapkit.const.tcp', PACKAGES) + + def test_every_name_in_all_resolves_to_what_its_import_names(self) -> 'None': + for package in PACKAGES: + module = importlib.import_module(package) + imports = declared(package) + with self.subTest(package=package): + self.assertTrue(module.__all__) + self.assertEqual([name for name in module.__all__ if name not in imports], [], + 'name each export in an import under TYPE_CHECKING') + eager = sorted(name for name in module.__all__ if not imports[name][2]) + self.assertEqual(eager, sorted(EAGER.get(package, ())), + 'import each export under TYPE_CHECKING alone, so that it loads on first use') + for name in module.__all__: + source, attribute, _ = imports[name] + self.assertIs(getattr(module, name), getattr(importlib.import_module(source), attribute), + f'{package}.{name}') + + def test_star_import_binds_every_name(self) -> 'None': + for package in PACKAGES: + module = importlib.import_module(package) + namespace = {} # type: dict[str, Any] + exec(f'from {package} import *', namespace) # pylint: disable=exec-used + namespace.pop('__builtins__') + with self.subTest(package=package): + self.assertEqual(sorted(namespace), sorted(module.__all__)) + for name, value in namespace.items(): + self.assertIs(value, getattr(module, name)) + + def test_dir_lists_what_an_eager_import_bound(self) -> 'None': + # NOTE: the eager import bound every submodule its imports name, and any + # other one already imported; the first are listed whether imported or + # not, as they are not on a cold import. + for package in PACKAGES: + module = importlib.import_module(package) + named = {source[len(package) + 1:].split('.')[0] for (source, _, _) in declared(package).values() + if source.startswith(f'{package}.')} + submodules = named | {name for (name, value) in vars(module).items() + if isinstance(value, types.ModuleType) and value.__name__ == f'{package}.{name}'} + with self.subTest(package=package): + self.assertTrue(named) + self.assertEqual(dir(module), sorted({*import_system_names(module), '__all__', *module.__all__, + *submodules})) + for helper in ('TYPE_CHECKING', 'lazy_exports', '__getattr__', '__dir__'): + self.assertIn(helper, vars(module)) + self.assertNotIn(helper, dir(module)) + + def test_every_member_pickles_as_itself(self) -> 'None': + enumerations = exported_enumerations() + self.assertGreater(len(enumerations), 100, 'the sweep found implausibly few enumerations') + for enumeration in enumerations: + members = list(enumeration) + if not members: + continue + with self.subTest(enumeration=f'{enumeration.__module__}.{enumeration.__qualname__}'): + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + self.assertIs(pickle.loads(pickle.dumps(members[0], protocol)), members[0]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/project/test_const_package_exports_2656_unit.py b/tests/project/test_const_package_exports_2656_unit.py index 76915145bf..db02443383 100644 --- a/tests/project/test_const_package_exports_2656_unit.py +++ b/tests/project/test_const_package_exports_2656_unit.py @@ -10,7 +10,8 @@ * :class:`ReaderTests` -- the toctree, ``__all__`` and star-import readers the sweeps use. * :class:`ConstPackageTests` -- each subpackage is star-imported by - :mod:`pcapkit.const`, lists every name of its own ``__all__`` in + :mod:`pcapkit.const` for the type checkers and named in its ``lazy_exports`` call + for the run time (#2729), lists every name of its own ``__all__`` in :data:`pcapkit.const.__all__`, and is in the toctree of the index page. * :class:`VendorPackageTests` -- the toctree rule for :mod:`pcapkit.vendor`, which mirrors :mod:`pcapkit.const` (*Const-Vendor Pairing* in @@ -107,6 +108,14 @@ def star_imports(path: 'pathlib.Path') -> 'set[str]': and any(alias.name == '*' for alias in node.names)} +def lazy_packages(path: 'pathlib.Path') -> 'set[str]': + """The subpackages the ``packages`` argument of the ``lazy_exports`` calls in ``path`` names.""" + return {element.value for node in ast.walk(parse(path)) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == 'lazy_exports' + for keyword in node.keywords if keyword.arg == 'packages' and isinstance(keyword.value, ast.Tuple) + for element in keyword.value.elts if isinstance(element, ast.Constant)} + + def toctree_entries(path: 'pathlib.Path') -> 'set[str]': """The documents listed by every toctree in the page at ``path``, as written. @@ -184,6 +193,12 @@ def test_literal_all_reads_a_list_and_refuses_the_rest(self) -> 'None': with self.subTest(text=text), self.assertRaises(AssertionError): literal_all(self.write(text)) + def test_lazy_packages_read_the_packages_argument(self) -> 'None': + """The names in a ``packages=`` tuple count; positional and other calls' arguments do not.""" + path = self.write("a = lazy_exports(n, g, {'x': ('y', 'z')}, packages=('b', 'c'))\n" + "d = other(packages=('e',))\n") + self.assertEqual(lazy_packages(path), {'b', 'c'}) + def test_star_imports_read_nested_absolute_ones(self) -> 'None': """A star import under ``if``/``else`` counts; a named or relative one does not.""" path = self.write('from a.b import *\nif x:\n from c import *\nfrom d import e\nfrom . import *\n') @@ -201,11 +216,16 @@ def test_the_walk_found_subpackages_and_a_toctree(self) -> 'None': self.assertTrue(toctree_entries(index_page('const'))) def test_every_subpackage_is_star_imported(self) -> 'None': - """Its ``from pcapkit.const. import *`` is what puts the names on the package.""" - stars = star_imports(self.init) + """Its ``from pcapkit.const. import *``, under ``TYPE_CHECKING``, puts the names on + the package for the type checkers, and its name in the ``lazy_exports`` call does at run + time (#2729).""" + stars, lazy = star_imports(self.init), lazy_packages(self.init) missing = [name for name in subpackages('const') if name not in KNOWN_UNWIRED and f'pcapkit.const.{name}' not in stars] self.assertEqual(missing, [], 'add a "from pcapkit.const. import *" for each') + unnamed = [name for name in subpackages('const') if name not in KNOWN_UNWIRED and name not in lazy] + self.assertEqual(unnamed, [], 'name each in the packages argument of the lazy_exports call') + self.assertEqual(sorted(lazy - set(subpackages('const'))), []) def test_every_subpackage_name_is_in_all(self) -> 'None': """Each name of its own ``__all__`` is in :data:`pcapkit.const.__all__`.""" diff --git a/tests/utilities/test_lazy_exports_2729_unit.py b/tests/utilities/test_lazy_exports_2729_unit.py new file mode 100644 index 0000000000..6392673cc1 --- /dev/null +++ b/tests/utilities/test_lazy_exports_2729_unit.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +""":func:`pcapkit.utilities.lazy.lazy_exports`, on packages written for the test (GitHub issue #2729). + +Every :mod:`pcapkit.const` package uses it, and +:mod:`tests.const.test_const_lazy_exports_2729_unit` sweeps those. Here it runs +on a package tree written to a temporary directory, so each test starts with +nothing imported and can count what every step imports. + +""" + +import importlib +import pathlib +import sys +import tempfile +import textwrap +import types +import unittest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + +#: Test tier (#1958): exercises the import system and pickling, which differ between interpreters. +__tier__ = 'ALL' + +#: The top-level name of the package tree, unique to this module. +NAME = '_lazy_exports_2729_probe' + +#: The package tree, as path to source. ``probe`` exports two names from +#: ``things`` and one from its subpackage ``sub``; the top-level package +#: re-exports ``probe``'s ``__all__``, as :mod:`pcapkit.const` does its +#: subpackages', and records each module the tree imports in ``imported``. +TREE = { + '__init__.py': ''' + from pcapkit.utilities.lazy import lazy_exports + + imported = [] + + __all__ = ['Thing', 'Other', 'Deep'] + + __getattr__, __dir__ = lazy_exports(__name__, globals(), packages=('probe',)) + ''', + 'probe/__init__.py': ''' + from typing import TYPE_CHECKING + + from pcapkit.utilities.lazy import lazy_exports + + if TYPE_CHECKING: + from {NAME}.probe.sub import Deep + from {NAME}.probe.things import Other, Thing + + __all__ = ['Thing', 'Other', 'Deep'] + + _private = 'a helper of the package' + + __getattr__, __dir__ = lazy_exports(__name__, globals(), {{ + 'Thing': ('things', 'Thing'), + 'Other': ('things', 'Renamed'), + 'Deep': ('sub', 'Deep'), + }}) + ''', + 'probe/things.py': ''' + import sys + + sys.modules[{NAME!r}].imported.append(__name__) + + + class Thing: + pass + + + class Renamed: + pass + ''', + 'probe/extra.py': ''' + VALUE = 1 + ''', + 'probe/sub/__init__.py': ''' + from pcapkit.utilities.lazy import lazy_exports + + __all__ = ['Deep'] + + __getattr__, __dir__ = lazy_exports(__name__, globals(), {{'Deep': ('deep', 'Deep')}}) + ''', + 'probe/sub/deep.py': ''' + import sys + + sys.modules[{NAME!r}].imported.append(__name__) + + + class Deep: + pass + ''', +} + + +class LazyExportsTests(unittest.TestCase): + """The two hooks, from a cold import of the package tree.""" + + def setUp(self) -> 'None': + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) / NAME + for path, source in TREE.items(): + target = root / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(textwrap.dedent(source.format(NAME=NAME)).lstrip(), encoding='utf-8') + sys.path.insert(0, directory.name) + self.addCleanup(sys.path.remove, directory.name) + self.addCleanup(self.purge) + self.purge() + importlib.invalidate_caches() + self.root = importlib.import_module(NAME) + + @staticmethod + def purge() -> 'None': + for name in [name for name in sys.modules if name == NAME or name.startswith(f'{NAME}.')]: + del sys.modules[name] + + def probe(self) -> 'Any': + return importlib.import_module(f'{NAME}.probe') + + def test_a_name_is_imported_on_first_use_and_bound(self) -> 'None': + probe = self.probe() + self.assertEqual(self.root.imported, []) + self.assertNotIn('Thing', vars(probe)) + thing = probe.Thing + self.assertEqual(self.root.imported, [f'{NAME}.probe.things']) + self.assertIs(thing, sys.modules[f'{NAME}.probe.things'].Thing) + self.assertIs(vars(probe)['Thing'], thing) + self.assertIs(probe.Other, sys.modules[f'{NAME}.probe.things'].Renamed) + self.assertEqual(self.root.imported, [f'{NAME}.probe.things']) + + def test_every_import_form_resolves(self) -> 'None': + namespace = {} # type: dict[str, Any] + exec(f'from {NAME}.probe import Deep', namespace) # pylint: disable=exec-used + self.assertEqual(self.root.imported, [f'{NAME}.probe.sub.deep']) + exec(f'import {NAME}.probe.things', namespace) # pylint: disable=exec-used + exec(f'from {NAME}.probe import *', namespace) # pylint: disable=exec-used + probe = self.probe() + self.assertEqual({name: namespace[name] for name in probe.__all__}, + {name: getattr(probe, name) for name in probe.__all__}) + self.assertIs(namespace['Deep'], sys.modules[f'{NAME}.probe.sub.deep'].Deep) + + def test_a_submodule_is_imported_by_its_name(self) -> 'None': + probe = self.probe() + self.assertNotIn(f'{NAME}.probe.things', sys.modules) + self.assertIs(probe.things, sys.modules[f'{NAME}.probe.things']) + self.assertIs(vars(probe)['things'], probe.things) + self.assertIs(probe.sub.deep.Deep, probe.Deep) + + def test_a_missing_name_raises_as_a_module_does(self) -> 'None': + probe, plain = self.probe(), types.ModuleType(f'{NAME}.probe') + for name in ('nope', 'thing', 'extra', '_private_'): + with self.subTest(name=name): + with self.assertRaises(AttributeError) as lazy: + getattr(probe, name) + with self.assertRaises(AttributeError) as eager: + getattr(plain, name) + self.assertEqual(str(lazy.exception), str(eager.exception)) + self.assertFalse(hasattr(probe, name)) + self.assertEqual(self.root.imported, []) + + def test_dir_lists_what_an_eager_import_binds(self) -> 'None': + probe = self.probe() + interpreter = {name for name in vars(probe) if name.startswith('__') and name.endswith('__')} + interpreter -= {'__getattr__', '__dir__'} + expected = sorted({*interpreter, 'Thing', 'Other', 'Deep', 'things', 'sub'}) + self.assertEqual(dir(probe), expected) + self.assertEqual(self.root.imported, []) + importlib.import_module(f'{NAME}.probe.extra') + self.assertEqual(dir(probe), sorted({*expected, 'extra'})) + for name in ('TYPE_CHECKING', 'lazy_exports', '_private', '__getattr__', '__dir__'): + with self.subTest(name=name): + self.assertIn(name, vars(probe)) + self.assertNotIn(name, dir(probe)) + + def test_packages_re_exports_a_subpackage_s_all(self) -> 'None': + root = self.root + self.assertIn(f'{NAME}.probe', sys.modules) + self.assertEqual(root.imported, []) + probe = self.probe() + self.assertIs(root.Deep, probe.Deep) + self.assertEqual(root.imported, [f'{NAME}.probe.sub.deep']) + self.assertIs(root.Thing, probe.Thing) + self.assertIs(vars(root)['Thing'], probe.Thing) + interpreter = {name for name in vars(root) if name.startswith('__') and name.endswith('__')} + self.assertEqual(dir(root), sorted({*interpreter - {'__getattr__', '__dir__'}, + 'Thing', 'Other', 'Deep', 'probe'})) + + +if __name__ == '__main__': + unittest.main()