├── .bumpversion.cfg ├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENCE ├── README.md ├── pynbt.py ├── setup.py └── tests ├── test_bigtest.py ├── test_creation.py └── test_unicode.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: TkTech 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Create release 8 | 9 | jobs: 10 | build: 11 | name: Creating Release 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.8 22 | 23 | - name: Install python dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | python -m pip install --upgrade setuptools 27 | pip install wheel twine 28 | 29 | - name: Publishing 30 | env: 31 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 32 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 33 | run: | 34 | python setup.py bdist_wheel 35 | python setup.py sdist 36 | twine upload dist/* 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python-version: [3.5, 3.6, 3.7, 3.8, '3.9.0-alpha - 3.9.0', pypy3] 13 | 14 | steps: 15 | # Python needs to be setup before checkout to prevent files from being 16 | # left in the source tree. See setup-python/issues/106. 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - uses: actions/checkout@v2 23 | 24 | - name: Installing python dependencies 25 | run: | 26 | pip install -e '.[test]' 27 | 28 | - name: Running tests 29 | run: | 30 | pytest 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.pyc 4 | build/* 5 | ENV/ 6 | __test__.nbt 7 | *.egg-info 8 | .ropeproject 9 | .DS_Store 10 | venv 11 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 Tyler Kennedy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Run tests](https://github.com/TkTech/PyNBT/workflows/Run%20tests/badge.svg?event=push) 2 | 3 | # PyNBT 4 | 5 | PyNBT is a tiny, liberally licenced (MIT) NBT library. 6 | It supports reading and writing big endian or little endian NBT files. 7 | Tested and supported on Py3.5-Py3.9, pypy3. 8 | 9 | ## Using the Library 10 | Using the library in your own programs is simple and is capable of reading, modifying, and saving NBT files. 11 | 12 | ### Writing 13 | 14 | **NOTE**: Beginning with version 1.1.0, names are optional for TAG_*'s that are added to a TAG_Compound, as they will be given the same name as their key. If you do 15 | specify a name, it will be used instead. This breaks compatibility with old code, as the position of the `name` and `value` parameter have now swapped. 16 | 17 | ```python 18 | from pynbt import NBTFile, TAG_Long, TAG_List, TAG_String 19 | 20 | value = { 21 | 'long_test': TAG_Long(104005), 22 | 'list_test': TAG_List(TAG_String, [ 23 | 'Timmy', 24 | 'Billy', 25 | 'Sally' 26 | ]) 27 | } 28 | 29 | nbt = NBTFile(value=value) 30 | with open('out.nbt', 'wb') as io: 31 | nbt.save(io) 32 | ``` 33 | 34 | ### Reading 35 | 36 | Reading is simple, and will accept any file-like object providing `read()`. 37 | Simply pretty-printing the file created from the example under writing: 38 | 39 | ```python 40 | from pynbt import NBTFile 41 | 42 | with open('out.nbt', 'rb') as io: 43 | nbt = NBTFile(io) 44 | print(nbt.pretty()) 45 | ``` 46 | 47 | This produces the output: 48 | 49 | ``` 50 | TAG_Compound(''): 2 entries 51 | { 52 | TAG_Long('long_test'): 104005 53 | TAG_List('list_test'): 3 entries 54 | { 55 | TAG_String(None): 'Timmy' 56 | TAG_String(None): 'Billy' 57 | TAG_String(None): 'Sally' 58 | } 59 | } 60 | ``` 61 | 62 | Every tag exposes a minimum of two fields, `.name` and `.value`. Every tag's value maps to a plain Python type, such as a `dict()` for `TAG_Compound` and a `list()` for `TAG_List`. Every tag 63 | also provides complete `__repr__` methods for printing. This makes traversal very simple and familiar to existing Python developers. 64 | 65 | ```python 66 | with open('out.nbt', 'rb') as io: 67 | nbt = NBTFile(io) 68 | # Iterate over every TAG in the root compound as you would any other dict 69 | for name, tag in nbt.items(): 70 | print(name, tag) 71 | 72 | # Print every tag in a list 73 | for tag in nbt['list_test']: 74 | print(tag) 75 | ``` 76 | 77 | ## Changelog 78 | 79 | These changelogs are summaries only and not comprehensive. See 80 | the commit history between tags for full changes. 81 | 82 | ### v3.0.0 83 | - TAG_Byte_Array now returns and accepts `bytearray()`, rather than a list 84 | of bytes (#18). 85 | 86 | ### v2.0.0 87 | - Py2 is no longer supported. 88 | 89 | ### v1.4.0 90 | - **Removed** pocket detection helpers and ``RegionFile``, leaving PyNBT to 91 | **only** handle NBT. 92 | - Added a simple unicode test. 93 | 94 | ### v1.3.0 95 | 96 | - Internal cleanups in ``nbt.py`` to ease some C work. 97 | - ``NBTFile.__init__()`` and ``NBTFile.save()``'s arguments have changed. 98 | For most cases changing ``compressed=True`` to ``NBTFIle.Compression.GZIP`` 99 | will suffice. 100 | - ``NBTFile.__init__()`` and ``NBTFile.save()`` no longer accept paths, 101 | instead accepting only file-like objects implementing ``read()`` and 102 | ``write()``, respectively. 103 | - ``name`` must now be provided at construction or before saving of an 104 | ``NBTFile()`` (defaults to ``None`` instead of ''). 105 | 106 | ### v1.2. 107 | 108 | - TAG_List's values no longer need to be ``TAG_*`` objects. They 109 | will be converted when the tag is saved. This allows much easier lists of 110 | native types. 111 | 112 | ### v1.2.0 113 | 114 | - Internal code cleanup. Breaks compatibility with pocket loading 115 | and saving (to be reimplemented as helpers). 116 | - Slight speed improvements. 117 | - TAG_List can now be treated as a plain python list (`.value` points to `self`) 118 | 119 | ### v1.1.0 120 | 121 | - Breaks compatibility with older code, but allows much more 122 | convenient creation of `TAG_Compound`. `name` and `value` have in most cases 123 | swapped spots. 124 | - `name` is now the last argument of every `TAG_*`, and 125 | optional for children of a `TAG_Compound`. Instead, they'll be given the key 126 | they're assigned to as a name. 127 | - `TAG_Compound`s can now be treated like 128 | dictionaries for convienience. `.value` simply maps to itself. 129 | 130 | ### v1.0.1 131 | 132 | - Small bugfixes. 133 | - Adds support for `TAG_Int_Array`. 134 | 135 | ### v1.0.0 136 | 137 | - First release. 138 | -------------------------------------------------------------------------------- /pynbt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements reading & writing for the Named Binary Tag (NBT) format used 3 | in Minecraft. 4 | """ 5 | __all__ = ( 6 | 'NBTFile', 7 | 'TAG_Byte', 8 | 'TAG_Short', 9 | 'TAG_Int', 10 | 'TAG_Long', 11 | 'TAG_Float', 12 | 'TAG_Double', 13 | 'TAG_Byte_Array', 14 | 'TAG_String', 15 | 'TAG_List', 16 | 'TAG_Compound', 17 | 'TAG_Int_Array', 18 | 'TAG_Long_Array' 19 | ) 20 | from functools import partial 21 | from struct import unpack, pack 22 | 23 | import mutf8 24 | 25 | 26 | class BaseTag(object): 27 | def __init__(self, value, name=None): 28 | self.name = name 29 | self.value = value 30 | 31 | @staticmethod 32 | def _read_utf8(read): 33 | """Reads a length-prefixed MUTF-8 string.""" 34 | name_length = read('h', 2)[0] 35 | return mutf8.decode_modified_utf8(read.src.read(name_length)) 36 | 37 | @staticmethod 38 | def _write_utf8(write, value): 39 | """Writes a length-prefixed MUTF-8 string.""" 40 | encoded_value = mutf8.encode_modified_utf8(value) 41 | write('h', len(encoded_value)) 42 | write.dst.write(encoded_value) 43 | 44 | @classmethod 45 | def read(cls, read, has_name=True): 46 | """ 47 | Read the tag in using the reader `rd`. 48 | If `has_name` is `False`, skip reading the tag name. 49 | """ 50 | name = cls._read_utf8(read) if has_name else None 51 | 52 | if cls is TAG_Compound: 53 | # A TAG_Compound is almost identical to Python's native dict() 54 | # object, or a Java HashMap. 55 | final = {} 56 | while True: 57 | # Find the type of each tag in a compound in turn. 58 | tag = read('b', 1)[0] 59 | if tag == 0: 60 | # A tag of 0 means we've reached TAG_End, used to terminate 61 | # a TAG_Compound. 62 | break 63 | # We read in each tag in turn, using its name as the key in 64 | # the dict (Since a compound cannot have repeating names, 65 | # this works fine). 66 | tmp = _tags[tag].read(read) 67 | final[tmp.name] = tmp 68 | return cls(final, name=name) 69 | elif cls is TAG_List: 70 | # A TAG_List is a very simple homogeneous array, similar to 71 | # Python's native list() object, but restricted to a single type. 72 | tag_type, length = read('bi', 5) 73 | tag_read = _tags[tag_type].read 74 | return cls( 75 | _tags[tag_type], 76 | [tag_read(read, has_name=False) for x in range(0, length)], 77 | name=name 78 | ) 79 | elif cls is TAG_String: 80 | # A simple length-prefixed UTF-8 string. 81 | value = cls._read_utf8(read) 82 | return cls(value, name=name) 83 | elif cls is TAG_Byte_Array: 84 | # A simple array of (signed) bytes. 85 | length = read('i', 4)[0] 86 | return cls(bytearray(read.src.read(length)), name=name) 87 | elif cls is TAG_Int_Array: 88 | # A simple array of (signed) 4-byte integers. 89 | length = read('i', 4)[0] 90 | return cls(read('{0}i'.format(length), length * 4), name=name) 91 | elif cls is TAG_Long_Array: 92 | # A simple array of (signed) 8-byte longs. 93 | length = read('i', 4)[0] 94 | return cls(read('{0}q'.format(length), length * 8), name=name) 95 | elif cls is TAG_Byte: 96 | # A single (signed) byte. 97 | return cls(read('b', 1)[0], name=name) 98 | elif cls is TAG_Short: 99 | # A single (signed) short. 100 | return cls(read('h', 2)[0], name=name) 101 | elif cls is TAG_Int: 102 | # A signed (signed) 4-byte int. 103 | return cls(read('i', 4)[0], name=name) 104 | elif cls is TAG_Long: 105 | # A single (signed) 8-byte long. 106 | return cls(read('q', 8)[0], name=name) 107 | elif cls is TAG_Float: 108 | # A single single-precision floating point value. 109 | return cls(read('f', 4)[0], name=name) 110 | elif cls is TAG_Double: 111 | # A single double-precision floating point value. 112 | return cls(read('d', 8)[0], name=name) 113 | elif cls is TAG_End: 114 | # A End of Compound Tag 115 | return cls(read('2b', 2)[0], name=name) 116 | 117 | def write(self, write): 118 | # Only write the name TAG_String if our name is not `None`. 119 | # If you want a blank name, use ''. 120 | if self.name is not None: 121 | if isinstance(self, NBTFile): 122 | write('b', 0x0A) 123 | else: 124 | write('b', _tags.index(self.__class__)) 125 | self._write_utf8(write, self.name) 126 | if isinstance(self, TAG_List): 127 | write('bi', _tags.index(self.type_), len(self.value)) 128 | for item in self.value: 129 | # If our list item isn't of type self._type, convert 130 | # it before writing. 131 | if not isinstance(item, self.type_): 132 | item = self.type_(item) 133 | item.write(write) 134 | elif isinstance(self, TAG_Compound): 135 | for v in self.value.values(): 136 | v.write(write) 137 | # A tag of type 0 (TAg_End) terminates a TAG_Compound. 138 | write('b', 0) 139 | elif isinstance(self, TAG_String): 140 | self._write_utf8(write, self.value) 141 | elif isinstance(self, TAG_Int_Array): 142 | length = len(self.value) 143 | write('i{0}i'.format(length), length, *self.value) 144 | elif isinstance(self, TAG_Long_Array): 145 | length = len(self.value) 146 | write('i{0}q'.format(length), length, *self.value) 147 | elif isinstance(self, TAG_Byte_Array): 148 | write('i', len(self.value)) 149 | write.dst.write(bytes(self.value)) 150 | elif isinstance(self, TAG_Byte): 151 | write('b', self.value) 152 | elif isinstance(self, TAG_Short): 153 | write('h', self.value) 154 | elif isinstance(self, TAG_Int): 155 | write('i', self.value) 156 | elif isinstance(self, TAG_Long): 157 | write('q', self.value) 158 | elif isinstance(self, TAG_Float): 159 | write('f', self.value) 160 | elif isinstance(self, TAG_Double): 161 | write('d', self.value) 162 | 163 | def pretty(self, indent=0, indent_str=' '): 164 | """ 165 | Pretty-print a tag in the same general style as Markus's example 166 | output. 167 | """ 168 | return '{0}{1}({2!r}): {3!r}'.format( 169 | indent_str * indent, 170 | self.__class__.__name__, 171 | self.name, 172 | self.value 173 | ) 174 | 175 | def __repr__(self): 176 | return '{0}({1!r}, {2!r})'.format( 177 | self.__class__.__name__, self.value, self.name) 178 | 179 | def __str__(self): 180 | return repr(self) 181 | 182 | 183 | class TAG_Byte(BaseTag): 184 | __slots__ = ('name', 'value') 185 | 186 | 187 | class TAG_Short(BaseTag): 188 | __slots__ = ('name', 'value') 189 | 190 | 191 | class TAG_Int(BaseTag): 192 | __slots__ = ('name', 'value') 193 | 194 | 195 | class TAG_Long(BaseTag): 196 | __slots__ = ('name', 'value') 197 | 198 | 199 | class TAG_Float(BaseTag): 200 | __slots__ = ('name', 'value') 201 | 202 | 203 | class TAG_Double(BaseTag): 204 | __slots__ = ('name', 'value') 205 | 206 | 207 | class TAG_Byte_Array(BaseTag): 208 | def pretty(self, indent=0, indent_str=' '): 209 | return '{0}TAG_Byte_Array({1!r}): [{2} bytes]'.format( 210 | indent_str * indent, self.name, len(self.value)) 211 | 212 | 213 | class TAG_String(BaseTag): 214 | __slots__ = ('name', 'value') 215 | 216 | 217 | class TAG_End(BaseTag): 218 | __slots__ = ('name', 'value') 219 | 220 | 221 | class TAG_List(BaseTag, list): 222 | def __init__(self, tag_type, value=None, name=None): 223 | """ 224 | Creates a new homogeneous list of `tag_type` items, copying `value` 225 | if provided. 226 | """ 227 | self.name = name 228 | self.value = self 229 | self.type_ = tag_type 230 | if value is not None: 231 | self.extend(value) 232 | 233 | def pretty(self, indent=0, indent_str=' '): 234 | t = [] 235 | t.append('{0}TAG_List({1!r}): {2} entries'.format( 236 | indent_str * indent, self.name, len(self.value))) 237 | t.append('{0}{{'.format(indent_str * indent)) 238 | for v in self.value: 239 | t.append(v.pretty(indent + 1, indent_str)) 240 | t.append('{0}}}'.format(indent_str * indent)) 241 | return '\n'.join(t) 242 | 243 | def __repr__(self): 244 | return '{0}({1!r} entries, {2!r})'.format( 245 | self.__class__.__name__, len(self), self.name) 246 | 247 | 248 | class TAG_Compound(BaseTag, dict): 249 | def __init__(self, value=None, name=None): 250 | self.name = name 251 | self.value = self 252 | if value is not None: 253 | self.update(value) 254 | 255 | def pretty(self, indent=0, indent_str=' '): 256 | t = [] 257 | t.append('{0}TAG_Compound({1!r}): {2} entries'.format( 258 | indent_str * indent, self.name, len(self.value))) 259 | t.append('{0}{{'.format(indent_str * indent)) 260 | for v in self.values(): 261 | t.append(v.pretty(indent + 1, indent_str)) 262 | t.append('{0}}}'.format(indent_str * indent)) 263 | return '\n'.join(t) 264 | 265 | def __repr__(self): 266 | return '{0}({1!r} entries, {2!r})'.format( 267 | self.__class__.__name__, len(self), self.name) 268 | 269 | def __setitem__(self, key, value): 270 | """ 271 | Sets the TAG_*'s name if it isn't already set to that of the key 272 | it's being assigned to. This results in cleaner code, as the name 273 | does not need to be specified twice. 274 | """ 275 | if value.name is None: 276 | value.name = key 277 | super(TAG_Compound, self).__setitem__(key, value) 278 | 279 | def update(self, *args, **kwargs): 280 | """See `__setitem__`.""" 281 | super(TAG_Compound, self).update(*args, **kwargs) 282 | for key, item in self.items(): 283 | if item.name is None: 284 | item.name = key 285 | 286 | 287 | class TAG_Int_Array(BaseTag): 288 | def pretty(self, indent=0, indent_str=' '): 289 | return '{0}TAG_Int_Array({1!r}): [{2} integers]'.format( 290 | indent_str * indent, self.name, len(self.value)) 291 | 292 | 293 | class TAG_Long_Array(BaseTag): 294 | def pretty(self, indent=0, indent_str=' '): 295 | return '{0}TAG_Long_Array({1!r}): [{2} longs]'.format( 296 | indent_str * indent, self.name, len(self.value)) 297 | 298 | 299 | # The TAG_* types have the convienient property of being continuous. 300 | # The code is written in such a way that if this were to no longer be 301 | # true in the future, _tags can simply be replaced with a dict(). 302 | _tags = ( 303 | TAG_End, # 0x00 304 | TAG_Byte, # 0x01 305 | TAG_Short, # 0x02 306 | TAG_Int, # 0x03 307 | TAG_Long, # 0x04 308 | TAG_Float, # 0x05 309 | TAG_Double, # 0x06 310 | TAG_Byte_Array, # 0x07 311 | TAG_String, # 0x08 312 | TAG_List, # 0x09 313 | TAG_Compound, # 0x0A 314 | TAG_Int_Array, # 0x0B 315 | TAG_Long_Array # 0x0C 316 | ) 317 | 318 | 319 | def _read_little(src, fmt, size): 320 | return unpack('<' + fmt, src.read(size)) 321 | 322 | 323 | def _read_big(src, fmt, size): 324 | return unpack('>' + fmt, src.read(size)) 325 | 326 | 327 | def _write_little(dst, fmt, *args): 328 | dst.write(pack('<' + fmt, *args)) 329 | 330 | 331 | def _write_big(dst, fmt, *args): 332 | dst.write(pack('>' + fmt, *args)) 333 | 334 | 335 | class NBTFile(TAG_Compound): 336 | def __init__(self, io=None, name='', value=None, little_endian=False): 337 | """ 338 | Creates a new NBTFile or loads one from any file-like object providing 339 | `read()`. 340 | 341 | Construction a new NBTFile() is as simple as: 342 | >>> nbt = NBTFile(name='') 343 | 344 | Whereas loading an existing one is most often done: 345 | >>> with open('my_file.nbt', 'rb') as io: 346 | ... nbt = NBTFile(io=io) 347 | 348 | :param io: A file-like object to read an existing NBT file from. 349 | :param name: Name of the root tag (usually blank) [default: ''] 350 | :param value: Value of the root rag. [default: `{}`] 351 | :param little_endian: `True` if the file is in little-endian byte 352 | order. [default: `False`] 353 | """ 354 | # No file or path given, so we're creating a new NBTFile. 355 | if io is None: 356 | super().__init__(value if value else {}, name) 357 | return 358 | 359 | # The pocket edition uses little-endian NBT files, but annoyingly 360 | # without any kind of header we can't determine that ourselves, 361 | # not even a magic number we could flip. 362 | read = _read_little if little_endian else _read_big 363 | read = partial(read, io) 364 | read.src = io 365 | 366 | # All valid NBT files will begin with 0x0A, which is a TAG_Compound. 367 | if read('b', 1)[0] != 0x0A: 368 | raise IOError('NBTFile does not begin with 0x0A.') 369 | 370 | tmp = TAG_Compound.read(read) 371 | super(NBTFile, self).__init__(tmp, tmp.name) 372 | 373 | def save(self, io, little_endian=False): 374 | """ 375 | Saves the `NBTFile()` to `io`, which can be any file-like object 376 | providing `write()`. 377 | 378 | :param io: A file-like object to write the resulting NBT file to. 379 | :param little_endian: `True` if little-endian byte order should be 380 | used. [default: `False`] 381 | """ 382 | write = _write_little if little_endian else _write_big 383 | write = partial(write, io) 384 | write.dst = io 385 | 386 | self.write(write) 387 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from setuptools import setup 3 | 4 | root = os.path.abspath(os.path.dirname(__file__)) 5 | with open(os.path.join(root, 'README.md'), 'rb') as readme: 6 | long_description = readme.read().decode('utf-8') 7 | 8 | setup( 9 | name='PyNBT', 10 | version='3.1.0', 11 | description='Tiny, liberally-licensed NBT library (Minecraft).', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | author='Tyler Kennedy', 15 | author_email='tk@tkte.ch', 16 | url='https://github.com/TkTech/PyNBT', 17 | keywords=['minecraft', 'nbt'], 18 | py_modules=['pynbt'], 19 | install_requires=[ 20 | 'mutf8>=1.0.2' 21 | ], 22 | extras_require={ 23 | 'test': ['pytest'] 24 | }, 25 | classifiers=[ 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Intended Audience :: Developers', 28 | 'Operating System :: OS Independent', 29 | 'License :: OSI Approved :: MIT License' 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /tests/test_bigtest.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import array 3 | from io import BytesIO 4 | 5 | from pynbt import NBTFile 6 | 7 | # bigtest.nbt 8 | BIG_TEST = array.array('B', [ 9 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xed, 0x54, 10 | 0xcf, 0x4f, 0x1a, 0x41, 0x14, 0x7e, 0xc2, 0x02, 0xcb, 0x96, 0x82, 0xb1, 11 | 0xc4, 0x10, 0x63, 0xcc, 0xab, 0xb5, 0x84, 0xa5, 0xdb, 0xcd, 0x42, 0x11, 12 | 0x89, 0xb1, 0x88, 0x16, 0x2c, 0x9a, 0x0d, 0x1a, 0xd8, 0xa8, 0x31, 0x86, 13 | 0xb8, 0x2b, 0xc3, 0x82, 0x2e, 0xbb, 0x66, 0x77, 0xb0, 0xf1, 0xd4, 0x4b, 14 | 0x7b, 0x6c, 0x7a, 0xeb, 0x3f, 0xd3, 0x23, 0x7f, 0x43, 0xcf, 0xbd, 0xf6, 15 | 0xbf, 0xa0, 0xc3, 0x2f, 0x7b, 0x69, 0xcf, 0xbd, 0xf0, 0x32, 0xc9, 0xf7, 16 | 0xe6, 0xbd, 0x6f, 0xe6, 0x7b, 0x6f, 0x26, 0x79, 0x02, 0x04, 0x54, 0x72, 17 | 0x4f, 0x2c, 0x0e, 0x78, 0xcb, 0xb1, 0x4d, 0x8d, 0x78, 0xf4, 0xe3, 0x70, 18 | 0x62, 0x3e, 0x08, 0x7b, 0x1d, 0xc7, 0xa5, 0x93, 0x18, 0x0f, 0x82, 0x47, 19 | 0xdd, 0xee, 0x84, 0x02, 0x62, 0xb5, 0xa2, 0xaa, 0xc7, 0x78, 0x76, 0x5c, 20 | 0x57, 0xcb, 0xa8, 0x55, 0x0f, 0x1b, 0xc8, 0xd6, 0x1e, 0x6a, 0x95, 0x86, 21 | 0x86, 0x0d, 0xad, 0x7e, 0x58, 0x7b, 0x8f, 0x83, 0xcf, 0x83, 0x4f, 0x83, 22 | 0x6f, 0xcf, 0x03, 0x10, 0x6e, 0x5b, 0x8e, 0x3e, 0xbe, 0xa5, 0x38, 0x4c, 23 | 0x64, 0xfd, 0x10, 0xea, 0xda, 0x74, 0xa6, 0x23, 0x40, 0xdc, 0x66, 0x2e, 24 | 0x69, 0xe1, 0xb5, 0xd3, 0xbb, 0x73, 0xfa, 0x76, 0x0b, 0x29, 0xdb, 0x0b, 25 | 0xe0, 0xef, 0xe8, 0x3d, 0x1e, 0x38, 0x5b, 0xef, 0x11, 0x08, 0x56, 0xf5, 26 | 0xde, 0x5d, 0xdf, 0x0b, 0x40, 0xe0, 0x5e, 0xb7, 0xfa, 0x64, 0xb7, 0x04, 27 | 0x00, 0x8c, 0x41, 0x4c, 0x73, 0xc6, 0x08, 0x55, 0x4c, 0xd3, 0x20, 0x2e, 28 | 0x7d, 0xa4, 0xc0, 0xc8, 0xc2, 0x10, 0xb3, 0xba, 0xde, 0x58, 0x0b, 0x53, 29 | 0xa3, 0xee, 0x44, 0x8e, 0x45, 0x03, 0x30, 0xb1, 0x27, 0x53, 0x8c, 0x4c, 30 | 0xf1, 0xe9, 0x14, 0xa3, 0x53, 0x8c, 0x85, 0xe1, 0xd9, 0x9f, 0xe3, 0xb3, 31 | 0xf2, 0x44, 0x81, 0xa5, 0x7c, 0x33, 0xdd, 0xd8, 0xbb, 0xc7, 0xaa, 0x75, 32 | 0x13, 0x5f, 0x28, 0x1c, 0x08, 0xd7, 0x2e, 0xd1, 0x59, 0x3f, 0xaf, 0x1d, 33 | 0x1b, 0x60, 0x21, 0x59, 0xdf, 0xfa, 0xf1, 0x05, 0xfe, 0xc1, 0xce, 0xfc, 34 | 0x9d, 0xbd, 0x00, 0xbc, 0xf1, 0x40, 0xc9, 0xf8, 0x85, 0x42, 0x40, 0x46, 35 | 0xfe, 0x9e, 0xeb, 0xea, 0x0f, 0x93, 0x3a, 0x68, 0x87, 0x60, 0xbb, 0xeb, 36 | 0x32, 0x37, 0xa3, 0x28, 0x0a, 0x8e, 0xbb, 0xf5, 0xd0, 0x69, 0x63, 0xca, 37 | 0x4e, 0xdb, 0xe9, 0xec, 0xe6, 0xe6, 0x2b, 0x3b, 0xbd, 0x25, 0xbe, 0x64, 38 | 0x49, 0x09, 0x3d, 0xaa, 0xbb, 0x94, 0xfd, 0x18, 0x7e, 0xe8, 0xd2, 0x0e, 39 | 0xda, 0x6f, 0x15, 0x4c, 0xb1, 0x68, 0x3e, 0x2b, 0xe1, 0x9b, 0x9c, 0x84, 40 | 0x99, 0xbc, 0x84, 0x05, 0x09, 0x65, 0x59, 0x16, 0x45, 0x00, 0xff, 0x2f, 41 | 0x28, 0xae, 0x2f, 0xf2, 0xc2, 0xb2, 0xa4, 0x2e, 0x1d, 0x20, 0x77, 0x5a, 42 | 0x3b, 0xb9, 0x8c, 0xca, 0xe7, 0x29, 0xdf, 0x51, 0x41, 0xc9, 0x16, 0xb5, 43 | 0xc5, 0x6d, 0xa1, 0x2a, 0xad, 0x2c, 0xc5, 0x31, 0x7f, 0xba, 0x7a, 0x92, 44 | 0x8e, 0x5e, 0x9d, 0x5f, 0xf8, 0x12, 0x05, 0x23, 0x1b, 0xd1, 0xf6, 0xb7, 45 | 0x77, 0xaa, 0xcd, 0x95, 0x72, 0xbc, 0x9e, 0xdf, 0x58, 0x5d, 0x4b, 0x97, 46 | 0xae, 0x92, 0x17, 0xb9, 0x44, 0xd0, 0x80, 0xc8, 0xfa, 0x3e, 0xbf, 0xb3, 47 | 0xdc, 0x54, 0xcb, 0x07, 0x75, 0x6e, 0xa3, 0xb6, 0x76, 0x59, 0x92, 0x93, 48 | 0xa9, 0xdc, 0x51, 0x50, 0x99, 0x6b, 0xcc, 0x35, 0xe6, 0x1a, 0xff, 0x57, 49 | 0x23, 0x08, 0x42, 0xcb, 0xe9, 0x1b, 0xd6, 0x78, 0xc2, 0xec, 0xfe, 0xfc, 50 | 0x7a, 0xfb, 0x7d, 0x78, 0xd3, 0x84, 0xdf, 0xd4, 0xf2, 0xa4, 0xfb, 0x08, 51 | 0x06, 0x00, 0x00 52 | ]).tobytes() 53 | 54 | 55 | def test_parse(): 56 | """ 57 | Test to ensure PyNBT can parse the defacto-test file, "bigtest.nbt". 58 | """ 59 | with gzip.GzipFile(fileobj=BytesIO(BIG_TEST)) as io: 60 | nbt = NBTFile(io) 61 | # Ensure every base tag was parsed. 62 | assert len(nbt) == 11 63 | 64 | # Test 3 tag types, and deep compounds. 65 | tag = nbt['listTest (compound)'].value[0]['created-on'] 66 | assert tag.value == 1264099775885 67 | 68 | tag = nbt[ 69 | 'byteArrayTest (the first 1000 values of (n*n*255+n*7)%100,' 70 | ' starting with n=0 (0, 62, 34, 16, 8, ...))' 71 | ] 72 | for i in range(0, 1000): 73 | assert tag.value[i] == (i * i * 255 + i * 7) % 100 74 | -------------------------------------------------------------------------------- /tests/test_creation.py: -------------------------------------------------------------------------------- 1 | from pynbt import ( 2 | NBTFile, 3 | TAG_Byte, 4 | TAG_Short, 5 | TAG_Int, 6 | TAG_Float, 7 | TAG_Double, 8 | TAG_String, 9 | TAG_Int_Array, 10 | TAG_Byte_Array, 11 | TAG_Long_Array, 12 | TAG_List, 13 | TAG_Compound 14 | ) 15 | 16 | 17 | def test_save(): 18 | n = NBTFile(name='') 19 | n['byte'] = TAG_Byte(0) 20 | n['short'] = TAG_Short(1) 21 | n['int'] = TAG_Int(2) 22 | n['float'] = TAG_Float(3.) 23 | n['double'] = TAG_Double(4.) 24 | n['string'] = TAG_String('Testing') 25 | n['int_array'] = TAG_Int_Array([45, 5, 6]) 26 | n['byte_array'] = TAG_Byte_Array([4, 3, 2]) 27 | n['long_array'] = TAG_Long_Array([5, 6, 7]) 28 | n['list'] = TAG_List(TAG_Int, [ 29 | TAG_Int(4) 30 | ]) 31 | n['autolist_int'] = TAG_List(TAG_Int, [ 32 | 5, 33 | 6, 34 | 7, 35 | 30240, 36 | -340 37 | ]) 38 | n['autolist_compound'] = TAG_List(TAG_Compound, [ 39 | { 40 | 'name': TAG_String('ABC'), 41 | 'health': TAG_Double(3.5) 42 | } 43 | ]) 44 | 45 | with open('__test__.nbt', 'wb') as io: 46 | n.save(io) 47 | -------------------------------------------------------------------------------- /tests/test_unicode.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | from pynbt import NBTFile, TAG_String 4 | 5 | 6 | def test_mutf8(): 7 | """Strings in NBT are MUTF-8, not UTF-8.""" 8 | nbt = NBTFile(name='') 9 | 10 | nbt['mutf_8'] = TAG_String('\u2764') 11 | 12 | with BytesIO() as out: 13 | nbt.save(out) 14 | assert out.getvalue() == ( 15 | # Root Compound 16 | b'\x0A' 17 | # Length of root name (empty) 18 | b'\x00\x00' 19 | # TAG_String 20 | b'\x08' 21 | # String's name and its length 22 | b'\x00\x06mutf_8' 23 | # String's length. 24 | b'\x00\x03' 25 | # String's value. 26 | b'\xe2\x9d\xa4' 27 | # TAG_End 28 | b'\x00' 29 | ) 30 | --------------------------------------------------------------------------------