pylitematic 0.0.0__tar.gz → 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylitematic
3
- Version: 0.0.0
3
+ Version: 0.0.1
4
4
  Summary: Load, modify, and save Litematica schematics
5
5
  Author-email: Boscawinks <bosca.winks@gmx.de>
6
6
  License: GPL-3.0-only
7
- Project-URL: Homepage, https://github.com/nieswand/pylitematic
8
- Project-URL: Repository, https://github.com/nieswand/pylitematic
9
- Project-URL: Issues, https://github.com/nieswand/pylitematic/issues
7
+ Project-URL: Homepage, https://github.com/boscawinks/pylitematic
8
+ Project-URL: Repository, https://github.com/boscawinks/pylitematic
9
+ Project-URL: Issues, https://github.com/boscawinks/pylitematic/issues
10
10
  Classifier: Programming Language :: Python :: 3
11
- Requires-Python: >=3.7
11
+ Requires-Python: >=3.10.12
12
12
  Description-Content-Type: text/markdown
13
13
  Requires-Dist: bitpacking>=0.1.0
14
- Requires-Dist: nbtlib>=1.6
15
- Requires-Dist: numpy>=1.21
14
+ Requires-Dist: nbtlib>=2.0.4
15
+ Requires-Dist: numpy>=2.2.6
16
16
 
17
17
  # pylitematic
18
18
 
