Skip to content

Commit 4c48a14

Browse files
Use Generic classes for Entity Metadata types
- Update particle IDs in Particle Data (we really need that Registry stuff) - Add tests for ParticleData - Split Masked proxy EME into BoolMasked and IntMasked for improved speed, simplicity and type checking
1 parent 3ed5833 commit 4c48a14

File tree

10 files changed

+422
-366
lines changed

10 files changed

+422
-366
lines changed

docs/conf.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from packaging.version import parse as parse_version
1818
from typing_extensions import override
1919

20-
from mcproto.types.entity.metadata import _DefaultEntityMetadataEntry, _ProxyEntityMetadataEntry
20+
from mcproto.types.entity.metadata import DefaultEntityMetadataEntryDeclaration, ProxyEntityMetadataEntryDeclaration
2121

2222
if sys.version_info >= (3, 11):
2323
from tomllib import load as toml_parse
@@ -123,9 +123,7 @@
123123

124124
def autodoc_skip_member(app: Any, what: str, name: str, obj: Any, skip: bool, options: Any) -> bool:
125125
"""Skip EntityMetadataEntry class fields as they are already documented in the docstring."""
126-
if isinstance(obj, type) and (
127-
issubclass(obj, _ProxyEntityMetadataEntry) or issubclass(obj, _DefaultEntityMetadataEntry)
128-
):
126+
if isinstance(obj, (DefaultEntityMetadataEntryDeclaration, ProxyEntityMetadataEntryDeclaration)):
129127
return True
130128
return skip
131129

mcproto/types/entity/generated.py

Lines changed: 82 additions & 81 deletions
Large diffs are not rendered by default.

mcproto/types/entity/metadata.py

Lines changed: 67 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
from __future__ import annotations
22

33
from abc import ABCMeta, abstractmethod
4-
from typing import Any, ClassVar, Literal, TypeVar, cast, final, overload
4+
from typing import Any, ClassVar, Generic, Literal, TypeVar, cast, final, overload
55

6+
from attrs import define
67
from typing_extensions import dataclass_transform, override
78

89
from mcproto.buffer import Buffer
910
from mcproto.protocol import StructFormat
1011
from mcproto.types.abc import MCType
1112

13+
T = TypeVar("T")
14+
T_2 = TypeVar("T_2")
1215

13-
class EntityMetadataEntry(MCType):
16+
17+
class EntityMetadataEntry(MCType, Generic[T]):
1418
"""Represents an entry in an entity metadata list.
1519
1620
:param index: The index of the entry.
@@ -24,26 +28,31 @@ class EntityMetadataEntry(MCType):
2428
ENTRY_TYPE: ClassVar[int] = None # type: ignore
2529

2630
index: int
27-
value: Any
31+
value: T
2832

2933
__slots__ = ("index", "value", "hidden", "default", "name")
3034

3135
def __init__(
32-
self, index: int, value: Any = None, default: Any = None, hidden: bool = False, name: str | None = None
36+
self,
37+
index: int,
38+
value: T | None = None,
39+
default: T | None = None,
40+
hidden: bool = False,
41+
name: str | None = None,
3342
):
3443
self.index = index
35-
self.value = default if value is None else value
44+
self.value = value if value is not None else default # type: ignore
3645
self.hidden = hidden
3746
self.default = default
3847
self.name = name # for debugging purposes
3948

4049
self.validate()
4150

42-
def setter(self, value: Any) -> None:
51+
def setter(self, value: T) -> None:
4352
"""Set the value of the entry."""
4453
self.value = value
4554

46-
def getter(self) -> Any:
55+
def getter(self) -> T:
4756
"""Get the value of the entry."""
4857
return self.value
4958

@@ -117,7 +126,7 @@ def read_value(cls, buf: Buffer) -> Any:
117126
@override
118127
@classmethod
119128
@final
120-
def deserialize(cls, buf: Buffer) -> EntityMetadataEntry:
129+
def deserialize(cls, buf: Buffer) -> EntityMetadataEntry[T]:
121130
"""Deserialize the entity metadata entry.
122131
123132
:param buf: The buffer to read from.
@@ -128,19 +137,19 @@ def deserialize(cls, buf: Buffer) -> EntityMetadataEntry:
128137
return cls(index=index, value=value)
129138

130139

131-
class ProxyEntityMetadataEntry(MCType):
140+
class ProxyEntityMetadataEntry(MCType, Generic[T, T_2]):
132141
"""A proxy entity metadata entry which is used to designate a part of a metadata entry in a human-readable format.
133142
134143
For example, this can be used to represent a certain mask for a ByteEME entry.
135144
"""
136145

137146
ENTRY_TYPE: ClassVar[int] = None # type: ignore
138147

139-
bound_entry: EntityMetadataEntry
148+
bound_entry: EntityMetadataEntry[T_2]
140149

141150
__slots__ = ("bound_entry",)
142151

143-
def __init__(self, bound_entry: EntityMetadataEntry, *args: Any, **kwargs: Any):
152+
def __init__(self, bound_entry: EntityMetadataEntry[T_2], *args: Any, **kwargs: Any):
144153
self.bound_entry = bound_entry
145154
self.validate()
146155

