-
-
Notifications
You must be signed in to change notification settings - Fork 16
/
tag.py
1254 lines (953 loc) · 38.5 KB
/
tag.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
r"""
.. testsetup::
import io
import struct
from pprint import pprint
from nbtlib import *
All the tag classes have a :meth:`Base.parse` classmethod that reads
nbt data from a file-like object and returns a tag instance. Tag
instances can then write their binary representation back to file-like
objects using the :meth:`Base.write` method.
.. doctest::
>>> fileobj = io.BytesIO(b"\x03\x00\x03foo\x00\x00\x00{\x00")
>>> data = Compound.parse(fileobj)
>>> data
Compound({'foo': Int(123)})
>>> fileobj = io.BytesIO()
>>> data.write(fileobj)
>>> fileobj.getvalue()
b'\x03\x00\x03foo\x00\x00\x00{\x00'
Each tag inherits from a closely equivalent python builtin. For instance,
the :class:`Compound` class inherits from the builtin ``dict`` type.
This means that all the familiar operations available on the base type
work out of the box on the derived tag instances.
+-------------------+---------------------------------------------------------+
| Base type | Associated nbt tags |
+===================+=========================================================+
| ``int`` | :class:`Byte` :class:`Short` :class:`Int` :class:`Long` |
+-------------------+---------------------------------------------------------+
| ``float`` | :class:`Float` :class:`Double` |
+-------------------+---------------------------------------------------------+
| ``str`` | :class:`String` |
+-------------------+---------------------------------------------------------+
| ``numpy.ndarray`` | :class:`ByteArray` :class:`IntArray` :class:`LongArray` |
+-------------------+---------------------------------------------------------+
| ``list`` | :class:`List` |
+-------------------+---------------------------------------------------------+
| ``dict`` | :class:`Compound` |
+-------------------+---------------------------------------------------------+
Operator overloading works as expected with all tag types. Note that values are
returned unwrapped.
.. doctest::
>>> data = Compound({"foo": Int(123)})
>>> data["foo"] = Int(-1 * data["foo"])
>>> data["bar"] = String("hello")
>>> data
Compound({'foo': Int(-123), 'bar': String('hello')})
"""
__all__ = [
"Base",
"Numeric",
"NumericInteger",
"Byte",
"Short",
"Int",
"Long",
"Float",
"Double",
"String",
"List",
"Compound",
"End",
"Array",
"ByteArray",
"IntArray",
"LongArray",
"EndInstantiation",
"OutOfRange",
"IncompatibleItemType",
"CastError",
]
from struct import Struct
from struct import error as StructError
import numpy as np
from .literal.serializer import serialize_tag
# Struct formats used to pack and unpack numeric values
def get_format(fmt, string):
"""Return a dictionary containing a format for each byte order."""
return {"big": fmt(">" + string), "little": fmt("<" + string)}
BYTE = get_format(Struct, "b")
SHORT = get_format(Struct, "h")
USHORT = get_format(Struct, "H")
INT = get_format(Struct, "i")
LONG = get_format(Struct, "q")
FLOAT = get_format(Struct, "f")
DOUBLE = get_format(Struct, "d")
# Custom errors
class EndInstantiation(TypeError):
"""Raised when trying to instantiate an :class:`End` tag."""
def __init__(self):
super().__init__("End tags can't be instantiated")
class OutOfRange(ValueError):
"""Raised when a numeric value is out of range.
Converting builtin ``int`` instances to numeric nbt tags can fail if
the tag type isn't big enough.
.. doctest::
>>> Byte(127)
Byte(127)
>>> Byte(128)
Traceback (most recent call last):
...
nbtlib.tag.OutOfRange: Byte(128) is out of range
"""
def __init__(self, value):
super().__init__(f"{value!r} is out of range")
class IncompatibleItemType(TypeError):
"""Raised when a list item is incompatible with the subtype of the list.
Unlike builtin python lists, list tags are homogeneous so adding an
incompatible item to the list raises an error.
.. doctest::
>>> List([String("foo"), Int(123)])
Traceback (most recent call last):
...
nbtlib.tag.IncompatibleItemType: Int(123) should be a String tag
"""
def __init__(self, item, subtype):
super().__init__(f"{item!r} should be a {subtype.__name__} tag")
self.item = item
self.subtype = subtype
class CastError(ValueError):
"""Raised when an object couldn't be converted to the appropriate tag type.
Casting occurs when adding items to list tags and nbt schema
instances. If the item couldn't be converted to the required type,
the conversion raises an error.
.. doctest::
>>> integers = List[Int]()
>>> integers.append("foo")
Traceback (most recent call last):
...
nbtlib.tag.CastError: Couldn't cast 'foo' to Int
Note that casting only occurs when the value is an unwrapped python object.
Incompatible tags will raise an :class:`IncompatibleItemType` exception.
.. doctest::
>>> strings = List[String]()
>>> strings.append(123)
>>> strings
List[String]([String('123')])
>>> strings.append(Int(123))
Traceback (most recent call last):
...
nbtlib.tag.IncompatibleItemType: Int(123) should be a String tag
"""
def __init__(self, obj, tag_type):
super().__init__(f"Couldn't cast {obj!r} to {tag_type.__name__}")
self.obj = obj
self.tag_type = tag_type
# Read/write helpers for numeric and string values
def read_numeric(fmt, fileobj, byteorder="big"):
"""Read a numeric value from a file-like object."""
try:
fmt = fmt[byteorder]
return fmt.unpack(fileobj.read(fmt.size))[0]
except StructError:
return 0
except KeyError as exc:
raise ValueError("Invalid byte order") from exc
def write_numeric(fmt, value, fileobj, byteorder="big"):
"""Write a numeric value to a file-like object."""
try:
fileobj.write(fmt[byteorder].pack(value))
except KeyError as exc:
raise ValueError("Invalid byte order") from exc
def read_string(fileobj, byteorder="big"):
"""Read a string from a file-like object."""
length = read_numeric(USHORT, fileobj, byteorder)
return fileobj.read(length).decode("utf-8", "replace")
def write_string(value, fileobj, byteorder="big"):
"""Write a string to a file-like object."""
data = value.encode("utf-8")
write_numeric(USHORT, len(data), fileobj, byteorder)
fileobj.write(data)
# Path helpers
def find_tag(key, tags):
"""Return the first recursively matching tag."""
for tag in tags:
if isinstance(tag, (Compound, List)):
value = tag.get(key)
if value is None:
value = find_tag(
key, list(tag if isinstance(tag, List) else tag.values())
)
if value is not None:
return value
return None
# Tag definitions
class Base:
"""Base class inherited by all nbt tags.
This class defines the API shared by all nbt tags. Derived classes
that define a :attr:`tag_id` attribute are considered as concrete
tag implementations and are registered in the :attr:`all_tags`
registry. Concrete tag implementations inherit from both the
:class:`Base` class and their associated builtin data type.
Attributes:
all_tags: A dictionnary mapping tag ids to child classes.
.. doctest::
>>> pprint(Base.all_tags)
{0: <class 'nbtlib.tag.End'>,
1: <class 'nbtlib.tag.Byte'>,
2: <class 'nbtlib.tag.Short'>,
3: <class 'nbtlib.tag.Int'>,
4: <class 'nbtlib.tag.Long'>,
5: <class 'nbtlib.tag.Float'>,
6: <class 'nbtlib.tag.Double'>,
7: <class 'nbtlib.tag.ByteArray'>,
8: <class 'nbtlib.tag.String'>,
9: <class 'nbtlib.tag.List'>,
10: <class 'nbtlib.tag.Compound'>,
11: <class 'nbtlib.tag.IntArray'>,
12: <class 'nbtlib.tag.LongArray'>}
The mapping is used by the :meth:`get_tag` classmethod to
retrieve the tag type when parsing the binary format.
tag_id: The id of the tag in the binary format.
.. doctest::
>>> Int.tag_id
3
serializer: The name of the associated snbt serializer.
.. doctest ::
>>> Int.serializer
'numeric'
"""
__slots__ = ()
all_tags = {}
tag_id = None
serializer = None
def __init_subclass__(cls):
# Add class to the `all_tags` dictionnary if it has a tag id
if cls.tag_id is not None and cls.tag_id not in cls.all_tags:
cls.all_tags[cls.tag_id] = cls
@classmethod
def get_tag(cls, tag_id):
"""Return the tag class corresponding to the given tag id.
.. doctest ::
>>> Base.get_tag(3)
<class 'nbtlib.tag.Int'>
Arguments:
tag_id: The tag id must be valid otherwise the method raises a ``KeyError``.
"""
return cls.all_tags[tag_id]
@classmethod
def parse(cls, fileobj, byteorder="big"):
r"""Parse data from a file-like object and return a tag instance.
The default implementation does nothing. Concrete tags override
this method.
Arguments:
fileobj: A readable file-like object.
byteorder: Whether the nbt data is big-endian or little-endian.
.. doctest::
>>> Int.parse(io.BytesIO(b"\x00\x00\x00\x01"))
Int(1)
>>> Int.parse(io.BytesIO(b"\x01\x00\x00\x00"), byteorder="little")
Int(1)
"""
def write(self, fileobj, byteorder="big"):
r"""Write the binary representation of the tag to a file-like object.
The default implementation does nothing. Concrete tags override
this method.
Arguments:
fileobj: A writable file-like object.
byteorder: Whether the nbt data should be big-endian or little-endian.
.. doctest::
>>> big_endian = io.BytesIO()
>>> little_endian = io.BytesIO()
>>> Int(1).write(big_endian)
>>> Int(1).write(little_endian, byteorder="little")
>>> big_endian.getvalue()
b'\x00\x00\x00\x01'
>>> little_endian.getvalue()
b'\x01\x00\x00\x00'
"""
def match(self, other):
"""Check whether the tag recursively matches a subset of values.
The default implementation checks that the :attr:`tag_id` of the argument
matches and that the two instances are equal. Concrete tags override
this method.
.. doctest::
>>> data = Compound({
... 'foo': Int(42),
... 'hello': String('world')
... })
>>> data.match({'foo': 42})
True
"""
if hasattr(other, "tag_id") and self.tag_id != other.tag_id:
return False
return self == other
def snbt(self, indent=None, compact=False, quote=None):
"""Return the snbt literal corresponding to the tag instance.
.. doctest::
>>> Compound({"foo": Long(123)}).snbt()
'{foo: 123L}'
>>> Compound({"foo": Long(123)}).snbt(compact=True)
'{foo:123L}'
>>> print(Compound({"foo": Long(123)}).snbt(indent=4))
{
foo: 123L
}
"""
return serialize_tag(self, indent=indent, compact=compact, quote=quote)
def unpack(self, json=False):
"""Return the unpacked nbt value as an instance of the associated base type.
.. doctest::
>>> Compound({"foo": Long(123)}).unpack()
{'foo': 123}
Arguments:
json: Whether the returned value should be json-serializable.
This argument will convert array tags into plain python lists
instead of numpy arrays.
.. doctest::
>>> Compound({"foo": ByteArray([1, 2, 3])}).unpack()
{'foo': array([1, 2, 3], dtype=int8)}
>>> Compound({"foo": ByteArray([1, 2, 3])}).unpack(json=True)
{'foo': [1, 2, 3]}
"""
return None
def __repr__(self):
if self.tag_id is not None:
return f"{self.__class__.__name__}({super().__repr__()})"
return super().__repr__()
class End(Base):
"""Nbt tag used to mark the end of compound tags.
:class:`End` tags are the markers that terminate compound tags in
the binary format. They need to exist as a type but can't be used on
their own so manual instantiation raises an :class:`EndInstantiation`
exception.
.. doctest::
>>> End()
Traceback (most recent call last):
...
nbtlib.tag.EndInstantiation: End tags can't be instantiated
"""
__slots__ = ()
tag_id = 0
def __new__(cls, *args, **kwargs):
raise EndInstantiation()
class Numeric(Base):
r"""Intermediate class that represents a numeric nbt tag.
This class inherits from the :class:`Base` class and implements
:meth:`parse` and :meth:`write` for all the numeric nbt tags using
the :attr:`fmt` attribute.
Derived tags will use the ``numeric`` serializer and can specify a
literal suffix with the :attr:`suffix` attribute.
Attributes:
fmt: The struct format used to pack and unpack the tag value.
.. doctest::
>>> Int.fmt['big'].pack(1)
b'\x00\x00\x00\x01'
>>> Int.fmt['little'].pack(1)
b'\x01\x00\x00\x00'
suffix: The suffix used by the ``numeric`` snbt serializer.
.. doctest::
>>> Long.suffix
'L'
>>> Long(123).snbt()
'123L'
"""
__slots__ = ()
serializer = "numeric"
fmt = None
suffix = ""
@classmethod
def parse(cls, fileobj, byteorder="big"):
"""Override :meth:`Base.parse` for numeric tags."""
return cls(read_numeric(cls.fmt, fileobj, byteorder))
def write(self, fileobj, byteorder="big"):
"""Override :meth:`Base.write` for numeric tags."""
write_numeric(self.fmt, self, fileobj, byteorder)
class NumericInteger(Numeric, int):
"""Intermediate class that represents a numeric integer nbt tag.
This class adds range checks to the :class:`Numeric` class. It also
inherits from ``int`` and raises an :class:`OutOfRange` exception
when the tag is instantiated with a value that can't be represented
by the associated struct format.
.. doctest::
>>> Byte(127)
Byte(127)
>>> Byte(128)
Traceback (most recent call last):
...
nbtlib.tag.OutOfRange: Byte(128) is out of range
Concrete tag implementations deriving from this class also inherit
utilities for interpreting the value of the tag as an unsigned
integer.
.. doctest::
>>> value = Byte.from_unsigned(255)
>>> value
Byte(-1)
>>> value.as_unsigned
255
Attributes:
range: The supported range of values.
.. doctest::
>>> Byte.range
range(-128, 128)
>>> Int.range
range(-2147483648, 2147483648)
mask: The bit mask derived from the struct format.
.. doctest::
>>> f'{Byte.mask:b}'
'11111111'
bits: The bit length derived from the struct format.
.. doctest::
>>> Int.bits
32
>>> Long.bits
64
"""
__slots__ = ()
range = None
mask = None
bits = None
def __init_subclass__(cls):
super().__init_subclass__()
limit = 2 ** (8 * cls.fmt["big"].size - 1)
cls.range = range(-limit, limit)
cls.mask = limit * 2 - 1
cls.bits = cls.mask.bit_length()
def __new__(cls, *args, **kwargs):
self = super().__new__(cls, *args, **kwargs)
if int(self) not in cls.range:
raise OutOfRange(self)
return self
def unpack(self, json=False):
"""Override :meth:`Base.unpack` for numeric integer tags."""
return int(self)
@property
def as_unsigned(self):
"""Interpret the value of the tag as an unsigned integer."""
return self & self.mask
@classmethod
def from_unsigned(cls, value):
"""Encode an unsigned integer as an integer tag."""
return cls(value - (value * 2 & cls.mask + 1))
class Byte(NumericInteger):
"""Nbt tag representing a signed byte."""
__slots__ = ()
tag_id = 1
fmt = BYTE
suffix = "b"
class Short(NumericInteger):
"""Nbt tag representing a signed 16 bit integer."""
__slots__ = ()
tag_id = 2
fmt = SHORT
suffix = "s"
class Int(NumericInteger):
"""Nbt tag representing a signed 32 bit integer."""
__slots__ = ()
tag_id = 3
fmt = INT
class Long(NumericInteger):
"""Nbt tag representing a signed 64 bit integer."""
__slots__ = ()
tag_id = 4
fmt = LONG
suffix = "L"
class Float(Numeric, float):
"""Nbt tag representing a single-precision floating point number."""
__slots__ = ()
tag_id = 5
fmt = FLOAT
suffix = "f"
def unpack(self, json=False):
"""Override :meth:`Base.unpack` for float tags."""
return float(self)
class Double(Numeric, float):
"""Nbt tag representing a double-precision floating point number."""
__slots__ = ()
tag_id = 6
fmt = DOUBLE
suffix = "d"
def unpack(self, json=False):
"""Override :meth:`Base.unpack` for double tags."""
return float(self)
class Array(Base, np.ndarray):
"""Intermediate class that represents an array nbt tag.
Array tags are represented by numpy arrays. This class combines the
:class:`Base` class with the numpy ``ndarray`` type and implements
:meth:`parse` and :meth:`write` depending on a few additional
attributes.
Derived tags will use the ``array`` serializer and can specify an array
prefix with the :attr:`array_prefix` attribute.
Attributes:
item_type: The numpy array data type.
.. doctest::
>>> IntArray.item_type['big']
dtype('>i4')
>>> IntArray.item_type['little']
dtype('int32')
array_prefix: The literal array prefix.
.. doctest::
>>> IntArray.array_prefix
'I'
>>> IntArray([1, 2, 3]).snbt()
'[I; 1, 2, 3]'
wrapper: The tag used to wrap the integer.
.. doctest::
>>> IntArray.wrapper
<class 'nbtlib.tag.Int'>
>>> IntArray([1, 2, 3])[0]
Int(1)
"""
__slots__ = ()
serializer = "array"
item_type = None
array_prefix = None
wrapper = None
def __new__(cls, value=None, *, length=0, byteorder="big"):
item_type = cls.item_type[byteorder]
if value is None:
return np.zeros((length,), item_type).view(cls)
return np.asarray(value, item_type).view(cls)
@classmethod
def parse(cls, fileobj, byteorder="big"):
"""Override :meth:`Base.parse` for array tags."""
item_type = cls.item_type[byteorder]
data = fileobj.read(read_numeric(INT, fileobj, byteorder) * item_type.itemsize)
return cls(np.frombuffer(data, item_type), byteorder=byteorder)
def write(self, fileobj, byteorder="big"):
"""Override :meth:`Base.write` for array tags."""
write_numeric(INT, len(self), fileobj, byteorder)
array = self if self.item_type[byteorder] is self.dtype else self.byteswap()
fileobj.write(array.tobytes())
def unpack(self, json=False):
"""Override :meth:`Base.unpack` for array tags."""
return self.tolist() if json else np.copy(self)
def __getitem__(self, index):
if isinstance(index, slice):
return super().__getitem__(index)
return int.__new__(self.wrapper, super().__getitem__(index))
def __bool__(self):
return all(self)
def __repr__(self):
return f'{self.__class__.__name__}([{", ".join(map(str, self))}])'
class ByteArray(Array):
"""Nbt tag representing an array of signed bytes."""
__slots__ = ()
tag_id = 7
item_type = get_format(np.dtype, "b")
array_prefix = "B"
wrapper = Byte
class String(Base, str):
"""Nbt tag representing a string."""
__slots__ = ()
tag_id = 8
serializer = "string"
@classmethod
def parse(cls, fileobj, byteorder="big"):
"""Override :meth:`Base.parse` for string tags."""
return cls(read_string(fileobj, byteorder))
def write(self, fileobj, byteorder="big"):
"""Override :meth:`Base.write` for string tags."""
write_string(self, fileobj, byteorder)
def unpack(self, json=False):
"""Override :meth:`Base.unpack` for string tags."""
return str(self)
class List(Base, list):
"""Nbt tag representing a list of other nbt tags.
Nbt lists are homogeneous and can only hold a single type of tag. This
constraint is enforced by requiring the :class:`List` class to be
subclassed and define an appropriate :attr:`subtype` attribute. The
``class_getitem`` operator is defined so that
``List[TagName]`` returns a subclass with the subtype ``TagName``.
.. doctest::
>>> List[Int]
<class 'nbtlib.tag.List[Int]'>
>>> List[Int].subtype
<class 'nbtlib.tag.Int'>
The base class constructor returns an instance of the appropriate
subtype if it can infer the subtype from the elements of the given
iterable. Check out :meth:`infer_list_subtype` for details.
.. doctest::
>>> List([Int(123)])
List[Int]([Int(123)])
The class inherits from the :class:`Base` class and the ``list``
builtin. The inherited mutating operations are overridden to include
an ``isinstance`` check. For example, the :meth:`append` method
will raise an :class:`IncompatibleItemType` exception if the list
subtype doesn't match the item type.
.. doctest::
>>> strings = List[String]()
>>> strings.append(Int(123))
Traceback (most recent call last):
...
nbtlib.tag.IncompatibleItemType: Int(123) should be a String tag
To make things a bit more ergonomic, arbitrary python objects are
transparently converted to the list subtype.
.. doctest::
>>> strings.append(String("foo"))
>>> strings.append("bar")
>>> strings
List[String]([String('foo'), String('bar')])
However, note that impossible conversions raise a :class:`CastError`.
.. doctest::
>>> List[Int](["foo"])
Traceback (most recent call last):
...
nbtlib.tag.CastError: Couldn't cast 'foo' to Int
Finally, list tags support path indexing. Check out the
:ref:`path documentation <NBT Paths>` for more details.
.. doctest::
>>> compounds = List([
... Compound({"foo": Int(123)}),
... Compound({"foo": Int(456)}),
... ])
>>> compounds[Path("[{foo: 456}]")]
Compound({'foo': Int(456)})
"""
__slots__ = ()
tag_id = 9
serializer = "list"
variants = {}
subtype = End
def __new__(cls, iterable=()):
if cls.subtype is End:
iterable = tuple(iterable)
subtype = cls.infer_list_subtype(iterable)
cls = cls[subtype]
return super().__new__(cls, iterable)
def __init__(self, iterable=()):
super().__init__(map(self.cast_item, iterable))
def __class_getitem__(cls, item):
if item is End:
return List
try:
return cls.variants[item]
except KeyError:
variant = type(
f"List[{item.__name__}]", (List,), {"__slots__": (), "subtype": item}
)
cls.variants[item] = variant
return variant
@staticmethod
def infer_list_subtype(items):
"""Infer a list subtype from a list of items.
.. doctest::
>>> List.infer_list_subtype([Int(123)])
<class 'nbtlib.tag.Int'>
This method is used by the base :class:`List` constructor to figure
out the subtype of the :class:`List` subclass that should be returned.
Arguments:
items:
Can be any kind of iterable containing at least one tag instance
and zero or more python objects convertible to the type of the
tag instance.
.. doctest::
>>> List.infer_list_subtype([123, Int(456)])
<class 'nbtlib.tag.Int'>
"""
subtype = End
for item in items:
item_type = type(item)
if not issubclass(item_type, Base):
continue
if subtype is End:
subtype = item_type
if not issubclass(subtype, List):
return subtype
elif subtype is not item_type:
stype, itype = subtype, item_type
generic = List
while issubclass(stype, List) and issubclass(itype, List):
stype, itype = stype.subtype, itype.subtype
generic = List[generic]
if stype is End:
subtype = item_type
elif itype is not End:
return generic.subtype
return subtype
@classmethod
def parse(cls, fileobj, byteorder="big"):
"""Override :meth:`Base.parse` for list tags."""
tag = cls.get_tag(read_numeric(BYTE, fileobj, byteorder))
length = read_numeric(INT, fileobj, byteorder)
return cls[tag](tag.parse(fileobj, byteorder) for _ in range(length))
def write(self, fileobj, byteorder="big"):
"""Override :meth:`Base.write` for list tags."""
write_numeric(BYTE, self.subtype.tag_id, fileobj, byteorder)
write_numeric(INT, len(self), fileobj, byteorder)
for elem in self:
elem.write(fileobj, byteorder)
def match(self, other):
"""Override :meth:`Base.match` for list tags.
The method returns ``True`` if all the elements the iterable
appear at least once in the current instance.
.. doctest::
>>> List[Int]([1, 2, 3]).match([3, 1])
True
"""
if not isinstance(other, list):
return False
if not other:
return not self
return all(any(item.match(other_item) for item in self) for other_item in other)
def unpack(self, json=False):
"""Override :meth:`Base.unpack` for list tags."""
return [item.unpack(json) for item in self]
def find(self, key, default=None):
"""Return the first recursively matching tag.
.. doctest::
>>> tag = parse_nbt("[{data: {value: 42}}, {data: {value: 7}}]")
>>> tag.find(Path("data.value"))
Int(42)
>>> tag.find("value")
Int(42)
Arguments:
index: Can be a string, an integer, a slice or an instance of :class:`nbtlib.path.Path`.
default: Returned when the element could not be found.
"""
value = find_tag(key, [self])
return default if value is None else value
def get(self, index, default=None):
"""Return the element at the specified index.
Arguments:
index: Can be an integer, a slice or an instance of :class:`nbtlib.path.Path`.
default: Returned when the element could not be found.
"""
return (self.get_all(index) or [default])[0]
def get_all(self, index):
"""Return all the elements matching the specified index.
Arguments:
index: Can be an integer, a slice or an instance of :class:`nbtlib.path.Path`.
"""
try:
return (
[super().__getitem__(index)]
if isinstance(index, (int, slice))
else index.get(self)
)
except (IndexError, AttributeError):
return []
def __getitem__(self, index):
if isinstance(index, (int, slice)):
return super().__getitem__(index)
values = index.get(self)
if not values:
raise IndexError(index)
return values[0]
def __setitem__(self, index, value):
if isinstance(index, (int, slice)):
super().__setitem__(
index,
[self.cast_item(item) for item in value]
if isinstance(index, slice)
else self.cast_item(value),
)
else:
index.set(self, value)
def __delitem__(self, index):