Skip to content

Commit aeb7272

Browse files
committed
pre-finalise infoclass to save runtime
1 parent 43cf904 commit aeb7272

40 files changed

+436
-92
lines changed

docs/source/pcapkit/corekit/infoclass.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ in :pep:`557`.
2121
:no-value:
2222
.. autoattribute:: __excluded__
2323
:no-value:
24+
25+
.. autodecorator:: pcapkit.corekit.infoclass.info_final

pcapkit/corekit/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ class :class:`~pcapkit.corekit.infoclass.Info`,
1616
1717
"""
1818
from pcapkit.corekit.fields import *
19-
from pcapkit.corekit.infoclass import Info
19+
from pcapkit.corekit.infoclass import Info, info_final
2020
from pcapkit.corekit.multidict import MultiDict, OrderedMultiDict
2121
from pcapkit.corekit.protochain import ProtoChain
2222
from pcapkit.corekit.version import VersionInfo
2323

2424
__all__ = [
25-
'Info',
25+
'Info', 'info_final',
2626

2727
'ProtoChain',
2828

pcapkit/corekit/infoclass.py

Lines changed: 100 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,112 @@
1212
"""
1313
import collections.abc
1414
import itertools
15-
from typing import TYPE_CHECKING, Generic, TypeVar
15+
from typing import TYPE_CHECKING, Generic, TypeVar, cast, final
1616

1717
from pcapkit.utilities.compat import Mapping
18-
from pcapkit.utilities.exceptions import UnsupportedCall
18+
from pcapkit.utilities.exceptions import UnsupportedCall, stacklevel
19+
from pcapkit.utilities.warnings import InfoWarning, warn
1920

2021
if TYPE_CHECKING:
21-
from typing import Any, Iterable, Iterator, NoReturn, Optional
22+
from typing import Any, Iterable, Iterator, NoReturn, Optional, Type
2223

2324
from typing_extensions import Self
2425

25-
__all__ = ['Info']
26+
__all__ = ['Info', 'info_final']
2627

2728
VT = TypeVar('VT')
2829

2930

