pylitematic 0.0.0__tar.gz → 0.0.2__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.2
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.2"
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.2"
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)