From e4ab4f1aedeccd41fad39bbaee0415be0858d2c4 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Mon, 1 Jul 2024 21:11:59 -0400 Subject: [PATCH 01/23] Add type exports to __init__ --- pytiled_parser/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index b52e8fbb..cf76ba1c 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -19,3 +19,29 @@ from .tiled_map import TiledMap from .tileset import Frame, Grid, Tile, Tileset, Transformations from .world import World, WorldMap + +__all__ = [ + "Color", + "OrderedPair", + "Size" + "UnknownFormat", + "Chunk", + "ImageLayer", + "Layer", + "LayerGroup", + "ObjectLayer", + "TileLayer", + "parse_map", + "parse_world", + "parse_tileset", + "Properties", + "Property", + "TiledMap", + "Frame", + "Grid", + "Tile", + "Tileset", + "Transformations", + "World", + "WorldMap" +] \ No newline at end of file From 2968b91b72d4c09bc3c65632d8bc7bfeab2b07bd Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Mon, 1 Jul 2024 21:13:14 -0400 Subject: [PATCH 02/23] Update version and changelog for 2.2.5 --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61db67dd..fe105402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +## [2.2.5] - 2024-07-01 + +Adds a `__all__` section to the main library `__init__.py` file which fixes problems when running pyright in strict mode against this library, it would not be able to see the exported types. + ## [2.2.4] - 2024-07-01 Small change to the default text color, in Tiled the text color defaults to blac(0, 0, 0), previously in pytiled-parser if the color was not specified it would default to white(255, 255, 255). This has been changed to match Tiled's behavior [#70](https://github.com/pythonarcade/pytiled_parser/pull/70) diff --git a/pyproject.toml b/pyproject.toml index 8a748020..3d1d61b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytiled_parser" -version = "2.2.4" +version = "2.2.5" description = "A library for parsing Tiled Map Editor maps and tilesets" readme = "README.md" authors = [ From d2b8b91b06e85f3fe3fbae4a9d1ae0711993fc68 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Mon, 1 Jul 2024 21:14:48 -0400 Subject: [PATCH 03/23] Small fix to __init__ --- pytiled_parser/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index cf76ba1c..ad097f39 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -23,7 +23,7 @@ __all__ = [ "Color", "OrderedPair", - "Size" + "Size", "UnknownFormat", "Chunk", "ImageLayer", @@ -43,5 +43,5 @@ "Tileset", "Transformations", "World", - "WorldMap" -] \ No newline at end of file + "WorldMap", +] From db22f6a06004d1525e0d1be39d73543b17abe36b Mon Sep 17 00:00:00 2001 From: Maic Siemering Date: Fri, 23 Aug 2024 04:14:56 +0200 Subject: [PATCH 04/23] Only first level layer are reported in TileMap.layers like it is done for tmj. --- pytiled_parser/parsers/tmx/tiled_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index 988e5cfa..3a582857 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -60,7 +60,7 @@ def parse(file: Path) -> TiledMap: ) layers = [] - for element in raw_map.iter(): + for element in raw_map.findall("./*"): if element.tag in ["layer", "objectgroup", "imagelayer", "group"]: layers.append(parse_layer(element, parent_dir)) From e0d88eea6dcb27b05ff307b210b69249b2a66015 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 22 Aug 2024 23:33:31 -0400 Subject: [PATCH 05/23] Fixes for object template properties --- pytiled_parser/parsers/json/tiled_object.py | 17 ++++++++++++++++- pytiled_parser/parsers/tmx/tiled_object.py | 19 ++++++++++++++++++- .../test_data/map_tests/template/expected.py | 2 +- .../template/template-rectangle.json | 14 +++++++++++++- .../map_tests/template/template-rectangle.tx | 7 ++++++- 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py index d5f3f08a..260ef90f 100644 --- a/pytiled_parser/parsers/json/tiled_object.py +++ b/pytiled_parser/parsers/json/tiled_object.py @@ -320,7 +320,22 @@ def parse( loaded_template = template["object"] for key in loaded_template: if key != "id": - raw_object[key] = loaded_template[key] # type: ignore + if key == "properties": + if "properties" not in raw_object: + raw_object["properties"] = [] + + for prop in loaded_template["properties"]: + + found = False + for prop2 in raw_object["properties"]: + if prop2["name"] == prop["name"]: + found = True + break + + if not found: + raw_object["properties"].append(prop) + else: + raw_object[key] = loaded_template[key] # type: ignore else: raise NotImplementedError( "Loading TMX object templates inside a JSON map is currently not supported, " diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py index 711664e1..820f37ad 100644 --- a/pytiled_parser/parsers/tmx/tiled_object.py +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -277,8 +277,25 @@ def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> Tiled continue new_object.attrib[key] = val + properties_element = raw_object.find("./properties") - if properties_element is not None: + temp_properties_element = new_object.find("./properties") + if properties_element is not None and temp_properties_element is None: + new_object.append(properties_element) + elif properties_element is None and temp_properties_element is not None: + pass + elif properties_element is not None and temp_properties_element is not None: + for prop in temp_properties_element: + + found = False + for prop2 in properties_element: + if prop.attrib["name"] == prop2.attrib["name"]: + found = True + break + + if not found: + properties_element.append(prop) + new_object.remove(temp_properties_element) new_object.append(properties_element) raw_object = new_object diff --git a/tests/test_data/map_tests/template/expected.py b/tests/test_data/map_tests/template/expected.py index a2266e8e..7470be29 100644 --- a/tests/test_data/map_tests/template/expected.py +++ b/tests/test_data/map_tests/template/expected.py @@ -21,7 +21,7 @@ coordinates=common_types.OrderedPair( 98.4987608686521, 46.2385012811358 ), - properties={"test": "hello"}, + properties={"test": "hello", "testtest": "fromtemplate"}, visible=True, class_="", ), diff --git a/tests/test_data/map_tests/template/template-rectangle.json b/tests/test_data/map_tests/template/template-rectangle.json index 3bed3fa1..c18b1f8c 100644 --- a/tests/test_data/map_tests/template/template-rectangle.json +++ b/tests/test_data/map_tests/template/template-rectangle.json @@ -6,7 +6,19 @@ "rotation":0, "class":"", "visible":true, - "width":63.6585878103079 + "width":63.6585878103079, + "properties":[ + { + "name":"test", + "type":"string", + "value":"world" + }, + { + "name":"testtest", + "type":"string", + "value":"fromtemplate" + } + ] }, "type":"template" } \ No newline at end of file diff --git a/tests/test_data/map_tests/template/template-rectangle.tx b/tests/test_data/map_tests/template/template-rectangle.tx index 6daa3644..1959f0f2 100644 --- a/tests/test_data/map_tests/template/template-rectangle.tx +++ b/tests/test_data/map_tests/template/template-rectangle.tx @@ -1,4 +1,9 @@ From 407c3c43624a58d0530777e4d3513a380fba66bb Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 22 Aug 2024 23:35:25 -0400 Subject: [PATCH 06/23] Cleanup deprecation warnings for etree.Element comparisons in TMX parser --- pytiled_parser/parsers/tmx/tiled_map.py | 2 +- pytiled_parser/parsers/tmx/tiled_object.py | 2 +- pytiled_parser/parsers/tmx/wang_set.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index 3a582857..a7af55a6 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -133,7 +133,7 @@ def parse(file: Path) -> TiledMap: map_.hex_side_length = int(raw_map.attrib["hexsidelength"]) properties_element = raw_map.find("./properties") - if properties_element: + if properties_element is not None: map_.properties = parse_properties(properties_element) if raw_map.attrib.get("staggeraxis") is not None: diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py index 820f37ad..498685ee 100644 --- a/pytiled_parser/parsers/tmx/tiled_object.py +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -55,7 +55,7 @@ def _parse_common(raw_object: etree.Element) -> TiledObject: common.class_ = raw_object.attrib["class"] properties_element = raw_object.find("./properties") - if properties_element: + if properties_element is not None: common.properties = parse_properties(properties_element) return common diff --git a/pytiled_parser/parsers/tmx/wang_set.py b/pytiled_parser/parsers/tmx/wang_set.py index 0789cb10..34013117 100644 --- a/pytiled_parser/parsers/tmx/wang_set.py +++ b/pytiled_parser/parsers/tmx/wang_set.py @@ -38,7 +38,7 @@ def _parse_wang_color(raw_wang_color: etree.Element) -> WangColor: wang_color.class_ = raw_wang_color.attrib["class"] properties = raw_wang_color.find("./properties") - if properties: + if properties is not None: wang_color.properties = parse_properties(properties) return wang_color @@ -74,7 +74,7 @@ def parse(raw_wangset: etree.Element) -> WangSet: wangset.class_ = raw_wangset.attrib["class"] properties = raw_wangset.find("./properties") - if properties: + if properties is not None: wangset.properties = parse_properties(properties) return wangset From adc918c0a40b3ed92618bb1f333f55662a61459c Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 22 Aug 2024 23:38:47 -0400 Subject: [PATCH 07/23] Update version and changelog for 2.2.6 --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe105402..299c0ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +## [2.2.6] - 2024-08-22 + +Fixes a bug where properties did not load as expected on objects when using object templates. As of this release, the functionality is such that if properties are defined on both an object, and it's template, they will both end up on the resulting object, with the ones defined directly on the object overriding any properties that have the same name from the template. It does not compare types, so a String property with the name `test` would override a number property with the name `test`, as an example. Comparing types could be done in the future, but is likely more complicated than it's worth doing right now. + +Fixes a bug where the TMX parser would report all layers as top level layers, ignoring the layer group nesting. This bug was not present in the JSON parser. (#74) + +Also handles some small deprecation warnings related to true/false comparisons of etree.Element classes in the TMX parser. + ## [2.2.5] - 2024-07-01 Adds a `__all__` section to the main library `__init__.py` file which fixes problems when running pyright in strict mode against this library, it would not be able to see the exported types. diff --git a/pyproject.toml b/pyproject.toml index 3d1d61b7..f520bb6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytiled_parser" -version = "2.2.5" +version = "2.2.6" description = "A library for parsing Tiled Map Editor maps and tilesets" readme = "README.md" authors = [ From 2e2f9709d04f46143d2aea3bc8e7d3e985dcf68b Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 22 Aug 2024 23:39:37 -0400 Subject: [PATCH 08/23] Black formatting --- pytiled_parser/parsers/json/tiled_object.py | 4 ++-- pytiled_parser/parsers/tmx/tiled_object.py | 24 +++++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py index 260ef90f..527cf15e 100644 --- a/pytiled_parser/parsers/json/tiled_object.py +++ b/pytiled_parser/parsers/json/tiled_object.py @@ -325,13 +325,13 @@ def parse( raw_object["properties"] = [] for prop in loaded_template["properties"]: - + found = False for prop2 in raw_object["properties"]: if prop2["name"] == prop["name"]: found = True break - + if not found: raw_object["properties"].append(prop) else: diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py index 498685ee..a2e16d53 100644 --- a/pytiled_parser/parsers/tmx/tiled_object.py +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -277,24 +277,26 @@ def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> Tiled continue new_object.attrib[key] = val - properties_element = raw_object.find("./properties") temp_properties_element = new_object.find("./properties") if properties_element is not None and temp_properties_element is None: new_object.append(properties_element) elif properties_element is None and temp_properties_element is not None: pass - elif properties_element is not None and temp_properties_element is not None: + elif ( + properties_element is not None + and temp_properties_element is not None + ): for prop in temp_properties_element: - - found = False - for prop2 in properties_element: - if prop.attrib["name"] == prop2.attrib["name"]: - found = True - break - - if not found: - properties_element.append(prop) + + found = False + for prop2 in properties_element: + if prop.attrib["name"] == prop2.attrib["name"]: + found = True + break + + if not found: + properties_element.append(prop) new_object.remove(temp_properties_element) new_object.append(properties_element) From 31458e041e54a5785867c16fccd4136ddb328b0a Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 22 Aug 2024 23:44:13 -0400 Subject: [PATCH 09/23] Remove 3.7 from CI testing matrix --- .github/workflows/test.yml | 2 +- README.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d780ea28..0c6b8b24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] architecture: ['x64'] steps: diff --git a/README.md b/README.md index 3d559f07..e06358b0 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ The [Arcade](https://api.arcade.academy) library has [supporting code](https://api.arcade.academy/en/latest/api/tilemap.html) to integrate PyTiled Parser and [example code](https://api.arcade.academy/en/latest/examples/index.html#using-tiled-map-editor-to-create-maps) showing its use. +## Supported Python Versions + +pytiled-parser works on Python 3.6 - 3.12(and should continue to work on new versions). However we are not actively testing on Python 3.6 and 3.7 due to other development tooling not supporting them, however as long as it remains tenable to do so, we will support these versions and fix problems for them. + ## Installation Simply install with pip: From dd4cd1d65347aedd7f95ab4204a421c7ae7d980b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Simi=C4=87?= Date: Thu, 3 Oct 2024 19:16:51 +0200 Subject: [PATCH 10/23] Fix multiline string properties (#75) * Test for multiline object properties. * Fix parsing of multiline string properties. --- pytiled_parser/parsers/tmx/properties.py | 5 ++--- tests/test_tiled_object_tmx.py | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pytiled_parser/parsers/tmx/properties.py b/pytiled_parser/parsers/tmx/properties.py index 1b20e3d4..2c6f7dc4 100644 --- a/pytiled_parser/parsers/tmx/properties.py +++ b/pytiled_parser/parsers/tmx/properties.py @@ -12,11 +12,10 @@ def parse(raw_properties: etree.Element) -> Properties: for raw_property in raw_properties.findall("property"): type_ = raw_property.attrib.get("type") - if "value" not in raw_property.attrib: + value_ = raw_property.attrib.get("value", raw_property.text) + if value_ is None: continue - value_ = raw_property.attrib["value"] - if type_ == "file": value = Path(value_) elif type_ == "color": diff --git a/tests/test_tiled_object_tmx.py b/tests/test_tiled_object_tmx.py index 702ced32..9b6a3848 100644 --- a/tests/test_tiled_object_tmx.py +++ b/tests/test_tiled_object_tmx.py @@ -129,6 +129,9 @@ + Hi +I can write multiple lines in here +That's pretty great """, @@ -144,6 +147,7 @@ "float property": 42.1, "int property": 8675309, "string property": "pytiled_parser rulez!1!!", + "multiline string property": "Hi\nI can write multiple lines in here\nThat's pretty great", }, ), ), From c8e7bbd45ed544fd3cc6d0d5bd21b35f07705c6c Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 3 Oct 2024 13:21:37 -0400 Subject: [PATCH 11/23] Update version and changelog for 2.2.7 --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 299c0ad3..ff02109e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +## [2.2.7] - 2024-10-03 + +Fixes a bug when using the TMX format, where multi-line String properties would not be correctly parsed, as they are placed differently in the XML than single line strings. (#75) + +The Tiled docs also state that this multi-line format may in the future be used for all property values, so this change will help to futureproof against that. + ## [2.2.6] - 2024-08-22 Fixes a bug where properties did not load as expected on objects when using object templates. As of this release, the functionality is such that if properties are defined on both an object, and it's template, they will both end up on the resulting object, with the ones defined directly on the object overriding any properties that have the same name from the template. It does not compare types, so a String property with the name `test` would override a number property with the name `test`, as an example. Comparing types could be done in the future, but is likely more complicated than it's worth doing right now. diff --git a/pyproject.toml b/pyproject.toml index f520bb6e..6869122d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytiled_parser" -version = "2.2.6" +version = "2.2.7" description = "A library for parsing Tiled Map Editor maps and tilesets" readme = "README.md" authors = [ From 6d7ccd7ccd0ce2728c7436e1ac03ca34eb07274e Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 16 Jan 2025 15:38:49 -0500 Subject: [PATCH 12/23] Default to UTF-8 Encoding and Add Optional Encoding Argument (#81) --- CHANGELOG.md | 4 +++ pytiled_parser/parser.py | 27 +++++++++-------- pytiled_parser/parsers/json/layer.py | 12 ++++---- pytiled_parser/parsers/json/tiled_map.py | 18 +++++++---- pytiled_parser/parsers/json/tiled_object.py | 5 +++- pytiled_parser/parsers/json/tileset.py | 11 ++++--- pytiled_parser/parsers/tmx/layer.py | 13 ++++---- pytiled_parser/parsers/tmx/tiled_map.py | 15 ++++++---- pytiled_parser/parsers/tmx/tiled_object.py | 8 +++-- pytiled_parser/parsers/tmx/tileset.py | 9 ++++-- pytiled_parser/util.py | 22 +++++++------- pytiled_parser/world.py | 4 +-- tests/test_layer.py | 33 +++++++++++++-------- tests/test_tiled_object_json.py | 5 ++-- tests/test_tiled_object_tmx.py | 3 +- tests/test_world.py | 3 +- 16 files changed, 118 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff02109e..1e93fca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +## [2.2.8] - UNRELEASED + +Converts all file loading to use UTF-8 encoding by default. In most cases, all Tiled files will be exported from Tiled in UTF-8 encoding, however the python `open()` function uses the system default locale. The only case where Tiled would not have used UTF-8 is for JSON files when Tiled was compiled against Qt 5, which is only in some builds of Tiled from older systems. All XML files exported from Tiled will always be UTF-8. If someone happens to have a JSON file which was exported from Tiled on an encoding other than UTF-8, or for some other reason is in a different encoding. This can be switched using a new optional argument named `encoding` in the various public API `parse` functions such as `parse_map()`. This value is handed down through the pipeline of file loading in pytiled-parser, and will apply to every file loaded during the chain from this. This means that every file in a chain(for example, Map, Tileset, and Template File) must share the same encoding. This new argument is a string which is ultimately passed to the Python [open()](https://docs.python.org/3/library/functions.html#open) function. This change does introduce breaking changes in the underlying API which is not intended to be public facing, but if you are going deeper than the top level parse functions, you may need to adjust for this, as many of the underlying internal functions now have a mandatory encoding argument. + ## [2.2.7] - 2024-10-03 Fixes a bug when using the TMX format, where multi-line String properties would not be correctly parsed, as they are placed differently in the XML than single line strings. (#75) diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py index 22918ecc..c7cb97d5 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/parser.py @@ -14,23 +14,24 @@ from pytiled_parser.world import parse_world as _parse_world -def parse_map(file: Path) -> TiledMap: +def parse_map(file: Path, encoding: str = "utf-8") -> TiledMap: """Parse the raw Tiled map into a pytiled_parser type Args: file: Path to the map file + encoding: The character encoding set to use when opening the file Returns: TiledMap: A parsed and typed TiledMap """ - parser = check_format(file) + parser = check_format(file, encoding) # The type ignores are because mypy for some reason thinks those functions return Any if parser == "tmx": - return tmx_map_parse(file) # type: ignore + return tmx_map_parse(file, encoding) # type: ignore else: try: - return json_map_parse(file) # type: ignore + return json_map_parse(file, encoding) # type: ignore except ValueError: raise UnknownFormat( "Unknown Map Format, please use either the TMX or JSON format. " @@ -38,26 +39,27 @@ def parse_map(file: Path) -> TiledMap: ) -def parse_tileset(file: Path) -> Tileset: +def parse_tileset(file: Path, encoding: str = "utf-8") -> Tileset: """Parse the raw Tiled Tileset into a pytiled_parser type Args: file: Path to the map file + encoding: The character encoding set to use when opening the file Returns: Tileset: A parsed and typed Tileset """ - parser = check_format(file) + parser = check_format(file, encoding) if parser == "tmx": - with open(file) as map_file: + with open(file, encoding=encoding) as map_file: raw_tileset = etree.parse(map_file).getroot() - return tmx_tileset_parse(raw_tileset, 1) + return tmx_tileset_parse(raw_tileset, 1, encoding) else: try: - with open(file) as my_file: + with open(file, encoding=encoding) as my_file: raw_tileset = json.load(my_file) - return json_tileset_parse(raw_tileset, 1) + return json_tileset_parse(raw_tileset, 1, encoding) except ValueError: raise UnknownFormat( "Unknowm Tileset Format, please use either the TSX or JSON format. " @@ -65,13 +67,14 @@ def parse_tileset(file: Path) -> Tileset: ) -def parse_world(file: Path) -> World: +def parse_world(file: Path, encoding: str = "utf-8") -> World: """Parse the raw world file into a pytiled_parser type Args: file: Path to the world file + encoding: The character encoding set to use when opening the file Returns: World: A parsed and typed World """ - return _parse_world(file) + return _parse_world(file, encoding) diff --git a/pytiled_parser/parsers/json/layer.py b/pytiled_parser/parsers/json/layer.py index 9682fc4e..d4ae6e69 100644 --- a/pytiled_parser/parsers/json/layer.py +++ b/pytiled_parser/parsers/json/layer.py @@ -298,6 +298,7 @@ def _parse_tile_layer(raw_layer: RawLayer) -> TileLayer: def _parse_object_layer( raw_layer: RawLayer, + encoding: str, parent_dir: Optional[Path] = None, ) -> ObjectLayer: """Parse the raw_layer to an ObjectLayer. @@ -310,7 +311,7 @@ def _parse_object_layer( """ objects = [] for object_ in raw_layer["objects"]: - objects.append(parse_object(object_, parent_dir)) + objects.append(parse_object(object_, encoding, parent_dir)) return ObjectLayer( tiled_objects=objects, @@ -339,7 +340,7 @@ def _parse_image_layer(raw_layer: RawLayer) -> ImageLayer: def _parse_group_layer( - raw_layer: RawLayer, parent_dir: Optional[Path] = None + raw_layer: RawLayer, encoding, parent_dir: Optional[Path] = None ) -> LayerGroup: """Parse the raw_layer to a LayerGroup. @@ -352,13 +353,14 @@ def _parse_group_layer( layers = [] for layer in raw_layer["layers"]: - layers.append(parse(layer, parent_dir=parent_dir)) + layers.append(parse(layer, encoding, parent_dir=parent_dir)) return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__) def parse( raw_layer: RawLayer, + encoding: str, parent_dir: Optional[Path] = None, ) -> Layer: """Parse a raw Layer into a pytiled_parser object. @@ -378,9 +380,9 @@ def parse( type_ = raw_layer["type"] if type_ == "objectgroup": - return _parse_object_layer(raw_layer, parent_dir) + return _parse_object_layer(raw_layer, encoding, parent_dir) elif type_ == "group": - return _parse_group_layer(raw_layer, parent_dir) + return _parse_group_layer(raw_layer, encoding, parent_dir) elif type_ == "imagelayer": return _parse_image_layer(raw_layer) elif type_ == "tilelayer": diff --git a/pytiled_parser/parsers/json/tiled_map.py b/pytiled_parser/parsers/json/tiled_map.py index a5113f54..299b9193 100644 --- a/pytiled_parser/parsers/json/tiled_map.py +++ b/pytiled_parser/parsers/json/tiled_map.py @@ -55,7 +55,7 @@ """ -def parse(file: Path) -> TiledMap: +def parse(file: Path, encoding: str) -> TiledMap: """Parse the raw Tiled map into a pytiled_parser type. Args: @@ -64,7 +64,7 @@ def parse(file: Path) -> TiledMap: Returns: TiledMap: A parsed TiledMap. """ - with open(file) as map_file: + with open(file, encoding=encoding) as map_file: raw_tiled_map = json.load(map_file) parent_dir = file.parent @@ -76,13 +76,14 @@ def parse(file: Path) -> TiledMap: if raw_tileset.get("source") is not None: # Is an external Tileset tileset_path = Path(parent_dir / raw_tileset["source"]) - parser = check_format(tileset_path) - with open(tileset_path) as raw_tileset_file: + parser = check_format(tileset_path, encoding) + with open(tileset_path, encoding=encoding) as raw_tileset_file: if parser == "tmx": raw_tileset_external = etree.parse(raw_tileset_file).getroot() tilesets[raw_tileset["firstgid"]] = parse_tmx_tileset( raw_tileset_external, raw_tileset["firstgid"], + encoding, external_path=tileset_path.parent, ) else: @@ -90,6 +91,7 @@ def parse(file: Path) -> TiledMap: tilesets[raw_tileset["firstgid"]] = parse_json_tileset( json.load(raw_tileset_file), raw_tileset["firstgid"], + encoding, external_path=tileset_path.parent, ) except ValueError: @@ -101,7 +103,7 @@ def parse(file: Path) -> TiledMap: # Is an embedded Tileset raw_tileset = cast(RawTileSet, raw_tileset) tilesets[raw_tileset["firstgid"]] = parse_json_tileset( - raw_tileset, raw_tileset["firstgid"] + raw_tileset, raw_tileset["firstgid"], encoding ) if isinstance(raw_tiled_map["version"], float): # pragma: no cover @@ -113,7 +115,10 @@ def parse(file: Path) -> TiledMap: map_ = TiledMap( map_file=file, infinite=raw_tiled_map.get("infinite", False), - layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]], + layers=[ + parse_layer(layer_, encoding, parent_dir) + for layer_ in raw_tiled_map["layers"] + ], map_size=Size(raw_tiled_map["width"], raw_tiled_map["height"]), next_layer_id=raw_tiled_map.get("nextlayerid"), next_object_id=raw_tiled_map["nextobjectid"], @@ -157,6 +162,7 @@ def parse(file: Path) -> TiledMap: map_.tilesets[new_firstgid] = parse_json_tileset( tiled_object.new_tileset, new_firstgid, + encoding, tiled_object.new_tileset_path, ) tiled_object.gid = tiled_object.gid + (new_firstgid - 1) diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py index 527cf15e..e837bad9 100644 --- a/pytiled_parser/parsers/json/tiled_object.py +++ b/pytiled_parser/parsers/json/tiled_object.py @@ -291,6 +291,7 @@ def _get_parser(raw_object: RawObject) -> Callable[[RawObject], TiledObject]: def parse( raw_object: RawObject, + encoding: str, parent_dir: Optional[Path] = None, ) -> TiledObject: """Parse the raw object into a pytiled_parser version @@ -314,7 +315,9 @@ def parse( "A parent directory must be specified when using object templates." ) template_path = Path(parent_dir / raw_object["template"]) - template, new_tileset, new_tileset_path = load_object_template(template_path) + template, new_tileset, new_tileset_path = load_object_template( + template_path, encoding + ) if isinstance(template, dict): loaded_template = template["object"] diff --git a/pytiled_parser/parsers/json/tileset.py b/pytiled_parser/parsers/json/tileset.py index cbbc51de..7830c2b1 100644 --- a/pytiled_parser/parsers/json/tileset.py +++ b/pytiled_parser/parsers/json/tileset.py @@ -161,7 +161,9 @@ def _parse_grid(raw_grid: RawGrid) -> Grid: ) -def _parse_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile: +def _parse_tile( + raw_tile: RawTile, encoding: str, external_path: Optional[Path] = None +) -> Tile: """Parse the raw_tile to a Tile object. Args: @@ -180,7 +182,7 @@ def _parse_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile tile.animation.append(_parse_frame(frame)) if raw_tile.get("objectgroup") is not None: - tile.objects = parse_layer(raw_tile["objectgroup"]) + tile.objects = parse_layer(raw_tile["objectgroup"], encoding) if raw_tile.get("properties") is not None: tile.properties = parse_properties(raw_tile["properties"]) @@ -231,6 +233,7 @@ def _parse_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile def parse( raw_tileset: RawTileSet, firstgid: int, + encoding: str, external_path: Optional[Path] = None, ) -> Tileset: """Parse the raw tileset into a pytiled_parser type @@ -309,12 +312,12 @@ def parse( assert raw_tile.get("id") is None raw_tile["id"] = int(raw_tile_id) tiles[raw_tile["id"]] = _parse_tile( - raw_tile, external_path=external_path + raw_tile, encoding, external_path=external_path ) else: for raw_tile in raw_tileset["tiles"]: tiles[raw_tile["id"]] = _parse_tile( - raw_tile, external_path=external_path + raw_tile, encoding, external_path=external_path ) tileset.tiles = tiles diff --git a/pytiled_parser/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py index ffb8e41e..648d0fe2 100644 --- a/pytiled_parser/parsers/tmx/layer.py +++ b/pytiled_parser/parsers/tmx/layer.py @@ -262,7 +262,7 @@ def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer: def _parse_object_layer( - raw_layer: etree.Element, parent_dir: Optional[Path] = None + raw_layer: etree.Element, encoding: str, parent_dir: Optional[Path] = None ) -> ObjectLayer: """Parse the raw_layer to an ObjectLayer. @@ -274,7 +274,7 @@ def _parse_object_layer( """ objects = [] for object_ in raw_layer.findall("./object"): - objects.append(parse_object(object_, parent_dir)) + objects.append(parse_object(object_, encoding, parent_dir)) object_layer = ObjectLayer( tiled_objects=objects, @@ -316,7 +316,7 @@ def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: def _parse_group_layer( - raw_layer: etree.Element, parent_dir: Optional[Path] = None + raw_layer: etree.Element, encoding: str, parent_dir: Optional[Path] = None ) -> LayerGroup: """Parse the raw_layer to a LayerGroup. @@ -331,7 +331,7 @@ def _parse_group_layer( layers.append(_parse_tile_layer(layer)) for layer in raw_layer.findall("./objectgroup"): - layers.append(_parse_object_layer(layer, parent_dir)) + layers.append(_parse_object_layer(layer, encoding, parent_dir)) for layer in raw_layer.findall("./imagelayer"): layers.append(_parse_image_layer(layer)) @@ -350,6 +350,7 @@ def _parse_group_layer( def parse( raw_layer: etree.Element, + encoding: str, parent_dir: Optional[Path] = None, ) -> Layer: """Parse a raw Layer into a pytiled_parser object. @@ -369,9 +370,9 @@ def parse( type_ = raw_layer.tag if type_ == "objectgroup": - return _parse_object_layer(raw_layer, parent_dir) + return _parse_object_layer(raw_layer, encoding, parent_dir) elif type_ == "group": - return _parse_group_layer(raw_layer, parent_dir) + return _parse_group_layer(raw_layer, encoding, parent_dir) elif type_ == "imagelayer": return _parse_image_layer(raw_layer) elif type_ == "layer": diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index a7af55a6..16ac75ed 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -12,7 +12,7 @@ from pytiled_parser.util import check_format, parse_color -def parse(file: Path) -> TiledMap: +def parse(file: Path, encoding: str) -> TiledMap: """Parse the raw Tiled map into a pytiled_parser type. Args: @@ -21,7 +21,7 @@ def parse(file: Path) -> TiledMap: Returns: TiledMap: A parsed TiledMap. """ - with open(file) as map_file: + with open(file, encoding=encoding) as map_file: raw_map = etree.parse(map_file).getroot() parent_dir = file.parent @@ -33,19 +33,21 @@ def parse(file: Path) -> TiledMap: if raw_tileset.attrib.get("source") is not None: # Is an external Tileset tileset_path = Path(parent_dir / raw_tileset.attrib["source"]) - parser = check_format(tileset_path) - with open(tileset_path) as tileset_file: + parser = check_format(tileset_path, encoding) + with open(tileset_path, encoding=encoding) as tileset_file: if parser == "tmx": raw_tileset_external = etree.parse(tileset_file).getroot() tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset( raw_tileset_external, int(raw_tileset.attrib["firstgid"]), + encoding, external_path=tileset_path.parent, ) elif parser == "json": tilesets[int(raw_tileset.attrib["firstgid"])] = parse_json_tileset( json.load(tileset_file), int(raw_tileset.attrib["firstgid"]), + encoding, external_path=tileset_path.parent, ) else: @@ -56,13 +58,13 @@ def parse(file: Path) -> TiledMap: else: # Is an embedded Tileset tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset( - raw_tileset, int(raw_tileset.attrib["firstgid"]) + raw_tileset, int(raw_tileset.attrib["firstgid"]), encoding ) layers = [] for element in raw_map.findall("./*"): if element.tag in ["layer", "objectgroup", "imagelayer", "group"]: - layers.append(parse_layer(element, parent_dir)) + layers.append(parse_layer(element, encoding, parent_dir)) map_ = TiledMap( map_file=file, @@ -114,6 +116,7 @@ def parse(file: Path) -> TiledMap: map_.tilesets[new_firstgid] = parse_tmx_tileset( tiled_object.new_tileset, new_firstgid, + encoding, tiled_object.new_tileset_path, ) tiled_object.gid = tiled_object.gid + (new_firstgid - 1) diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py index a2e16d53..a36cfd1a 100644 --- a/pytiled_parser/parsers/tmx/tiled_object.py +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -245,7 +245,9 @@ def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObj return _parse_rectangle -def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> TiledObject: +def parse( + raw_object: etree.Element, encoding: str, parent_dir: Optional[Path] = None +) -> TiledObject: """Parse the raw object into a pytiled_parser version Args: @@ -267,7 +269,9 @@ def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> Tiled "A parent directory must be specified when using object templates." ) template_path = Path(parent_dir / raw_object.attrib["template"]) - template, new_tileset, new_tileset_path = load_object_template(template_path) + template, new_tileset, new_tileset_path = load_object_template( + template_path, encoding + ) if isinstance(template, etree.Element): new_object = template.find("./object") diff --git a/pytiled_parser/parsers/tmx/tileset.py b/pytiled_parser/parsers/tmx/tileset.py index 5d788af8..13933f9b 100644 --- a/pytiled_parser/parsers/tmx/tileset.py +++ b/pytiled_parser/parsers/tmx/tileset.py @@ -63,7 +63,9 @@ def _parse_transformations(raw_transformations: etree.Element) -> Transformation ) -def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) -> Tile: +def _parse_tile( + raw_tile: etree.Element, encoding: str, external_path: Optional[Path] = None +) -> Tile: """Parse the raw_tile to a Tile object. Args: @@ -89,7 +91,7 @@ def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) - object_element = raw_tile.find("./objectgroup") if object_element is not None: - tile.objects = parse_layer(object_element) + tile.objects = parse_layer(object_element, encoding) properties_element = raw_tile.find("./properties") if properties_element is not None: @@ -129,6 +131,7 @@ def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) - def parse( raw_tileset: etree.Element, firstgid: int, + encoding: str, external_path: Optional[Path] = None, ) -> Tileset: tileset = Tileset( @@ -204,7 +207,7 @@ def parse( tiles = {} for tile_element in raw_tileset.findall("./tile"): tiles[int(tile_element.attrib["id"])] = _parse_tile( - tile_element, external_path=external_path + tile_element, encoding, external_path=external_path ) if tiles: tileset.tiles = tiles diff --git a/pytiled_parser/util.py b/pytiled_parser/util.py index 8ef4e257..089cdace 100644 --- a/pytiled_parser/util.py +++ b/pytiled_parser/util.py @@ -37,8 +37,8 @@ def parse_color(color: str) -> Color: raise ValueError("Improperly formatted color passed to parse_color") -def check_format(file_path: Path) -> str: - with open(file_path) as file: +def check_format(file_path: Path, encoding: str) -> str: + with open(file_path, encoding=encoding) as file: line = file.readline().rstrip().strip() if line[0] == "<": return "tmx" @@ -46,38 +46,38 @@ def check_format(file_path: Path) -> str: return "json" -def load_object_template(file_path: Path) -> Any: - template_format = check_format(file_path) +def load_object_template(file_path: Path, encoding: str) -> Any: + template_format = check_format(file_path, encoding) new_tileset = None new_tileset_path = None if template_format == "tmx": - with open(file_path) as template_file: + with open(file_path, encoding=encoding) as template_file: template = etree.parse(template_file).getroot() tileset_element = template.find("./tileset") if tileset_element is not None: tileset_path = Path(file_path.parent / tileset_element.attrib["source"]) - new_tileset = load_object_tileset(tileset_path) + new_tileset = load_object_tileset(tileset_path, encoding) new_tileset_path = tileset_path.parent else: - with open(file_path) as template_file: + with open(file_path, encoding=encoding) as template_file: template = json.load(template_file) if "tileset" in template: tileset_path = Path(file_path.parent / template["tileset"]["source"]) # type: ignore - new_tileset = load_object_tileset(tileset_path) + new_tileset = load_object_tileset(tileset_path, encoding) new_tileset_path = tileset_path.parent return (template, new_tileset, new_tileset_path) -def load_object_tileset(file_path: Path) -> Any: - tileset_format = check_format(file_path) +def load_object_tileset(file_path: Path, encoding: str) -> Any: + tileset_format = check_format(file_path, encoding) new_tileset = None - with open(file_path) as tileset_file: + with open(file_path, encoding=encoding) as tileset_file: if tileset_format == "tmx": new_tileset = etree.parse(tileset_file).getroot() else: diff --git a/pytiled_parser/world.py b/pytiled_parser/world.py index 67214a6a..64ddfed3 100644 --- a/pytiled_parser/world.py +++ b/pytiled_parser/world.py @@ -100,7 +100,7 @@ def _parse_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap: ) -def parse_world(file: Path) -> World: +def parse_world(file: Path, encoding: str) -> World: """Parse the raw world into a pytiled_parser type Args: @@ -110,7 +110,7 @@ def parse_world(file: Path) -> World: World: A properly parsed [World][pytiled_parser.world.World] """ - with open(file) as world_file: + with open(file, encoding="utf-8") as world_file: raw_world = json.load(world_file) parent_dir = file.parent diff --git a/tests/test_layer.py b/tests/test_layer.py index 727fca45..bf47d1d9 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -1,4 +1,5 @@ """Tests for tilesets""" + import importlib.util import json import os @@ -72,23 +73,25 @@ def test_layer_integration(parser_type, layer_test): raw_layers_path = layer_test / "map.json" with open(raw_layers_path) as raw_layers_file: raw_layers = json.load(raw_layers_file)["layers"] - layers = [parse_json(raw_layer) for raw_layer in raw_layers] + layers = [ + parse_json(raw_layer, encoding="utf-8") for raw_layer in raw_layers + ] elif parser_type == "tmx": raw_layers_path = layer_test / "map.tmx" with open(raw_layers_path) as raw_layers_file: raw_layer = etree.parse(raw_layers_file).getroot() layers = [] for layer in raw_layer.findall("./layer"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in raw_layer.findall("./objectgroup"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in raw_layer.findall("./group"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in raw_layer.findall("./imagelayer"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in layers: fix_layer(layer) @@ -99,6 +102,7 @@ def test_layer_integration(parser_type, layer_test): assert layers == expected.EXPECTED + @pytest.mark.parametrize("parser_type", ["json", "tmx"]) def test_zstd_not_installed(parser_type): if parser_type == "json": @@ -106,7 +110,9 @@ def test_zstd_not_installed(parser_type): with open(raw_layers_path) as raw_layers_file: raw_layers = json.load(raw_layers_file)["layers"] with pytest.raises(ValueError): - layers = [parse_json(raw_layer) for raw_layer in raw_layers] + layers = [ + parse_json(raw_layer, encoding="utf-8") for raw_layer in raw_layers + ] elif parser_type == "tmx": raw_layers_path = ZSTD_LAYER_TEST / "map.tmx" with open(raw_layers_path) as raw_layers_file: @@ -114,25 +120,28 @@ def test_zstd_not_installed(parser_type): raw_layer = etree.parse(raw_layers_file).getroot() layers = [] for layer in raw_layer.findall("./layer"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in raw_layer.findall("./objectgroup"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in raw_layer.findall("./group"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in raw_layer.findall("./imagelayer"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) + def test_unknown_layer_type(): # We only test JSON here because due to the nature of the TMX format # there does not exist a scenario where pytiled_parser can attempt to - # parse an unknown layer type. In JSON a RuntimeError error will be + # parse an unknown layer type. In JSON a RuntimeError error will be # raised if an unknown type is provided. In TMX the layer will just # be ignored. raw_layers_path = UNKNOWN_LAYER_TYPE_TEST / "map.json" with open(raw_layers_path) as raw_layers_file: raw_layers = json.load(raw_layers_file)["layers"] with pytest.raises(RuntimeError): - layers = [parse_json(raw_layer) for raw_layer in raw_layers] + layers = [ + parse_json(raw_layer, encoding="utf-8") for raw_layer in raw_layers + ] diff --git a/tests/test_tiled_object_json.py b/tests/test_tiled_object_json.py index 90daf31b..16d24737 100644 --- a/tests/test_tiled_object_json.py +++ b/tests/test_tiled_object_json.py @@ -1,4 +1,5 @@ """Tests for objects""" + import json from contextlib import ExitStack as does_not_raise from pathlib import Path @@ -1110,7 +1111,7 @@ @pytest.mark.parametrize("raw_object_json,expected", OBJECTS) def test_parse_layer(raw_object_json, expected): raw_object = json.loads(raw_object_json) - result = parse(raw_object) + result = parse(raw_object, encoding="utf-8") assert result == expected @@ -1128,4 +1129,4 @@ def test_parse_no_parent_dir(): json_object = json.loads(raw_object) with pytest.raises(RuntimeError): - parse(json_object) + parse(json_object, encoding="utf-8") diff --git a/tests/test_tiled_object_tmx.py b/tests/test_tiled_object_tmx.py index 9b6a3848..0b6fd070 100644 --- a/tests/test_tiled_object_tmx.py +++ b/tests/test_tiled_object_tmx.py @@ -1,4 +1,5 @@ """Tests for objects""" + import xml.etree.ElementTree as etree from contextlib import ExitStack as does_not_raise from pathlib import Path @@ -491,6 +492,6 @@ @pytest.mark.parametrize("raw_object_tmx,expected", OBJECTS) def test_parse_layer(raw_object_tmx, expected): raw_object = etree.fromstring(raw_object_tmx) - result = parse(raw_object) + result = parse(raw_object, encoding="utf-8") assert result == expected diff --git a/tests/test_world.py b/tests/test_world.py index 1bed9817..8224e035 100644 --- a/tests/test_world.py +++ b/tests/test_world.py @@ -1,4 +1,5 @@ """Tests for worlds""" + import importlib.util import operator import os @@ -35,7 +36,7 @@ def test_world_integration(world_test): raw_world_path = world_test / "world.world" - casted_world = world.parse_world(raw_world_path) + casted_world = world.parse_world(raw_world_path, encoding="utf-8") # These fix calls sort the map list in the world by the map_file # attribute because we don't actually care about the order of the list From cf28bf54ee40b5c31d73d13ecc66b67d28117c5f Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 16 Jan 2025 15:40:47 -0500 Subject: [PATCH 13/23] Add explicit support or Python 3.13 --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ README.md | 2 +- pyproject.toml | 66 +++++++++++++++----------------------- 4 files changed, 30 insertions(+), 42 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c6b8b24..91bd5ce2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] architecture: ['x64'] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e93fca6..2ae6a7e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [2.2.8] - UNRELEASED +Add explicit support for 3.13, previous versions work on 3.13 but it was not explicitly labeled as supported or being tested by CI. + Converts all file loading to use UTF-8 encoding by default. In most cases, all Tiled files will be exported from Tiled in UTF-8 encoding, however the python `open()` function uses the system default locale. The only case where Tiled would not have used UTF-8 is for JSON files when Tiled was compiled against Qt 5, which is only in some builds of Tiled from older systems. All XML files exported from Tiled will always be UTF-8. If someone happens to have a JSON file which was exported from Tiled on an encoding other than UTF-8, or for some other reason is in a different encoding. This can be switched using a new optional argument named `encoding` in the various public API `parse` functions such as `parse_map()`. This value is handed down through the pipeline of file loading in pytiled-parser, and will apply to every file loaded during the chain from this. This means that every file in a chain(for example, Map, Tileset, and Template File) must share the same encoding. This new argument is a string which is ultimately passed to the Python [open()](https://docs.python.org/3/library/functions.html#open) function. This change does introduce breaking changes in the underlying API which is not intended to be public facing, but if you are going deeper than the top level parse functions, you may need to adjust for this, as many of the underlying internal functions now have a mandatory encoding argument. ## [2.2.7] - 2024-10-03 diff --git a/README.md b/README.md index e06358b0..faba7630 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ integrate PyTiled Parser and [example code](https://api.arcade.academy/en/latest ## Supported Python Versions -pytiled-parser works on Python 3.6 - 3.12(and should continue to work on new versions). However we are not actively testing on Python 3.6 and 3.7 due to other development tooling not supporting them, however as long as it remains tenable to do so, we will support these versions and fix problems for them. +pytiled-parser works on Python 3.6 - 3.13(and should continue to work on new versions). However we are not actively testing on Python 3.6 and 3.7 due to other development tooling not supporting them, however as long as it remains tenable to do so, we will support these versions and fix problems for them. ## Installation diff --git a/pyproject.toml b/pyproject.toml index 6869122d..b68b3fbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,42 +4,36 @@ version = "2.2.7" description = "A library for parsing Tiled Map Editor maps and tilesets" readme = "README.md" authors = [ - {name="Benjamin Kirkbride", email="BenjaminKirkbride@gmail.com"}, - {name="Darren Eberly", email="Darren.Eberly@gmail.com"}, + { name = "Benjamin Kirkbride", email = "BenjaminKirkbride@gmail.com" }, + { name = "Darren Eberly", email = "Darren.Eberly@gmail.com" }, ] -maintainers = [ - {name="Darren Eberly", email="Darren.Eberly@gmail.com"} -] -license = {file = "LICENSE"} +maintainers = [{ name = "Darren Eberly", email = "Darren.Eberly@gmail.com" }] +license = { file = "LICENSE" } requires-python = ">=3.6" classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Software Development :: Libraries :: Python Modules" -] -dependencies = [ - "attrs >= 18.2.0", - "typing-extensions" + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Software Development :: Libraries :: Python Modules", ] +dependencies = ["attrs >= 18.2.0", "typing-extensions"] [project.urls] homepage = "https://github.com/pythonarcade/pytiled_parser" [project.optional-dependencies] -zstd = [ - "zstd" -] +zstd = ["zstd"] dev = [ "pytest", @@ -50,20 +44,12 @@ dev = [ "sphinx", "sphinx-sitemap", "myst-parser", - "furo" + "furo", ] -tests = [ - "pytest", - "pytest-cov", - "black", - "ruff", - "mypy" -] +tests = ["pytest", "pytest-cov", "black", "ruff", "mypy"] -build = [ - "build" -] +build = ["build"] [tool.setuptools.packages.find] include = ["pytiled_parser", "pytiled_parser.*"] @@ -82,7 +68,7 @@ branch = true show_missing = true [tool.mypy] -python_version = 3.11 +python_version = "3.13" warn_unused_configs = true warn_redundant_casts = true ignore_missing_imports = true @@ -93,4 +79,4 @@ ignore_errors = true [tool.ruff] exclude = ["__init__.py"] -ignore = ["E501"] \ No newline at end of file +ignore = ["E501"] From 1ade435d7db2dab3fef089a4e578d9b4039109b6 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 16 Jan 2025 15:41:07 -0500 Subject: [PATCH 14/23] Switch to PyPi Trusted Publishers --- .github/workflows/publish-to-pypi.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 5faeb7ca..4f0655df 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -9,12 +9,14 @@ jobs: name: Build and Publish to PyPI runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags') + permissions: + id-token: write steps: - uses: actions/checkout@master - - name: Set up Python 3.10 - uses: actions/setup-python@v3 + - name: Set up Python 3.13 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" - name: Install pypa/build run: >- python -m pip install --user ".[build]" @@ -23,5 +25,3 @@ jobs: python -m build --sdist --wheel --outdir dist/ - name: Publish Distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file From bdc671b1755ed832ff4243a115ccab105eaa59f1 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 16 Jan 2025 15:43:17 -0500 Subject: [PATCH 15/23] Another small encoding fix --- pytiled_parser/parsers/tmx/layer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytiled_parser/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py index 648d0fe2..3891ce3f 100644 --- a/pytiled_parser/parsers/tmx/layer.py +++ b/pytiled_parser/parsers/tmx/layer.py @@ -337,7 +337,7 @@ def _parse_group_layer( layers.append(_parse_image_layer(layer)) for layer in raw_layer.findall("./group"): - layers.append(_parse_group_layer(layer, parent_dir)) + layers.append(_parse_group_layer(layer, encoding, parent_dir)) # layers = [] # layers = [ # parse(child_layer, parent_dir=parent_dir) From 73f9d9d2b60b2c3bda63a9bb23d0c3ff52d826ea Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 16 Jan 2025 16:06:37 -0500 Subject: [PATCH 16/23] Explicit int and float property types. (#80) --- pytiled_parser/parsers/json/properties.py | 2 + pytiled_parser/parsers/tmx/properties.py | 4 +- pytiled_parser/properties.py | 2 +- .../expected.py | 48 +++++++++++++++ .../special_do_not_resave_from_tiled/map.json | 59 +++++++++++++++++++ .../special_do_not_resave_from_tiled/map.tmx | 13 ++++ .../tileset.json | 14 +++++ .../tileset.tsx | 4 ++ tests/test_map.py | 4 ++ 9 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 tests/test_data/map_tests/special_do_not_resave_from_tiled/expected.py create mode 100644 tests/test_data/map_tests/special_do_not_resave_from_tiled/map.json create mode 100644 tests/test_data/map_tests/special_do_not_resave_from_tiled/map.tmx create mode 100644 tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.json create mode 100644 tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.tsx diff --git a/pytiled_parser/parsers/json/properties.py b/pytiled_parser/parsers/json/properties.py index 62b2e17b..784125df 100644 --- a/pytiled_parser/parsers/json/properties.py +++ b/pytiled_parser/parsers/json/properties.py @@ -45,6 +45,8 @@ def parse(raw_properties: List[RawProperty]) -> Properties: value = Path(cast(str, raw_property["value"])) elif raw_property["type"] == "color": value = parse_color(cast(str, raw_property["value"])) + elif raw_property["type"] == "int": + value = round(raw_property["value"]) else: value = raw_property["value"] final[raw_property["name"]] = value diff --git a/pytiled_parser/parsers/tmx/properties.py b/pytiled_parser/parsers/tmx/properties.py index 2c6f7dc4..26b82433 100644 --- a/pytiled_parser/parsers/tmx/properties.py +++ b/pytiled_parser/parsers/tmx/properties.py @@ -20,7 +20,9 @@ def parse(raw_properties: etree.Element) -> Properties: value = Path(value_) elif type_ == "color": value = parse_color(value_) - elif type_ == "int" or type_ == "float": + elif type_ == "int": + value = round(float(value_)) + elif type_ == "float": value = float(value_) elif type_ == "bool": if value_ == "true": diff --git a/pytiled_parser/properties.py b/pytiled_parser/properties.py index f8bc0acf..a8d26b41 100644 --- a/pytiled_parser/properties.py +++ b/pytiled_parser/properties.py @@ -13,6 +13,6 @@ from .common_types import Color -Property = Union[float, Path, str, bool, Color] +Property = Union[int, float, Path, str, bool, Color] Properties = Dict[str, Property] diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/expected.py b/tests/test_data/map_tests/special_do_not_resave_from_tiled/expected.py new file mode 100644 index 00000000..16356569 --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/expected.py @@ -0,0 +1,48 @@ +from pathlib import Path + +from pytiled_parser import common_types, tiled_map, tileset + +EXPECTED = tiled_map.TiledMap( + map_file=None, + infinite=False, + layers=[], + map_size=common_types.Size(8, 6), + next_layer_id=2, + next_object_id=1, + orientation="orthogonal", + render_order="right-down", + tiled_version="1.9.1", + tile_size=common_types.Size(32, 32), + version="1.9", + background_color=common_types.Color(255, 0, 4, 255), + parallax_origin=common_types.OrderedPair(10, 15), + tilesets={ + 1: tileset.Tileset( + columns=8, + image=Path(Path(__file__).parent / "../../images/tmw_desert_spacing.png") + .absolute() + .resolve(), + image_width=265, + image_height=199, + firstgid=1, + margin=1, + spacing=1, + name="tile_set_image", + tile_count=48, + tiled_version="1.6.0", + tile_height=32, + tile_width=32, + version="1.6", + type="tileset", + ) + }, + properties={ + "bool property - true": True, + "color property": common_types.Color(73, 252, 255, 255), + "file property": Path("../../../../../../var/log/syslog"), + "float property": 1.23456789, + "int property": 13, + "broken int property": 14, + "string property": "Hello, World!!", + }, +) diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.json b/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.json new file mode 100644 index 00000000..6b147a85 --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.json @@ -0,0 +1,59 @@ +{ "backgroundcolor":"#ff0004", + "compressionlevel":0, + "height":6, + "infinite":false, + "layers":[], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "parallaxoriginx":10, + "parallaxoriginy":15, + "properties":[ + { + "name":"bool property - true", + "type":"bool", + "value":true + }, + { + "name":"color property", + "type":"color", + "value":"#ff49fcff" + }, + { + "name":"file property", + "type":"file", + "value":"..\/..\/..\/..\/..\/..\/var\/log\/syslog" + }, + { + "name":"float property", + "type":"float", + "value":1.23456789 + }, + { + "name":"int property", + "type":"int", + "value":13 + }, + { + "name":"broken int property", + "type":"int", + "value":13.6 + }, + { + "name":"string property", + "type":"string", + "value":"Hello, World!!" + }], + "renderorder":"right-down", + "tiledversion":"1.9.1", + "tileheight":32, + "tilesets":[ + { + "firstgid":1, + "source":"tileset.json" + }], + "tilewidth":32, + "type":"map", + "version":"1.9", + "width":8 +} \ No newline at end of file diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.tmx b/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.tmx new file mode 100644 index 00000000..ecdd0f87 --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.tmx @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.json b/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.json new file mode 100644 index 00000000..0879fe64 --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.json @@ -0,0 +1,14 @@ +{ "columns":8, + "image":"..\/..\/images\/tmw_desert_spacing.png", + "imageheight":199, + "imagewidth":265, + "margin":1, + "name":"tile_set_image", + "spacing":1, + "tilecount":48, + "tiledversion":"1.9.0", + "tileheight":32, + "tilewidth":32, + "type":"tileset", + "version":"1.8" +} \ No newline at end of file diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.tsx b/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.tsx new file mode 100644 index 00000000..8b1cf24b --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_map.py b/tests/test_map.py index 6083d0e3..4fe5a3b4 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -1,4 +1,5 @@ """Tests for maps""" + import importlib.util import os from pathlib import Path @@ -20,10 +21,12 @@ MAP_TESTS / "embedded_tileset", MAP_TESTS / "template", MAP_TESTS / "cross_format_tileset", + MAP_TESTS / "special_do_not_resave_from_tiled", ] JSON_INVALID_TILESET = MAP_TESTS / "json_invalid_tileset" + def fix_object(my_object): my_object.coordinates = OrderedPair( round(my_object.coordinates[0], 3), round(my_object.coordinates[1], 3) @@ -90,6 +93,7 @@ def test_map_integration(parser_type, map_test): fix_map(casted_map) assert casted_map == expected.EXPECTED + def test_json_invalid_tileset(): raw_map_path = JSON_INVALID_TILESET / "map.json" From 81e3dcbdc0462215df814a5a789432932e9699ee Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 16 Jan 2025 16:20:07 -0500 Subject: [PATCH 17/23] Ensure name field of objects is not overriden by templates (#79) --- pytiled_parser/parsers/json/tiled_object.py | 3 +++ tests/test_data/map_tests/template/expected.py | 2 +- tests/test_data/map_tests/template/map.json | 1 + tests/test_data/map_tests/template/map.tmx | 2 +- tests/test_data/map_tests/template/template-rectangle.tx | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py index e837bad9..6ca3dabb 100644 --- a/pytiled_parser/parsers/json/tiled_object.py +++ b/pytiled_parser/parsers/json/tiled_object.py @@ -337,6 +337,9 @@ def parse( if not found: raw_object["properties"].append(prop) + elif key == "name": + if "name" not in raw_object: + raw_object["name"] = loaded_template[key] else: raw_object[key] = loaded_template[key] # type: ignore else: diff --git a/tests/test_data/map_tests/template/expected.py b/tests/test_data/map_tests/template/expected.py index 7470be29..acc5bd84 100644 --- a/tests/test_data/map_tests/template/expected.py +++ b/tests/test_data/map_tests/template/expected.py @@ -15,7 +15,7 @@ tiled_objects=[ tiled_object.Rectangle( id=2, - name="", + name="hello", rotation=0, size=common_types.Size(63.6585878103079, 38.2811778048473), coordinates=common_types.OrderedPair( diff --git a/tests/test_data/map_tests/template/map.json b/tests/test_data/map_tests/template/map.json index b372362d..25cab9e5 100644 --- a/tests/test_data/map_tests/template/map.json +++ b/tests/test_data/map_tests/template/map.json @@ -17,6 +17,7 @@ "value":"hello" }], "template":"template-rectangle.json", + "name": "hello", "x":98.4987608686521, "y":46.2385012811358 }, diff --git a/tests/test_data/map_tests/template/map.tmx b/tests/test_data/map_tests/template/map.tmx index b2138990..18c0a273 100644 --- a/tests/test_data/map_tests/template/map.tmx +++ b/tests/test_data/map_tests/template/map.tmx @@ -11,7 +11,7 @@ - + diff --git a/tests/test_data/map_tests/template/template-rectangle.tx b/tests/test_data/map_tests/template/template-rectangle.tx index 1959f0f2..94fe8e38 100644 --- a/tests/test_data/map_tests/template/template-rectangle.tx +++ b/tests/test_data/map_tests/template/template-rectangle.tx @@ -1,6 +1,6 @@