31+
def info_final(cls: 'Type[Info[VT]]') -> 'Type[Info[VT]]':
32+
"""Finalise info class.
33+
34+
Args:
35+
cls: Info class.
36+
37+
Returns:
38+
Finalised info class.
39+
40+
Notes:
41+
This decorator function is used to generate necessary
42+
attributes and methods for the decorated :class:`Info`
43+
class. It can be useful to reduce runtime generation
44+
time as well as caching already generated attributes.
45+
46+
:meta decorator:
47+
"""
48+
if cls.__finalised__:
49+
warn(f'{cls.__name__}: info class has been finalised; now skipping',
50+
InfoWarning, stacklevel=stacklevel())
51+
return cls
52+
53+
temp = ['__map__', '__map_reverse__', '__builtin__', '__finalised__']
54+
temp.extend(cls.__additional__)
55+
for obj in cls.mro():
56+
temp.extend(dir(obj))
57+
cls.__builtin__ = set(temp)
58+
cls.__excluded__.extend(cls.__builtin__)
59+
60+
# NOTE: We only generate ``__init__`` method for subclasses of the
61+
# ``Info`` class, rather than itself, plus that such class does not
62+
# override the ``__init__`` method of the meta class.
63+
if '__init__' not in cls.__dict__ and cls is not Info:
64+
args_ = [] # type: list[str]
65+
dict_ = [] # type: list[str]
66+
67+
for cls_ in cls.mro():
68+
# NOTE: We skip the ``Info`` class itself, to avoid superclass
69+
# type annotations being considered.
70+
if cls_ is Info:
71+
break
72+
73+
# NOTE: We iterate in reversed order to ensure that the type
74+
# annotations of the superclasses are considered first.
75+
for key in reversed(cls_.__annotations__):
76+
# NOTE: We skip duplicated annotations to avoid duplicate
77+
# argument in function definition.
78+
if key in args_:
79+
continue
80+
81+
args_.append(key)
82+
dict_.append(f'{key}={key}')
83+
84+
# NOTE: We reverse the two lists such that the order of the
85+
# arguments is the same as the order of the type annotations, i.e.,
86+
# from the most base class to the most derived class.
87+
args_.reverse()
88+
dict_.reverse()
89+
90+
# NOTE: We only generate typed ``__init__`` method if only the class
91+
# has type annotations from any of itself and its base classes.
92+
if args_:
93+
# NOTE: The following code is to make the ``__init__`` method work.
94+
# It is inspired from the :func:`dataclasses._create_fn` function.
95+
init_ = (
96+
f'def __create_fn__():\n'
97+
f' def __init__(self, {", ".join(args_)}):\n'
98+
f' self.__update__({", ".join(dict_)})\n'
99+
f' self.__post_init__()\n'
100+
f' return __init__\n'
101+
)
102+
else:
103+
init_ = (
104+
'def __create_fn__():\n'
105+
' def __init__(self, dict_=None, **kwargs):\n'
106+
' self.__update__(dict_, **kwargs)\n'
107+
' self.__post_init__()\n'
108+
' return __init__\n'
109+
)
110+
111+
ns = {} # type: dict[str, Any]
112+
exec(init_, None, ns) # pylint: disable=exec-used # nosec
113+
114+
cls.__init__ = ns['__create_fn__']()
115+
cls.__init__.__qualname__ = f'{cls.__name__}.__init__'
116+
117+
cls.__finalised__ = True
118+
return final(cls)
119+
120+
30121
class Info(Mapping[str, VT], Generic[VT]):
31122
"""Turn dictionaries into :obj:`object` like instances.
32123
@@ -55,6 +146,9 @@ class Info(Mapping[str, VT], Generic[VT]):
55146
#: List of builtin methods.
56147
__builtin__: 'set[str]'
57148

149+
#: Flag for finalised class initialisation.
150+
__finalised__ = False
151+
58152
#: List of additional built-in names.
59153
__additional__: 'list[str]' = []
60154
#: List of names to be excluded from :obj:`dict` conversion.
@@ -72,69 +166,8 @@ def __new__(cls, *args: 'VT', **kwargs: 'VT') -> 'Self': # pylint: disable=unus
72166
**kwargs: Arbitrary keyword arguments.
73167
74168
"""
75-
temp = ['__map__', '__map_reverse__', '__builtin__']
76-
temp.extend(cls.__additional__)
77-
for obj in cls.mro():
78-
temp.extend(dir(obj))
79-
cls.__builtin__ = set(temp)
80-
cls.__excluded__.extend(cls.__builtin__)
81-
82-
# NOTE: We only generate ``__init__`` method for subclasses of the
83-
# ``Info`` class, rather than itself, plus that such class does not
84-
# override the ``__init__`` method of the meta class.
85-
if '__init__' not in cls.__dict__ and cls is not Info:
86-
args_ = [] # type: list[str]
87-
dict_ = [] # type: list[str]
88-
89-
for cls_ in cls.mro():
90-
# NOTE: We skip the ``Info`` class itself, to avoid superclass
91-
# type annotations being considered.
92-
if cls_ is Info:
93-
break
94-
95-
# NOTE: We iterate in reversed order to ensure that the type
96-
# annotations of the superclasses are considered first.
97-
for key in reversed(cls_.__annotations__):
98-
# NOTE: We skip duplicated annotations to avoid duplicate
99-
# argument in function definition.
100-
if key in args_:
101-
continue
102-
103-
args_.append(key)
104-
dict_.append(f'{key}={key}')
105-
106-
# NOTE: We reverse the two lists such that the order of the
107-
# arguments is the same as the order of the type annotations, i.e.,
108-
# from the most base class to the most derived class.
109-
args_.reverse()
110-
dict_.reverse()
111-
112-
# NOTE: We only generate typed ``__init__`` method if only the class
113-
# has type annotations from any of itself and its base classes.
114-
if args_:
115-
# NOTE: The following code is to make the ``__init__`` method work.
116-
# It is inspired from the :func:`dataclasses._create_fn` function.
117-
init_ = (
118-
f'def __create_fn__():\n'
119-
f' def __init__(self, {", ".join(args_)}):\n'
120-
f' self.__update__({", ".join(dict_)})\n'
121-
f' self.__post_init__()\n'
122-
f' return __init__\n'
123-
)
124-
else:
125-
init_ = (
126-
'def __create_fn__():\n'
127-
' def __init__(self, dict_=None, **kwargs):\n'
128-
' self.__update__(dict_, **kwargs)\n'
129-
' self.__post_init__()\n'
130-
' return __init__\n'
131-
)
132-
133-
ns = {} # type: dict[str, Any]
134-
exec(init_, None, ns) # pylint: disable=exec-used # nosec
135-
cls.__init__ = ns['__create_fn__']()
136-
cls.__init__.__qualname__ = f'{cls.__name__}.__init__'
137-
169+
if not cls.__finalised__:
170+
cls = cast('Type[Self]', info_final(cls))
138171
self = super().__new__(cls)
139172