@@ -150,55 +159,43 @@ def serialize_to(self, buf: Buffer) -> None:
150159

151160
@override
152161
@classmethod
153-
def deserialize(cls, buf: Buffer) -> ProxyEntityMetadataEntry:
162+
def deserialize(cls, buf: Buffer) -> ProxyEntityMetadataEntry[T, T_2]:
154163
raise NotImplementedError("Proxy entity metadata entries cannot be deserialized.")
155164

156165
@abstractmethod
157-
def setter(self, value: Any) -> None:
166+
def setter(self, value: T) -> None:
158167
"""Set the value of the entry by modifying the bound entry."""
159168

160169
@abstractmethod
161-
def getter(self) -> Any:
170+
def getter(self) -> T:
162171
"""Get the value of the entry by reading the bound entry."""
163172

164173
def validate(self) -> None:
165174
"""Validate that the proxy metadata entry has valid values."""
166175

167176

168-
EntityDefault = TypeVar("EntityDefault")
169-
177+
@define
178+
class DefaultEntityMetadataEntryDeclaration(Generic[T]):
179+
"""Class used to pass the default metadata to the entity metadata."""
170180

171-
class _DefaultEntityMetadataEntry:
172181
m_default: Any
173-
m_type: type[EntityMetadataEntry]
182+
m_type: type[EntityMetadataEntry[T]]
174183
m_index: int
175184

176-
__slots__ = ("m_default", "m_type")
177185

178-
179-
def entry(entry_type: type[EntityMetadataEntry], value: EntityDefault) -> EntityDefault:
186+
def entry(entry_type: type[EntityMetadataEntry[T]], value: T) -> T:
180187
"""Create a entity metadata entry with the given value.
181188
182189
:param entry_type: The type of the entry.
183190
:param default: The default value of the entry.
184191
:return: The default entity metadata entry.
185192
"""
186-
187-
class DefaultEntityMetadataEntry(_DefaultEntityMetadataEntry):
188-
m_default = value
189-
m_type = entry_type
190-
m_index = -1
191-
192-
__slots__ = ()
193-
194193
# This will be taken care of by EntityMetadata
195-
return DefaultEntityMetadataEntry # type: ignore
196-
194+
return DefaultEntityMetadataEntryDeclaration(m_default=value, m_type=entry_type, m_index=-1) # type: ignore
197195

198-
ProxyInitializer = TypeVar("ProxyInitializer")
199196

200-
201-
class _ProxyEntityMetadataEntry:
197+
@define
198+
class ProxyEntityMetadataEntryDeclaration(Generic[T, T_2]):
202199
"""Class used to pass the bound entry and additional arguments to the proxy entity metadata entry.
203200
204201
Explanation:
@@ -212,21 +209,19 @@ class _ProxyEntityMetadataEntry:
212209
This is set by the EntityMetadataCreator.
213210
"""
214211

215-
m_bound_entry: EntityMetadataEntry
212+
m_bound_entry: EntityMetadataEntry[T_2]
216213
m_args: tuple[Any]
217214
m_kwargs: dict[str, Any]
218-
m_type: type[ProxyEntityMetadataEntry]
215+
m_type: type[ProxyEntityMetadataEntry[T, T_2]]
219216
m_bound_index: int
220217

221-
__slots__ = ("m_bound_entry", "m_args", "m_kwargs", "m_type", "m_bound_index")
222-
223218

224219
def proxy(
225-
bound_entry: EntityDefault, # type: ignore # Used only once but I prefer to keep the type hint
226-
proxy: type[ProxyEntityMetadataEntry],
220+
bound_entry: T_2, # This will in fact be an EntityMetadataEntry, but treated as a T_2 during type checking
221+
proxy: type[ProxyEntityMetadataEntry[T, T_2]],
227222
*args: Any,
228223
**kwargs: Any,
229-
) -> ProxyInitializer: # type: ignore
224+
) -> T:
230225
"""Initialize the proxy entity metadata entry with the given bound entry and additional arguments.
231226
232227
:param bound_entry: The bound entry.
@@ -236,22 +231,16 @@ def proxy(
236231
237232
:return: The proxy entity metadata entry initializer.
238233
"""
239-
if not isinstance(bound_entry, type):
234+
if not isinstance(bound_entry, DefaultEntityMetadataEntryDeclaration):
240235
raise TypeError("The bound entry must be an entity metadata entry type.")
241-
if not issubclass(bound_entry, _DefaultEntityMetadataEntry):
242-
raise TypeError("The bound entry must be an entity metadata entry.")
243-
244-
class ProxyEntityMetadataEntry(_ProxyEntityMetadataEntry):
245-
m_bound_entry = bound_entry # type: ignore # This will be taken care of by EntityMetadata
246-
m_args = args
247-
m_kwargs = kwargs
248-
m_type = proxy
249-
250-
m_bound_index = -1
251236