@@ -25,5 +25,5 @@ Load, modify, and save [Litematica](https://litematica.org/) schematics
25
25
 
26
26
  `pylitematic` is available on pypi:
27
27
  ```bash
28
- pip install --upgrade pylitematic
28
+ python3 -m pip install --upgrade pylitematic
29
29
  ```
@@ -9,5 +9,5 @@ Load, modify, and save [Litematica](https://litematica.org/) schematics
9
9
 
10
10
  `pylitematic` is available on pypi:
11
11
  ```bash
12
- pip install --upgrade pylitematic
12
+ python3 -m pip install --upgrade pylitematic
13
13
  ```
@@ -1,30 +1,30 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61.0"]
2
+ requires = ["setuptools>=59.6.0"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pylitematic"
7
- version = "0.0.0"
7
+ version = "0.0.1"
8
8
  description = "Load, modify, and save Litematica schematics"
9
9
  authors = [
10
10
  { name="Boscawinks", email="bosca.winks@gmx.de" }
11
11
  ]
12
12
  dependencies = [
13
13
  "bitpacking >=0.1.0",
14
- "nbtlib >=1.6",
15
- "numpy >=1.21"
14
+ "nbtlib >=2.0.4",
15
+ "numpy >=2.2.6"
16
16
  ]
17
17
  readme = "README.md"
18
- requires-python = ">=3.7"
18
+ requires-python = ">=3.10.12"
19
19
  license = { text = "GPL-3.0-only" }
20
20
  classifiers = [
21
21
  "Programming Language :: Python :: 3",
22
22
  ]
23
23
 
24
24
  [project.urls]
25
- Homepage = "https://github.com/nieswand/pylitematic"
26
- Repository = "https://github.com/nieswand/pylitematic"
27
- Issues = "https://github.com/nieswand/pylitematic/issues"
25
+ Homepage = "https://github.com/boscawinks/pylitematic"
26
+ Repository = "https://github.com/boscawinks/pylitematic"
27
+ Issues = "https://github.com/boscawinks/pylitematic/issues"
28
28
 
29
29
  [tool.setuptools.packages.find]
30
30
  where = ["src"]
File without changes
@@ -0,0 +1,7 @@
1
+ __version__ = "0.0.1"
2
+
3
+ from .block_state import BlockState
4
+ from .geometry import BlockPosition, Size3D
5
+ from .region import Region
6
+ from .resource_location import ResourceLocation
7
+ from .schematic import Schematic
@@ -0,0 +1,325 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ import json
5
+ import nbtlib
6
+ import re
7
+ from typing import Any
8
+
9
+
10
+ PROPERTY_NAME_REGEX: str = r"[a-z][a-z0-9_]*"
11
+ PROPERTY_NAME_PATTERN: re.Pattern = re.compile(PROPERTY_NAME_REGEX)
12
+
13
+ ENUM_VALUE_REGEX: str = r"[a-z]+(_[a-z]+)*" # snake case
14
+ ENUM_VALUE_PATTERN: re.Pattern = re.compile(ENUM_VALUE_REGEX)
15
+
16
+
17
+ class Property():
18
+
19
+ __slots__ = ("_name", "_value")
20
+
21
+ def __init__(self, name: str, value: Any | PropertyValue) -> None:
22
+ if not PROPERTY_NAME_PATTERN.fullmatch(name):
23
+ raise ValueError(f"Invalid property name {name!r}")
24
+ self._name = name
25
+
26
+ if not isinstance(value, PropertyValue):
27
+ value = PropertyValue.value_factory(value=value)
28
+ self._value = value
29
+
30
+ def __str__(self) -> str:
31
+ return f"{self._name}={self._value}"
32
+
33
+ def __repr__(self) -> str:
34
+ return (
35
+ f"{type(self).__name__}("
36
+ f"name: {self._name}, value: {self._value!r})")
37
+
38
+ def __eq__(self, other: Any) -> bool:
39
+ if not isinstance(other, Property):
40
+ return NotImplemented
41
+ return (self.name, self.value) == (other.name, other.value)
42
+
43
+ def __lt__(self, other: Any) -> bool:
44
+ if not isinstance(other, Property):
45
+ return NotImplemented
46
+ return (self.name, self.value) < (other.name, other.value)
47
+
48
+ @property
49
+ def name(self) -> str:
50
+ return self._name
51
+
52
+ @property
53
+ def value(self) -> Any:
54
+ return self._value.get()
55
+
56
+ @value.setter
57
+ def value(self, value: Any) -> None:
58
+ self._value.set(value)
59
+
60
+ def to_string(self) -> str:
61
+ return str(self)
62
+
63
+ @staticmethod
64
+ def from_string(string: str, value: str | None = None) -> Property:
65
+ if value is None:
66
+ # tread string as "name=value"
67
+ try:
68
+ string, value = string.split("=")
69
+ except ValueError as exc:
70
+ raise ValueError(f"Invalid property string {string!r}") from exc
71
+ return Property(name=string, value=PropertyValue.from_string(value))
72
+
73
+ def to_nbt(self) -> tuple[str, nbtlib.String]:
74
+ # return nbtlib.Compound(Name=nbtlib.String(self._name), Value=self._value.to_nbt()})
75
+ return self._name, self._value.to_nbt()
76
+
77
+ @staticmethod
78
+ def from_nbt(name: str, nbt: nbtlib.String) -> Property:
79
+ # return Property.from_string(name=nbt["Name"], value=str(nbt["Value"]))
80
+ return Property.from_string(string=name, value=str(nbt))
81
+
82
+
83
+ class Properties(dict):
84
+
85
+ def __init__(self, *args, **kwargs):
86
+ props = {}
87
+ for name, value in dict(*args, **kwargs).items():
88
+ self.validate_name(name)
89
+ props[name] = PropertyValue.value_factory(value)
90
+ super().__init__(props)
91
+
92
+ def __getitem__(self, key):
93
+ return super().__getitem__(key).get()
94
+
95
+ def __setitem__(self, key, value):
96
+ if key not in self:
97
+ self.validate_name(key)
98
+ super().__setitem__(key, PropertyValue.value_factory(value))
99
+ else:
100
+ super().__getitem__(key).set(value)
101
+
102
+ def __lt__(self, other: Any) -> bool:
103
+ if not isinstance(other, Properties):
104
+ return NotImplemented
105
+ return sorted(self.items()) < sorted(other.items())
106
+
107
+ def __hash__(self) -> int:
108
+ return hash(tuple(sorted(self)))
109
+
110
+ def __str__(self) -> str:
111
+ props_str = [f"{n}={v}" for n, v in sorted(super().items())]
112
+ return f"[{','.join(props_str)}]"
113
+
114
+ def __repr__(self) -> str:
115
+ props_reps = [f"{n}: {v!r}" for n, v in sorted(super().items())]
116
+ return f"{type(self).__name__}({', '.join(props_reps)})"
117
+
118
+ def get(self, key, default=None):
119
+ value = super().get(key, None)
120
+ if value is None:
121
+ return default
122
+ return value.get()
123
+
124
+ def setdefault(self, key, default=None):
125
+ if key not in self:
126
+ self[key] = default
127
+ return self[key]
128
+
129
+ def update(self, *args, **kwargs):
130
+ for name, value in dict(*args, **kwargs).items():
131
+ self[name] = value
132
+
133
+ def pop(self, key, default=None):
134
+ value = super().pop(key, None)
135
+ if value is None:
136
+ return default
137
+ return value.get()
138
+
139
+ def popitem(self):
140
+ name, value = super().popitem()
141
+ return name, value.get()
142
+
143
+ def values(self):
144
+ for value in super().values():
145
+ yield value.get()
146
+
147
+ def items(self):
148
+ for key, value in super().items():
149
+ yield key, value.get()
150
+
151
+ @staticmethod
152
+ def is_valid_name(name: str) -> bool:
153
+ return PROPERTY_NAME_PATTERN.fullmatch(name) is not None
154
+
155
+ @staticmethod
156
+ def validate_name(name: str) -> None:
157
+ if not Properties.is_valid_name(name=name):
158
+ raise ValueError(f"Invalid property name {name!r}")
159
+
160
+ def to_string(self) -> str:
161
+ return str(self)
162
+
163
+ @staticmethod
164
+ def from_string(string: str) -> Properties:
165
+ if string in ("", "[]"):
166
+ return Properties()
167
+
168
+ if not (string.startswith("[") and string.endswith("]")):
169
+ raise ValueError(f"Invalid properties string {string!r}")
170
+ string = string[1:-1]
171
+
172
+ props = {}
173
+ for prop in string.split(","):
174
+ try:
175
+ name, val_str = prop.split("=")
176
+ except ValueError as exc:
177
+ raise ValueError(f"Invalid property string {string!r}") from exc
178
+ if name in props:
179
+ ValueError(f"Duplicate property name {name!r}")
180
+ props[name] = PropertyValue.from_string(string=val_str).get()
181
+
182
+ return Properties(props)
183
+
184
+ def to_nbt(self) -> nbtlib.Compound:
185
+ return nbtlib.Compound(
186
+ {name: value.to_nbt() for name, value in sorted(super().items())})
187
+
188
+ @staticmethod
189
+ def from_nbt(nbt: nbtlib.Compound) -> Properties:
190
+ props = {}
191
+ for name, value in nbt.items():
192
+ props[name] = PropertyValue.from_nbt(nbt=value).get()
193
+ return Properties(props)
194
+
195
+
196
+ class PropertyValue(ABC):
197
+
198
+ __slots__ = ("_value")
199
+ __registry: dict[type, type[PropertyValue]] = {}
200
+
201
+ def __init_subclass__(cls, **kwargs) -> None:
202
+ super().__init_subclass__(**kwargs)
203
+ py_type = cls.python_type()
204
+ if py_type in cls.__registry:
205
+ raise ValueError(
206
+ f"Duplicate Value subclass for type {py_type.__name__!r}:"
207
+ f" {cls.__registry[py_type].__name__} vs {cls.__name__}")
208
+ cls.__registry[py_type] = cls
209
+
210
+ def __init__(self, value: Any) -> None:
211
+ self.set(value)
212
+
213
+ def __str__(self) -> str:
214
+ return json.dumps(self._value)
215
+
216
+ def __repr__(self) -> str:
217
+ return f"{type(self).__name__}({self._value!r})"
218
+
219
+ def __hash__(self) -> int:
220
+ return hash((self.__class__, self._value))
221
+
222
+ def __eq__(self, other: Any) -> bool:
223
+ if not isinstance(other, self.__class__):
224
+ return NotImplemented
225
+ return self._value == other._value
226
+
227
+ def __lt__(self, other: Any) -> bool:
228
+ if not isinstance(other, PropertyValue):
229
+ return NotImplemented
230
+ return self._value < other._value
231
+
232
+ @classmethod
233
+ @abstractmethod
234
+ def is_valid_value(cls, value: Any) -> bool:
235
+ ...
236
+
237
+ @classmethod
238
+ def validate_value(cls, value: Any) -> None:
239
+ if not isinstance(value, cls.python_type()):
240
+ raise TypeError(
241
+ f"{cls.__name__} expects value of type"
242
+ f" {cls.python_type().__name__}, got {type(value).__name__}"
243
+ f" ({value!r})")
244
+ if not cls.is_valid_value(value):
245
+ raise ValueError(f"Invalid value {value!r} for {cls.__name__}")
246
+
247
+ def get(self) -> Any:
248
+ return self._value
249
+
250
+ def set(self, value: Any) -> None:
251
+ self.validate_value(value=value)
252
+ self._value = self.python_type()(value)
253
+
254
+ @classmethod
255
+ @abstractmethod
256
+ def python_type(cls) -> type:
257
+ """Return the native Python type this Value corresponds to."""
258
+
259
+ @staticmethod
260
+ def value_factory(value: Any) -> PropertyValue:
261
+ reg = PropertyValue.__registry
262
+ sub_cls = reg.get(type(value))
263
+ if sub_cls is None:
264
+ opt_str = ", ".join(map(lambda x: x.__name__, reg))
265
+ raise TypeError(
266
+ f"No Value subclass registered for {type(value).__name__} value"
267
+ f" {value!r}. Classes registered for: {opt_str}")
268
+ return sub_cls(value)
269
+
270
+ def to_string(self) -> str:
271
+ return str(self)
272
+
273
+ @staticmethod
274
+ def from_string(string: str) -> PropertyValue:
275
+ try:
276
+ value = json.loads(string)
277
+ except json.JSONDecodeError:
278
+ value = string
279
+ return PropertyValue.value_factory(value)
280
+
281
+ def to_nbt(self) -> nbtlib.String:
282
+ return nbtlib.String(self)
283
+
284
+ @staticmethod
285
+ def from_nbt(nbt: nbtlib.String) -> PropertyValue:
286
+ return PropertyValue.from_string(str(nbt))
287
+
288
+
289
+ class BooleanValue(PropertyValue):
290
+
291
+ @classmethod
292
+ def is_valid_value(cls, value: Any) -> bool:
293
+ return True
294
+
295
+ @classmethod
296
+ def python_type(cls) -> type:
297
+ """Return the native Python type a BooleanValue corresponds to."""
298
+ return bool
299
+
300
+
301
+ class IntegerValue(PropertyValue):
302
+
303
+ @classmethod
304
+ def is_valid_value(cls, value: Any) -> bool:
305
+ return value >= 0
306
+
307
+ @classmethod
308
+ def python_type(cls) -> type:
309
+ """Return the native Python type an IntegerValue corresponds to."""
310
+ return int
311
+
312
+
313
+ class EnumValue(PropertyValue):
314
+
315
+ def __str__(self) -> str:
316
+ return self._value
317
+
318
+ @classmethod
319
+ def is_valid_value(cls, value: Any) -> bool:
320
+ return ENUM_VALUE_PATTERN.fullmatch(value) is not None
321
+
322
+ @classmethod
323
+ def python_type(cls) -> type:
324
+ """Return the native Python type an EnumValue corresponds to."""
325
+ return str
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from nbtlib import Compound
5
+ from typing import Any, Iterator
6
+
7
+ from .resource_location import ResourceLocation
8
+ from .block_property import Properties
9
+
10
+
11
+ class BlockState:
12
+
13
+ __slots__ = ("_id", "_props")
14
+
15
+ def __init__(self, _id: str, **props: Any) -> None:
16
+ self._id: ResourceLocation = ResourceLocation.from_string(_id)
17
+ self._props: Properties = Properties(**props)
18
+
19
+ def __getitem__(self, name: str) -> Any:
20
+ try:
21
+ return self._props[name]
22
+ except KeyError as exc:
23
+ raise KeyError(
24
+ f"{type(self).__name__} '{self}' does not"
25
+ f" have {name!r} property") from exc
26
+
27
+ # def __getattr__(self, name: str) -> Any:
28
+ # return self[name]
29
+
30
+ def __contains__(self, name: str) -> bool:
31
+ return name in self._props
32
+
33
+ def __len__(self) -> int:
34
+ return len(self._props)
35
+
36
+ def __eq__(self, other: Any) -> bool:
37
+ if isinstance(other, str):
38
+ other = BlockState.from_string(other)
39
+ elif not isinstance(other, BlockState):
40
+ return NotImplemented
41
+ return (self.id, self._props) == (other.id, other._props)
42
+
43
+ def __lt__(self, other: Any) -> bool:
44
+ if not isinstance(other, BlockState):
45
+ return NotImplemented
46
+ return (self.id, self._props) < (other.id, other._props)
47
+
48
+ def __hash__(self) -> int:
49
+ return hash((self._id, self._props))
50
+
51
+ def __str__(self) -> str:
52
+ props_str = "" if not self._props else str(self._props)
53
+ return f"{self.id}{props_str}"
54
+
55
+ def __repr__(self) -> str:
56
+ return (
57
+ f"{type(self).__name__}("
58
+ f"id: {self._id!r}, props: {self._props!r})")
59
+
60
+ @property
61
+ def id(self) -> str:
62
+ return str(self._id)
63
+
64
+ def props(self) -> Iterator[tuple[str, Any]]:
65
+ return self._props.items()
66
+
67
+ def to_string(self) -> str:
68
+ return str(self)
69
+
70
+ @staticmethod
71
+ def from_string(string: str) -> BlockState:
72
+ idx = string.find("[") # basic parsing to separate block:id[name=value]
73
+ if idx == -1:
74
+ id, props = string, ""
75
+ else:
76
+ id, props = string[:idx], string[idx:]
77
+
78
+ state = BlockState(id)
79
+ state._props = Properties.from_string(props)
80
+ return state
81
+
82
+ def to_nbt(self) -> Compound:
83
+ nbt = Compound()
84
+ nbt["Name"] = self._id.to_nbt()
85
+ if self._props:
86
+ nbt["Properties"] = self._props.to_nbt()
87
+ return nbt
88
+
89
+ @staticmethod
90
+ def from_nbt(nbt: Compound) -> BlockState:
91
+ state = BlockState(str(nbt["Name"]))
92
+ state._props = Properties.from_nbt(nbt.get("Properties", Compound()))
93
+ return state
94
+
95
+ def with_id(self, id: str) -> BlockState:
96
+ state = BlockState(id)
97
+ state._props = deepcopy(self._props)
98
+ return state
99
+
100
+ def with_props(self, **props: Any) -> BlockState:
101
+ state = BlockState(self.id)
102
+ new_props = deepcopy(self._props)
103
+ for name, value in props.items():
104
+ if value is None:
105
+ del new_props[name]
106
+ else:
107
+ new_props[name] = value
108
+ state._props = new_props
109
+ return state
110
+
111
+ def without_props(self) -> BlockState:
112
+ return BlockState(self.id)
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import nbtlib
5
+ import numpy as np
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Vec3i:
10
+ _a: int
11
+ _b: int
12
+ _c: int
13
+
14
+ def __post_init__(self) -> None:
15
+ object.__setattr__(self, "_a", self._to_int(self._a))
16
+ object.__setattr__(self, "_b", self._to_int(self._b))
17
+ object.__setattr__(self, "_c", self._to_int(self._c))
18
+
19
+ def __str__(self) -> str:
20
+ return str(list(self))
21
+
22
+ def __add__(self, other) -> Vec3i:
23
+ arr = np.array(self)
24
+ other_arr = self._to_array(other)
25
+ try:
26
+ result = arr + other_arr
27
+ except Exception:
28
+ return NotImplemented
29
+ return type(self)(*result.astype(int))
30
+
31
+ def __radd__(self, other) -> Vec3i:
32
+ return self.__add__(other)
33
+
34
+ def __sub__(self, other) -> Vec3i:
35
+ arr = np.array(self)
36
+ other_arr = self._to_array(other)
37
+ try:
38
+ result = arr - other_arr
39
+ except Exception:
40
+ return NotImplemented
41
+ return type(self)(*result.astype(int))
42
+
43
+ def __rsub__(self, other) -> Vec3i:
44
+ arr = np.array(self)
45
+ try:
46
+ result = other - arr
47
+ except Exception:
48
+ return NotImplemented
49
+ return type(self)(*result.astype(int))
50
+
51
+ def __mul__(self, scalar: int) -> Vec3i:
52
+ return type(self)(
53
+ self._a * scalar, self._b * scalar, self._c * scalar)
54
+
55
+ def __floordiv__(self, scalar: int) -> Vec3i:
56
+ return type(self)(
57
+ self._a // scalar, self._b // scalar, self._c // scalar)
58
+
59
+ def __neg__(self) -> Vec3i:
60
+ return type(self)(-self._a, -self._b, -self._c)
61
+
62
+ def __getitem__(self, index: int) -> int:
63
+ return self.to_tuple()[index]
64
+
65
+ def __iter__(self):
66
+ return iter(self.to_tuple())
67
+
68
+ def __abs__(self) -> Vec3i:
69
+ return type(self)(*np.abs(self))
70
+
71
+ def __lt__(self, other):
72
+ return np.array(self) < self._to_array(other)
73
+
74
+ def __le__(self, other):
75
+ return np.array(self) <= self._to_array(other)
76
+
77
+ def __gt__(self, other):
78
+ return np.array(self) > self._to_array(other)
79
+
80
+ def __ge__(self, other):
81
+ return np.array(self) >= self._to_array(other)
82
+
83
+ def __eq__(self, other):
84
+ return np.array(self) == self._to_array(other)
85
+
86
+ def __ne__(self, other):
87
+ return np.array(self) != self._to_array(other)
88
+
89
+ def __array__(self, dtype: type | None = None, copy: bool = True):
90
+ arr = np.array([self._a, self._b, self._c], dtype=dtype)
91
+ if copy:
92
+ return arr.copy()
93
+ else:
94
+ return arr
95
+
96
+ def _to_array(self, other):
97
+ if isinstance(other, Vec3i):
98
+ return np.array(other)
99
+ else:
100
+ return other
101
+
102
+ @staticmethod
103
+ def _to_int(value) -> int:
104
+ if isinstance(value, (int, np.integer)):
105
+ return int(value)
106
+ elif isinstance(value, float):
107
+ if value.is_integer():
108
+ return int(value)
109
+ raise TypeError(
110
+ f"{type(value).__name__} value {value!r} is not"
111
+ " int, numpy integer, or whole float")
112
+
113
+ def to_tuple(self) -> tuple[int, int, int]:
114
+ return (self._a, self._b, self._c)
115
+
116
+ @classmethod
117
+ def from_tuple(cls, t: tuple[int, int, int]) -> Vec3i:
118
+ return cls(*t)
119
+
120
+ def to_nbt(self) -> nbtlib.Compound:
121
+ return nbtlib.Compound({
122
+ "x": nbtlib.Int(self._a),
123
+ "y": nbtlib.Int(self._b),
124
+ "z": nbtlib.Int(self._c),
125
+ })
126
+
127
+ @classmethod
128
+ def from_nbt(cls, nbt: nbtlib.Compound) -> Vec3i:
129
+ return cls(int(nbt["x"]), int(nbt["y"]), int(nbt["z"]))
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class BlockPosition(Vec3i):
134
+
135
+ @property
136
+ def x(self) -> int:
137
+ return self._a
138
+
139
+ @property
140
+ def y(self) -> int:
141
+ return self._b
142
+
143
+ @property
144
+ def z(self) -> int:
145
+ return self._c
146
+
147
+ def __repr__(self) -> str:
148
+ return f"{type(self).__name__}(x={self.x}, y={self.y}, z={self.z})"
149
+
150
+
151
+ @dataclass(frozen=True)
152
+ class Size3D(Vec3i):
153
+
154
+ @property
155
+ def width(self) -> int:
156
+ return self._a
157
+
158
+ @property
159
+ def height(self) -> int:
160
+ return self._b
161
+
162
+ @property
163
+ def length(self) -> int:
164
+ return self._c
165
+
166
+ def __repr__(self) -> str:
167
+ return (
168
+ f"{type(self).__name__}("
169
+ f"width={self.width}, height={self.height}, length={self.length})")