140173
# NOTE: We define the ``__map__`` and ``__map_reverse__`` attributes

pcapkit/foundation/engines/pcapng.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import TYPE_CHECKING, cast
1313

1414
from pcapkit.const.pcapng.block_type import BlockType as Enum_BlockType
15-
from pcapkit.corekit.infoclass import Info
15+
from pcapkit.corekit.infoclass import Info, info_final
1616
from pcapkit.foundation.engines.engine import Engine
1717
from pcapkit.protocols.misc.pcapng import PCAPNG as P_PCAPNG
1818
from pcapkit.utilities.exceptions import FormatError, stacklevel
@@ -39,6 +39,7 @@
3939
from pcapkit.protocols.data.misc.pcapng import UnknownBlock as Data_UnknownBlock
4040

4141

42+
@info_final
4243
class Context(Info):
4344
"""Context manager for PCAP-NG file format."""
4445

pcapkit/foundation/reassembly/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@
2828

2929
from typing import TYPE_CHECKING
3030

31-
from pcapkit.corekit.infoclass import Info
31+
from pcapkit.corekit.infoclass import Info, info_final
3232

3333
if TYPE_CHECKING:
3434
from typing import Optional
3535

3636

37+
@info_final
3738
class ReassemblyManager(Info):
3839
"""Reassembly Manager."""
3940

pcapkit/foundation/reassembly/data/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@
2727

2828
from typing import TYPE_CHECKING
2929

30-
from pcapkit.corekit.infoclass import Info
30+
from pcapkit.corekit.infoclass import Info, info_final
3131

3232
if TYPE_CHECKING:
3333
from typing import Optional
3434

3535

36+
@info_final
3637
class ReassemblyData(Info):
3738
"""Data storage for reassembly."""
3839

pcapkit/foundation/reassembly/data/ip.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from typing import TYPE_CHECKING, Generic, TypeVar
55

6-
from pcapkit.corekit.infoclass import Info
6+
from pcapkit.corekit.infoclass import Info, info_final
77
from pcapkit.utilities.compat import Tuple
88