252-
__slots__ = ()
253-
254-
return ProxyEntityMetadataEntry # type: ignore
237+
return ProxyEntityMetadataEntryDeclaration( # type: ignore
238+
m_bound_entry=bound_entry, # type: ignore
239+
m_args=args,
240+
m_kwargs=kwargs,
241+
m_type=proxy,
242+
m_bound_index=-1,
243+
)
255244

256245

257246
@dataclass_transform(kw_only_default=True) # field_specifiers=(entry, proxy))
@@ -265,10 +254,16 @@ class EntityMetadataCreator(ABCMeta):
265254
```
266255
"""
267256

268-
m_defaults: ClassVar[dict[str, type[_DefaultEntityMetadataEntry | _ProxyEntityMetadataEntry]]]
257+
m_defaults: ClassVar[
258+
dict[
259+
str,
260+
DefaultEntityMetadataEntryDeclaration[Any]
261+
| ProxyEntityMetadataEntryDeclaration[Any, EntityMetadataEntry[Any]],
262+
]
263+
]
269264
m_index: ClassVar[dict[int, str]]
270265
m_metadata: ClassVar[
271-
dict[str, EntityMetadataEntry | ProxyEntityMetadataEntry]
266+
dict[str, EntityMetadataEntry[Any] | ProxyEntityMetadataEntry[Any, EntityMetadataEntry[Any]]]
272267
] # This is not an actual classvar, but I
273268
# Do not want it to appear in the __init__ signature
274269

@@ -309,7 +304,8 @@ def setup_class(cls: type[EntityMetadata]) -> None:
309304
if default is None:
310305
raise ValueError(f"Default value for {name} is not set. Use the entry() or proxy() functions.")
311306
# Check if we have a default entry
312-
if isinstance(default, type) and issubclass(default, _DefaultEntityMetadataEntry):
307+
if isinstance(default, DefaultEntityMetadataEntryDeclaration):
308+
default = cast(DefaultEntityMetadataEntryDeclaration[Any], default)
313309
# Set the index of the entry
314310
default.m_index = current_index
315311

@@ -321,7 +317,8 @@ def setup_class(cls: type[EntityMetadata]) -> None:
321317

322318
# Increment the index
323319
current_index += 1
324-
elif isinstance(default, type) and issubclass(default, _ProxyEntityMetadataEntry):
320+
elif isinstance(default, ProxyEntityMetadataEntryDeclaration):
321+
default = cast(ProxyEntityMetadataEntryDeclaration[Any, EntityMetadataEntry[Any]], default)
325322
# Find the bound entry
326323
if id(default.m_bound_entry) not in bound_index:
327324
raise ValueError(f"Bound entry for {name} is not set.")
@@ -364,14 +361,16 @@ def __init__(self, *args: None, **kwargs: Any) -> None:
364361
raise ValueError(
365362
"EntityMetadata does not accept positional arguments. Specify all metadata entries by name."
366363
)
367-
self.m_metadata: dict[str, EntityMetadataEntry | ProxyEntityMetadataEntry] = {}
364+
self.m_metadata: dict[
365+
str, EntityMetadataEntry[Any] | ProxyEntityMetadataEntry[Any, EntityMetadataEntry[Any]]
366+
] = {}
368367
for name, default in self.m_defaults.items():
369-
if issubclass(default, _DefaultEntityMetadataEntry):
368+
if isinstance(default, DefaultEntityMetadataEntryDeclaration):
370369
self.m_metadata[name] = default.m_type(index=default.m_index, default=default.m_default, name=name)
371-
elif issubclass(default, _ProxyEntityMetadataEntry): # type: ignore # We want to check anyways
370+
elif isinstance(default, ProxyEntityMetadataEntryDeclaration): # type: ignore # Check anyway
372371
# Bound entry
373372
bound_name = self.m_index[default.m_bound_index]
374-
bound_entry = cast(EntityMetadataEntry, self.m_metadata[bound_name])
373+
bound_entry = cast(EntityMetadataEntry[Any], self.m_metadata[bound_name])
375374
self.m_metadata[name] = default.m_type(bound_entry, *default.m_args, **default.m_kwargs)
376375
else: # pragma: no cover
377376
raise ValueError(f"Invalid default value for {name}. Use the entry() or proxy() functions.") # noqa: TRY004
@@ -383,13 +382,18 @@ def __init__(self, *args: None, **kwargs: Any) -> None:
383382

384383
@override
385384
def __setattr__(self, name: str, value: Any) -> None:
385+
"""Any is used here because the type will be discovered statically by other means (dataclass_transform)."""
386386
if name != "m_metadata" and hasattr(self, "m_metadata") and name in self.m_metadata:
387387
self.m_metadata[name].setter(value)
388388
else:
389389
super().__setattr__(name, value)
390390

391391
@override
392392
def __getattribute__(self, name: str) -> Any:
393+
"""Get the value of the metadata entry.
394+
395+
.. seealso:: :meth:`__setattr__`
396+
"""
393397
if name != "m_metadata" and hasattr(self, "m_metadata") and name in self.m_metadata:
394398
return self.m_metadata[name].getter()
395399
return super().__getattribute__(name)

0 commit comments

Comments
 (0)