99
__all__ = [
@@ -25,6 +25,7 @@
2525
BufferID = Tuple[AT, AT, int, 'TransType']
2626

2727

28+
@info_final
2829
class Packet(Info, Generic[AT]):
2930
"""Data model for :term:`reasm.ipv4.packet` / :term:`reasm.ipv6.packet`."""
3031

@@ -49,6 +50,7 @@ class Packet(Info, Generic[AT]):
4950
def __init__(self, bufid: 'tuple[AT, AT, int, TransType]', num: 'int', fo: 'int', ihl: 'int', mf: 'bool', tl: 'int', header: 'bytes', payload: 'bytearray') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
5051

5152

53+
@info_final
5254
class DatagramID(Info, Generic[AT]):
5355
"""Data model for :term:`reasm.ipv4.datagram` / :term:`reasm.ipv6.datagram` original packet identifier."""
5456

@@ -65,6 +67,7 @@ class DatagramID(Info, Generic[AT]):
6567
def __init__(self, src: 'AT', dst: 'AT', id: 'int', proto: 'TransType') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
6668

6769

70+
@info_final
6871
class Datagram(Info, Generic[AT]):
6972
"""Data model for :term:`reasm.ipv4.datagram` / :term:`reasm.ipv6.datagram`."""
7073

@@ -91,6 +94,7 @@ def __init__(self, completed: 'Literal[False]', id: 'DatagramID[AT]', index: 'tu
9194
def __init__(self, completed: 'bool', id: 'DatagramID[AT]', index: 'tuple[int, ...]', header: 'bytes', payload: 'bytes | tuple[bytes, ...]', packet: 'Optional[Protocol]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
9295

9396

97+
@info_final
9498
class Buffer(Info, Generic[AT]):
9599
"""Data model for :term:`reasm.ipv4.buffer` / :term:`reasm.ipv6.buffer`."""
96100

pcapkit/foundation/reassembly/data/tcp.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from typing import TYPE_CHECKING, Generic, TypeVar
55

6-
from pcapkit.corekit.infoclass import Info
6+
from pcapkit.corekit.infoclass import Info, info_final
77
from pcapkit.utilities.compat import Tuple
88

99
__all__ = [
@@ -25,6 +25,7 @@
2525
BufferID = Tuple[IPAddress, int, IPAddress, int]
2626

2727

28+
@info_final
2829
class Packet(Info):
2930
"""Data model for :term:`reasm.tcp.packet`."""
3031

@@ -57,6 +58,7 @@ class Packet(Info):
5758
def __init__(self, bufid: 'BufferID', dsn: 'int', ack: 'int', num: 'int', syn: 'bool', fin: 'bool', rst: 'bool', len: 'int', first: 'int', last: 'int', header: 'bytes', payload: 'bytearray') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
5859

5960

61+
@info_final
6062
class DatagramID(Info, Generic[IPAddress]):
6163
"""Data model for :term:`reasm.tcp.datagram` original packet identifier."""
6264

@@ -71,6 +73,7 @@ class DatagramID(Info, Generic[IPAddress]):
7173
def __init__(self, src: 'tuple[IPAddress, int]', dst: 'tuple[IPAddress, int]', ack: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
7274

7375

76+
@info_final
7477
class Datagram(Info, Generic[IPAddress]):
7578
"""Data model for :term:`reasm.tcp.datagram`."""
7679

@@ -97,6 +100,7 @@ def __init__(self, completed: 'Literal[False]', id: 'DatagramID[IPAddress]', ind
97100
def __init__(self, completed: 'bool', id: 'DatagramID[IPAddress]', index: 'tuple[int, ...]', header: 'bytes', payload: 'bytes | tuple[bytes, ...]', packet: 'Optional[Protocol]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
98101

99102

103+
@info_final
100104
class HoleDiscriptor(Info):
101105
"""Data model for :term:`reasm.tcp.buffer` hole descriptor."""
102106

@@ -109,6 +113,7 @@ class HoleDiscriptor(Info):
109113
def __init__(self, first: 'int', last: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
110114

111115

116+
@info_final
112117
class Fragment(Info):
113118
"""Data model for :term:`reasm.tcp.buffer` ACK list fragment item."""
114119

@@ -125,6 +130,7 @@ class Fragment(Info):
125130
def __init__(self, ind: 'list[int]', isn: 'int', len: 'int', raw: 'bytearray') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
126131

127132

133+
@info_final
128134
class Buffer(Info):
129135
"""Data model for :term:`reasm.tcp.buffer`."""
130136

pcapkit/foundation/traceflow/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@
2727

2828
from typing import TYPE_CHECKING
2929

30-
from pcapkit.corekit.infoclass import Info
30+
from pcapkit.corekit.infoclass import Info, info_final
3131

3232
if TYPE_CHECKING:
3333
from typing import Optional
3434

3535

36+
@info_final
3637
class TraceFlowManager(Info):
3738
"""TraceFlow Manager."""
3839

pcapkit/foundation/traceflow/data/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313

1414
from typing import TYPE_CHECKING
1515

16-
from pcapkit.corekit.infoclass import Info
16+
from pcapkit.corekit.infoclass import Info, info_final
1717

1818
if TYPE_CHECKING:
1919
from typing import Optional
2020

2121

22+
@info_final
2223
class TraceFlowData(Info):
2324
"""Data storage for flow tracing."""
2425

0 commit comments

Comments
 (0)