From a78d7c8fa71d348c748a0dc4e0d984aedefb8e51 Mon Sep 17 00:00:00 2001 From: Jaskrendix Date: Fri, 19 Jan 2024 19:23:19 +0100 Subject: [PATCH 1/2] typehints --- apps/benchmark.py | 31 ++++++++++------ apps/pygame_demo.py | 31 ++++++++++------ apps/pygame_sdl2_demo.py | 11 ++++-- apps/pysdl2_demo.py | 4 +- pytmx/pytmx.py | 79 ++++++++++++++++++++++------------------ pytmx/util_pygame.py | 4 +- 6 files changed, 96 insertions(+), 64 deletions(-) diff --git a/apps/benchmark.py b/apps/benchmark.py index 9a20edc..b2aa1eb 100644 --- a/apps/benchmark.py +++ b/apps/benchmark.py @@ -29,14 +29,14 @@ logger.setLevel(logging.INFO) -def init_screen(width, height): +def init_screen(width: int, height: int) -> pygame.Surface: """Set the screen mode This function is used to handle window resize events """ return pygame.display.set_mode((width, height), pygame.RESIZABLE) -class TiledRenderer(object): +class TiledRenderer: """ Super simple way to render a tiled map """ @@ -55,7 +55,7 @@ def __init__(self, filename) -> None: print(i) continue - def render_map(self, surface) -> None: + def render_map(self, surface: pygame.Surface) -> None: """Render our map to a pygame surface Feel free to use this as a starting point for your pygame app. @@ -84,7 +84,7 @@ def render_map(self, surface) -> None: elif isinstance(layer, TiledImageLayer): self.render_image_layer(surface, layer) - def render_tile_layer(self, surface, layer) -> None: + def render_tile_layer(self, surface: pygame.Surface, layer: TiledTileLayer) -> None: """Render all TiledTiles in this layer""" # deref these heavily used references for speed tw = self.tmx_data.tilewidth @@ -95,7 +95,9 @@ def render_tile_layer(self, surface, layer) -> None: for x, y, image in layer.tiles(): surface_blit(image, (x * tw, y * th)) - def render_object_layer(self, surface, layer) -> None: + def render_object_layer( + self, surface: pygame.Surface, layer: TiledObjectGroup + ) -> None: """Render all TiledObjects contained in this layer""" # deref these heavily used references for speed draw_rect = pygame.draw.rect @@ -125,12 +127,14 @@ def render_object_layer(self, surface, layer) -> None: else: draw_rect(surface, rect_color, (obj.x, obj.y, obj.width, obj.height), 3) - def render_image_layer(self, surface, layer) -> None: + def render_image_layer( + self, surface: pygame.Surface, layer: TiledImageLayer + ) -> None: if layer.image: surface.blit(layer.image, (0, 0)) -class SimpleTest(object): +class SimpleTest: """Basic app to display a rendered Tiled map""" def __init__(self, filename) -> None: @@ -144,7 +148,7 @@ def load_map(self, filename) -> None: """Create a renderer, load data, and print some debug info""" self.renderer = TiledRenderer(filename) - def draw(self, surface) -> None: + def draw(self, surface: pygame.Surface) -> None: """Draw our map to some surface (probably the display)""" # first we make a temporary surface that will accommodate the entire # size of the map. @@ -161,7 +165,7 @@ def draw(self, surface) -> None: # display a bit of use info on the display f = pygame.font.Font(pygame.font.get_default_font(), 20) - i = f.render("press any key for next map or ESC to quit", 1, (180, 180, 0)) + i = f.render("press any key for next map or ESC to quit", True, (180, 180, 0)) surface.blit(i, (0, 0)) def handle_input(self) -> None: @@ -187,8 +191,13 @@ def handle_input(self) -> None: self.exit_status = 0 self.running = False - def run(self): - """This is our app main loop""" + def run(self) -> int: + """ + This is our app main loop + + Returns: + Int: 0 means no error, 1 is an error + """ self.dirty = True self.running = True self.exit_status = 1 diff --git a/apps/pygame_demo.py b/apps/pygame_demo.py index 5e8c375..7f25b5d 100644 --- a/apps/pygame_demo.py +++ b/apps/pygame_demo.py @@ -27,14 +27,14 @@ logger = logging.getLogger(__name__) -def init_screen(width, height): +def init_screen(width: int, height: int) -> pygame.Surface: """Set the screen mode This function is used to handle window resize events """ return pygame.display.set_mode((width, height), pygame.RESIZABLE) -class TiledRenderer(object): +class TiledRenderer: """ Super simple way to render a tiled map """ @@ -47,7 +47,7 @@ def __init__(self, filename) -> None: self.pixel_size = tm.width * tm.tilewidth, tm.height * tm.tileheight self.tmx_data = tm - def render_map(self, surface) -> None: + def render_map(self, surface: pygame.Surface) -> None: """Render our map to a pygame surface Feel free to use this as a starting point for your pygame app. @@ -76,7 +76,7 @@ def render_map(self, surface) -> None: elif isinstance(layer, TiledImageLayer): self.render_image_layer(surface, layer) - def render_tile_layer(self, surface, layer) -> None: + def render_tile_layer(self, surface: pygame.Surface, layer: TiledTileLayer) -> None: """Render all TiledTiles in this layer""" # deref these heavily used references for speed tw = self.tmx_data.tilewidth @@ -96,7 +96,9 @@ def render_tile_layer(self, surface, layer) -> None: sy = x * th2 + y * th2 surface_blit(image, (sx + ox, sy)) - def render_object_layer(self, surface, layer) -> None: + def render_object_layer( + self, surface: pygame.Surface, layer: TiledObjectGroup + ) -> None: """Render all TiledObjects contained in this layer""" # deref these heavily used references for speed draw_lines = pygame.draw.lines @@ -122,12 +124,14 @@ def render_object_layer(self, surface, layer) -> None: surface, rect_color, obj.closed, obj.apply_transformations(), 3 ) - def render_image_layer(self, surface, layer) -> None: + def render_image_layer( + self, surface: pygame.Surface, layer: TiledImageLayer + ) -> None: if layer.image: surface.blit(layer.image, (0, 0)) -class SimpleTest(object): +class SimpleTest: """Basic app to display a rendered Tiled map""" def __init__(self, filename) -> None: @@ -155,7 +159,7 @@ def load_map(self, filename) -> None: for k, v in self.renderer.tmx_data.get_tile_colliders(): logger.info("%s\t%s", k, list(v)) - def draw(self, surface) -> None: + def draw(self, surface: pygame.Surface) -> None: """Draw our map to some surface (probably the display)""" # first we make a temporary surface that will accommodate the entire # size of the map. @@ -172,7 +176,7 @@ def draw(self, surface) -> None: # display a bit of use info on the display f = pygame.font.Font(pygame.font.get_default_font(), 20) - i = f.render("press any key for next map or ESC to quit", 1, (180, 180, 0)) + i = f.render("press any key for next map or ESC to quit", True, (180, 180, 0)) surface.blit(i, (0, 0)) def handle_input(self) -> None: @@ -198,8 +202,13 @@ def handle_input(self) -> None: self.exit_status = 0 self.running = False - def run(self): - """This is our app main loop""" + def run(self) -> int: + """ + This is our app main loop + + Returns: + Int: 0 means no error, 1 is an error + """ self.dirty = True self.running = True self.exit_status = 1 diff --git a/apps/pygame_sdl2_demo.py b/apps/pygame_sdl2_demo.py index 7ad7578..e4bc93d 100644 --- a/apps/pygame_sdl2_demo.py +++ b/apps/pygame_sdl2_demo.py @@ -26,7 +26,7 @@ class GameContext: renderer: Renderer -class TiledRenderer(object): +class TiledRenderer: """ Super simple way to render a tiled map @@ -136,8 +136,13 @@ def handle_input(self) -> None: self.exit_status = 0 self.running = False - def run(self): - """This is our app main loop""" + def run(self) -> int: + """ + This is our app main loop + + Returns: + Int: 0 means no error, 1 is an error + """ self.running = True self.exit_status = 1 diff --git a/apps/pysdl2_demo.py b/apps/pysdl2_demo.py index fec4feb..72c0a28 100644 --- a/apps/pysdl2_demo.py +++ b/apps/pysdl2_demo.py @@ -36,7 +36,7 @@ from pytmx.util_pysdl2 import load_pysdl2 -class TiledRenderer(object): +class TiledRenderer: """ Super simple way to render a tiled map with pyglet @@ -81,7 +81,7 @@ def render_map(self) -> None: self.render_tile_layer(layer) -class SimpleTest(object): +class SimpleTest: def __init__(self, filename, window) -> None: self.running = False self.dirty = False diff --git a/pytmx/pytmx.py b/pytmx/pytmx.py index 7d40621..513483b 100644 --- a/pytmx/pytmx.py +++ b/pytmx/pytmx.py @@ -27,12 +27,12 @@ import zlib from base64 import b64decode from collections import defaultdict, namedtuple -from collections.abc import Iterable, Sequence +from collections.abc import Generator, Iterable, Sequence from copy import deepcopy from itertools import chain, product from math import cos, radians, sin from operator import attrgetter -from typing import Optional, Union +from typing import Any, Optional, Union from xml.etree import ElementTree # for type hinting @@ -136,10 +136,7 @@ def decode_gid(raw_gid: int) -> tuple[int, TileFlags]: ) -def reshape_data( - gids: list[int], - width: int, -) -> list[list[int]]: +def reshape_data(gids: list[int], width: int) -> list[list[int]]: """Change 1D list to 2d list Args: @@ -418,7 +415,9 @@ def _log_property_error_message() -> None: msg = "Some name are reserved for {0} objects and cannot be used." logger.error(msg) - def _set_properties(self, node: ElementTree.Element, customs=None) -> None: + def _set_properties( + self, node: ElementTree.Element, customs: Optional[dict] = None + ) -> None: """Set properties from xml data Reads the xml attributes and Tiled "properties" from an XML node and fills @@ -449,7 +448,7 @@ def __getattr__(self, item): else: raise AttributeError("Element has no property {0}".format(item)) - def __repr__(self): + def __repr__(self) -> str: if hasattr(self, "id"): return '<{}[{}]: "{}">'.format(self.__class__.__name__, self.id, self.name) else: @@ -506,8 +505,8 @@ def __init__( # allow duplicate names to be parsed and loaded TiledElement.allow_duplicate_names = kwargs.get("allow_duplicate_names", False) - self.layers = list() # all layers in proper order - self.tilesets = list() # TiledTileset objects + self.layers: list[TiledLayer] = [] # all layers in proper order + self.tilesets: list[TiledTileset] = [] # TiledTileset objects self.tile_properties = dict() # tiles that have properties self.layernames = dict() self.objects_by_id = dict() @@ -550,7 +549,7 @@ def __init__( if filename: self.parse_xml(ElementTree.parse(self.filename).getroot()) - def __repr__(self): + def __repr__(self) -> str: return '<{0}: "{1}">'.format(self.__class__.__name__, self.filename) # iterate over layers and objects in map @@ -1046,11 +1045,11 @@ def objects(self) -> Iterable[TiledObject]: return chain(*self.objectgroups) @property - def visible_layers(self): + def visible_layers(self) -> Iterable[TiledLayer]: """Returns iterator of Layer objects that are set "visible". Returns: - ???: Iterator of Layer objects that are set "visible". + Iterable[TiledLayer]: Layer objects that are set "visible". """ @@ -1178,11 +1177,11 @@ class TiledTileset(TiledElement): """ - def __init__(self, parent, node) -> None: + def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: """Represents a Tiled Tileset Args: - parent (???): ???. + parent (TiledMap): ???. node (ElementTree.Element): ???. """ @@ -1208,7 +1207,7 @@ def __init__(self, parent, node) -> None: self.parse_xml(node) - def parse_xml(self, node: ElementTree.Element) -> "TiledTileset": + def parse_xml(self, node: ElementTree.Element) -> TiledTileset: """Parse a Tileset from ElementTree xml element. A bit of mangling is done here so that tilesets that have @@ -1320,21 +1319,21 @@ def parse_xml(self, node: ElementTree.Element) -> "TiledTileset": class TiledGroupLayer(TiledElement): - def __init__(self, parent, node: ElementTree.Element) -> None: + def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: """ Args: - parent (???): ???. + parent (TiledMap): ???. node (ElementTree.Element): ???. """ TiledElement.__init__(self) self.parent = parent self.name = None - self.visible = 1 + self.visible = True self.parse_xml(node) - def parse_xml(self, node) -> "TiledGroupLayer": + def parse_xml(self, node: ElementTree.Element) -> TiledGroupLayer: """ Parse a TiledGroup layer from ElementTree xml node. @@ -1357,10 +1356,10 @@ class TiledTileLayer(TiledElement): """ - def __init__(self, parent, node) -> None: + def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: TiledElement.__init__(self) self.parent = parent - self.data = list() + self.data: list[list[int]] = [] # defaults from the specification self.name = None @@ -1373,7 +1372,7 @@ def __init__(self, parent, node) -> None: self.parse_xml(node) - def __iter__(self): + def __iter__(self) -> Iterable[tuple[int, int, int]]: return self.iter_data() def iter_data(self) -> Iterable[tuple[int, int, int]]: @@ -1387,7 +1386,7 @@ def iter_data(self) -> Iterable[tuple[int, int, int]]: for x, gid in enumerate(row): yield x, y, gid - def tiles(self): + def tiles(self) -> Generator[tuple[int, int, Any], Any, None]: """Yields X, Y, Image tuples for each tile in the layer. Yields: @@ -1407,7 +1406,7 @@ def _set_properties(self, node) -> None: self.height = int(self.height) self.width = int(self.width) - def parse_xml(self, node: ElementTree.Element) -> "TiledTileLayer": + def parse_xml(self, node: ElementTree.Element) -> TiledTileLayer: """Parse a Tile Layer from ElementTree xml node. Args: @@ -1451,7 +1450,12 @@ class TiledObjectGroup(TiledElement, list): """ - def __init__(self, parent, node, customs) -> None: + def __init__( + self, + parent: TiledMap, + node: ElementTree.Element, + customs: Optional[dict] = None, + ) -> None: TiledElement.__init__(self) self.parent = parent @@ -1459,7 +1463,7 @@ def __init__(self, parent, node, customs) -> None: self.name = None self.color = None self.opacity = 1 - self.visible = 1 + self.visible = True self.offsetx = 0 self.offsety = 0 self.custom_types = customs @@ -1467,7 +1471,7 @@ def __init__(self, parent, node, customs) -> None: self.parse_xml(node) - def parse_xml(self, node: ElementTree.Element) -> "TiledObjectGroup": + def parse_xml(self, node: ElementTree.Element) -> TiledObjectGroup: """Parse an Object Group from ElementTree xml node Args: @@ -1490,7 +1494,12 @@ class TiledObject(TiledElement): """ - def __init__(self, parent, node, custom_types) -> None: + def __init__( + self, + parent: TiledMap, + node: ElementTree.Element, + custom_types: Optional[dict] = None, + ) -> None: TiledElement.__init__(self) self.parent = parent @@ -1504,7 +1513,7 @@ def __init__(self, parent, node, custom_types) -> None: self.height = 0 self.rotation = 0 self.gid = 0 - self.visible = 1 + self.visible = True self.closed = True self.template = None self.custom_types = custom_types @@ -1523,7 +1532,7 @@ def image(self): return self.parent.images[self.gid] return None - def parse_xml(self, node: ElementTree.Element) -> "TiledObject": + def parse_xml(self, node: ElementTree.Element) -> TiledObject: """Parse an Object from ElementTree xml node. Args: @@ -1602,7 +1611,7 @@ class TiledImageLayer(TiledElement): """ - def __init__(self, parent, node: ElementTree.Element) -> None: + def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: TiledElement.__init__(self) self.parent = parent self.source = None @@ -1612,7 +1621,7 @@ def __init__(self, parent, node: ElementTree.Element) -> None: # defaults from the specification self.name = None self.opacity = 1 - self.visible = 1 + self.visible = True self.parse_xml(node) @@ -1633,7 +1642,7 @@ def parse_xml(self, node: ElementTree.Element): self._set_properties(node) self.name = node.get("name", None) self.opacity = node.get("opacity", self.opacity) - self.visible = node.get("visible", self.visible) + self.visible = bool(node.get("visible", self.visible)) image_node = node.find("image") self.source = image_node.get("source", None) self.trans = image_node.get("trans", None) @@ -1643,7 +1652,7 @@ def parse_xml(self, node: ElementTree.Element): class TiledProperty(TiledElement): """Represents Tiled Property.""" - def __init__(self, parent, node: ElementTree.Element) -> None: + def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: TiledElement.__init__(self) # defaults from the specification diff --git a/pytmx/util_pygame.py b/pytmx/util_pygame.py index f1dc2ac..d563586 100644 --- a/pytmx/util_pygame.py +++ b/pytmx/util_pygame.py @@ -240,8 +240,8 @@ def build_rects( layer_data = tmxmap.get_layer_data(layer) elif isinstance(layer, str): try: - layer = [l for l in tmxmap.layers if l.name == layer].pop() - layer_data = layer.data + _layer = [l for l in tmxmap.layers if l.name == layer].pop() + layer_data = _layer.data except IndexError: msg = 'Layer "{0}" not found in map {1}.' logger.debug(msg.format(layer, tmxmap)) From f9f15d2d51dbf47593af108a0b980a5efc3d2350 Mon Sep 17 00:00:00 2001 From: JaskRendix Date: Thu, 24 Jul 2025 15:31:12 +0200 Subject: [PATCH 2/2] type hints --- .github/workflows/test.yml | 2 +- Makefile | 22 ++++ apps/benchmark.py | 39 +++--- apps/pygame_demo.py | 29 ++--- apps/pygame_sdl2_demo.py | 11 +- apps/pyglet_demo.py | 2 +- apps/pysdl2_demo.py | 13 +- docs/conf.py | 2 +- pytmx/__init__.py | 3 +- pytmx/pytmx.py | 243 ++++++++++++++++--------------------- pytmx/util_pyglet.py | 2 +- pytmx/util_pysdl2.py | 2 +- setup.py | 8 -- 13 files changed, 178 insertions(+), 200 deletions(-) create mode 100755 Makefile delete mode 100755 setup.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6048d2b..f4ed9cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,4 +28,4 @@ jobs: if: ${{ matrix.dependencies != 'null' }} run: pip install ${{ matrix.dependencies }} - name: Run Tests - run: python -m unittest tests/pytmx/test_pytmx.py + run: python -m unittest discover -s tests/pytmx -p "test_*.py" diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..13d4e57 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +PACKAGE_NAME := pytmx + +.PHONY: build clean publish install lint test + +build: + python3 -m build + +clean: + rm -rf dist/ *.egg-info + +publish: build + python3 -m twine upload dist/* + +install: + pip install -e . + +lint: + black $(PACKAGE_NAME)/ + isort $(PACKAGE_NAME)/ + +test: + pytest tests/ diff --git a/apps/benchmark.py b/apps/benchmark.py index b2aa1eb..b406d7b 100644 --- a/apps/benchmark.py +++ b/apps/benchmark.py @@ -1,6 +1,6 @@ """ This is tested on pygame 1.9 and python 2.7 and 3.3+. -Leif Theden "bitcraft", 2012-2024 +Leif Theden "bitcraft", 2012-2025 Rendering demo for the TMXLoader. @@ -14,7 +14,9 @@ - object rotation - terrains """ + import logging +from pathlib import Path import pygame from pygame.locals import * @@ -41,8 +43,8 @@ class TiledRenderer: Super simple way to render a tiled map """ - def __init__(self, filename) -> None: - tm = load_pygame(filename) + def __init__(self, filename: Path) -> None: + tm = load_pygame(filename.as_posix()) # self.size will be the pixel size of the map # this value is used later to render the entire map to a pygame surface @@ -137,14 +139,14 @@ def render_image_layer( class SimpleTest: """Basic app to display a rendered Tiled map""" - def __init__(self, filename) -> None: + def __init__(self, filename: Path) -> None: self.renderer = None self.running = False self.dirty = False self.exit_status = 0 self.load_map(filename) - def load_map(self, filename) -> None: + def load_map(self, filename: Path) -> None: """Create a renderer, load data, and print some debug info""" self.renderer = TiledRenderer(filename) @@ -217,8 +219,7 @@ def run(self) -> int: if __name__ == "__main__": - import glob - import os.path + import time import pytmx @@ -227,22 +228,28 @@ def run(self) -> int: screen = init_screen(600, 600) pygame.display.set_caption("PyTMX Map Viewer") - logger.info(pytmx.__version__) + logger.info(f"PyTMX Version: {pytmx.__version__}") - # loop through a bunch of maps in the maps folder - import time + # Path to your maps folder + map_folder = Path("apps/data") + map_files = sorted(map_folder.glob("*.tmx")) # sort for consistent order + + if not map_files: + logger.warning("No TMX files found in apps/data/") + else: + logger.info(f"Found {len(map_files)} TMX files.") try: - # 6.6 start = time.time() + for i in range(500): - for filename in glob.glob(os.path.join("*.tmx")): + for filepath in map_files: pygame.event.clear() - SimpleTest(filename) + SimpleTest(filepath).run() - end = time.time() - start - print(end) + logger.info(f"Total time: {time.time() - start:.2f} seconds") - except: + except Exception as e: + logger.exception("Unexpected error:") pygame.quit() raise diff --git a/apps/pygame_demo.py b/apps/pygame_demo.py index 7f25b5d..64e2c32 100644 --- a/apps/pygame_demo.py +++ b/apps/pygame_demo.py @@ -27,14 +27,14 @@ logger = logging.getLogger(__name__) -def init_screen(width: int, height: int) -> pygame.Surface: +def init_screen(width, height): """Set the screen mode This function is used to handle window resize events """ return pygame.display.set_mode((width, height), pygame.RESIZABLE) -class TiledRenderer: +class TiledRenderer(object): """ Super simple way to render a tiled map """ @@ -47,7 +47,7 @@ def __init__(self, filename) -> None: self.pixel_size = tm.width * tm.tilewidth, tm.height * tm.tileheight self.tmx_data = tm - def render_map(self, surface: pygame.Surface) -> None: + def render_map(self, surface) -> None: """Render our map to a pygame surface Feel free to use this as a starting point for your pygame app. @@ -76,7 +76,7 @@ def render_map(self, surface: pygame.Surface) -> None: elif isinstance(layer, TiledImageLayer): self.render_image_layer(surface, layer) - def render_tile_layer(self, surface: pygame.Surface, layer: TiledTileLayer) -> None: + def render_tile_layer(self, surface, layer) -> None: """Render all TiledTiles in this layer""" # deref these heavily used references for speed tw = self.tmx_data.tilewidth @@ -96,9 +96,7 @@ def render_tile_layer(self, surface: pygame.Surface, layer: TiledTileLayer) -> N sy = x * th2 + y * th2 surface_blit(image, (sx + ox, sy)) - def render_object_layer( - self, surface: pygame.Surface, layer: TiledObjectGroup - ) -> None: + def render_object_layer(self, surface, layer) -> None: """Render all TiledObjects contained in this layer""" # deref these heavily used references for speed draw_lines = pygame.draw.lines @@ -124,14 +122,12 @@ def render_object_layer( surface, rect_color, obj.closed, obj.apply_transformations(), 3 ) - def render_image_layer( - self, surface: pygame.Surface, layer: TiledImageLayer - ) -> None: + def render_image_layer(self, surface, layer) -> None: if layer.image: surface.blit(layer.image, (0, 0)) -class SimpleTest: +class SimpleTest(object): """Basic app to display a rendered Tiled map""" def __init__(self, filename) -> None: @@ -159,7 +155,7 @@ def load_map(self, filename) -> None: for k, v in self.renderer.tmx_data.get_tile_colliders(): logger.info("%s\t%s", k, list(v)) - def draw(self, surface: pygame.Surface) -> None: + def draw(self, surface) -> None: """Draw our map to some surface (probably the display)""" # first we make a temporary surface that will accommodate the entire # size of the map. @@ -202,13 +198,8 @@ def handle_input(self) -> None: self.exit_status = 0 self.running = False - def run(self) -> int: - """ - This is our app main loop - - Returns: - Int: 0 means no error, 1 is an error - """ + def run(self): + """This is our app main loop""" self.dirty = True self.running = True self.exit_status = 1 diff --git a/apps/pygame_sdl2_demo.py b/apps/pygame_sdl2_demo.py index e4bc93d..7ad7578 100644 --- a/apps/pygame_sdl2_demo.py +++ b/apps/pygame_sdl2_demo.py @@ -26,7 +26,7 @@ class GameContext: renderer: Renderer -class TiledRenderer: +class TiledRenderer(object): """ Super simple way to render a tiled map @@ -136,13 +136,8 @@ def handle_input(self) -> None: self.exit_status = 0 self.running = False - def run(self) -> int: - """ - This is our app main loop - - Returns: - Int: 0 means no error, 1 is an error - """ + def run(self): + """This is our app main loop""" self.running = True self.exit_status = 1 diff --git a/apps/pyglet_demo.py b/apps/pyglet_demo.py index d47123f..c8fe6cb 100644 --- a/apps/pyglet_demo.py +++ b/apps/pyglet_demo.py @@ -1,6 +1,6 @@ """ This is tested on pyglet 1.2 and python 2.7. -Leif Theden "bitcraft", 2012-2024 +Leif Theden "bitcraft", 2012-2025 Rendering demo for the TMXLoader. diff --git a/apps/pysdl2_demo.py b/apps/pysdl2_demo.py index 72c0a28..255d699 100644 --- a/apps/pysdl2_demo.py +++ b/apps/pysdl2_demo.py @@ -1,6 +1,6 @@ """ This is tested on pysdl2 1.2 and python 2.7. -Leif Theden "bitcraft", 2012-2024 +Leif Theden "bitcraft", 2012-2025 Rendering demo for the TMXLoader. @@ -104,8 +104,13 @@ def draw(self) -> None: self.map_renderer.render_map() self.sdl_renderer.present() - def run(self, window): - """Starts an event loop without actually processing any event.""" + def run(self, window) -> int: + """ + Starts an event loop without actually processing any event. + + Returns: + Int: 0 means no error, 1 is an error + """ import ctypes event = events.SDL_Event() @@ -126,7 +131,7 @@ def run(self, window): return self.exit_status -def all_filenames(): +def all_filenames() -> list[str]: import glob import os.path diff --git a/docs/conf.py b/docs/conf.py index 3f16112..211668b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ from typing import Any project = "pytmx" -copyright = "2012-2024, Leif Theden" +copyright = "2012-2025, Leif Theden" author = "Leif Theden" # The full version, including alpha/beta/rc tags diff --git a/pytmx/__init__.py b/pytmx/__init__.py index 056a051..37de55e 100644 --- a/pytmx/__init__.py +++ b/pytmx/__init__.py @@ -1,5 +1,5 @@ """ -Copyright (C) 2012-2024, Leif Theden +Copyright (C) 2012-2025, Leif Theden This file is part of pytmx. @@ -16,6 +16,7 @@ You should have received a copy of the GNU Lesser General Public License along with pytmx. If not, see . """ + import logging from .pytmx import * diff --git a/pytmx/pytmx.py b/pytmx/pytmx.py index 513483b..dc397f4 100644 --- a/pytmx/pytmx.py +++ b/pytmx/pytmx.py @@ -1,5 +1,5 @@ """ -Copyright (C) 2012-2024, Leif Theden +Copyright (C) 2012-2025, Leif Theden This file is part of pytmx. @@ -17,6 +17,7 @@ License along with pytmx. If not, see . """ + from __future__ import annotations import gzip @@ -27,7 +28,7 @@ import zlib from base64 import b64decode from collections import defaultdict, namedtuple -from collections.abc import Generator, Iterable, Sequence +from collections.abc import Iterable, Sequence from copy import deepcopy from itertools import chain, product from math import cos, radians, sin @@ -74,11 +75,11 @@ 'Cannot set user {} property on {} "{}"; Tiled property already exists.' ) -flag_names = ("flipped_horizontally", "flipped_vertically", "flipped_diagonally") - AnimationFrame = namedtuple("AnimationFrame", ["gid", "duration"]) Point = namedtuple("Point", ["x", "y"]) -TileFlags = namedtuple("TileFlags", flag_names) +TileFlags = namedtuple( + "TileFlags", ("flipped_horizontally", "flipped_vertically", "flipped_diagonally") +) empty_flags = TileFlags(False, False, False) ColorLike = Union[tuple[int, int, int, int], tuple[int, int, int], int, str] MapPoint = tuple[int, int, int] @@ -104,7 +105,6 @@ def default_image_loader(filename: str, flags, **kwargs): Returns: Tuple[str, ???, ???]: A tuple of the file name, rect, and flags. - """ def load(rect=None, flags=None): @@ -121,7 +121,6 @@ def decode_gid(raw_gid: int) -> tuple[int, TileFlags]: Returns: Tuple[int, TileFlags]: Tuple of the GID after rotation flags, and TileFlags object - """ if raw_gid < GID_TRANS_ROT: return raw_gid, empty_flags @@ -145,7 +144,6 @@ def reshape_data(gids: list[int], width: int) -> list[list[int]]: Returns: List[List[int]]: 2D nested list object. - """ return [gids[i : i + width] for i in range(0, len(gids), width)] @@ -164,7 +162,6 @@ def unpack_gids( Returns: List[int]: List of all the GIDs in the layer. - """ if encoding == "base64": data = b64decode(text) @@ -193,7 +190,6 @@ def convert_to_bool(value: Optional[Union[str, int, float]] = None) -> bool: Returns: bool: The converted boolean. - """ value = str(value).strip() if value: @@ -219,7 +215,6 @@ def resolve_to_class(value: str, custom_types: dict) -> TiledClassType: Returns: TiledClassType: The converted python class type. - """ return deepcopy(custom_types[value]) @@ -238,7 +233,6 @@ def rotate( Returns: List[Point]: List of rotated points. - """ sin_t = sin(radians(angle)) cos_t = cos(radians(angle)) @@ -336,7 +330,6 @@ def parse_properties(node: ElementTree.Element, customs: dict = None) -> dict: Returns: Dict: Dictionary of the properties, as set in the Tiled editor. - """ d = dict() for child in node.findall("properties"): @@ -382,7 +375,6 @@ def from_xml_string(cls, xml_string: str) -> TiledElement: Returns: TiledElement: The TiledElement from the xml string. - """ return cls().parse_xml(ElementTree.fromstring(xml_string)) @@ -423,7 +415,6 @@ def _set_properties( Reads the xml attributes and Tiled "properties" from an XML node and fills in the values into the object's dictionary. Names will be checked to make sure that they do not conflict with reserved names. - """ self._cast_and_set_attributes_from_node_items(node.items()) properties = parse_properties(node, customs) @@ -464,7 +455,6 @@ def __init__(self, name: str, members: list[dict]) -> None: Args: name (str): The name of the class type. members (List[dict]): The members of the class type. - """ self.name = name for member in members: @@ -490,7 +480,6 @@ def __init__( invert_y (bool): Invert the y axis. load_all_tiles (bool): Load all tile images, even if never used. allow_duplicate_names (bool): Allow duplicates in objects' metadata. - """ TiledElement.__init__(self) self.filename = filename @@ -505,19 +494,26 @@ def __init__( # allow duplicate names to be parsed and loaded TiledElement.allow_duplicate_names = kwargs.get("allow_duplicate_names", False) - self.layers: list[TiledLayer] = [] # all layers in proper order - self.tilesets: list[TiledTileset] = [] # TiledTileset objects - self.tile_properties = dict() # tiles that have properties - self.layernames = dict() - self.objects_by_id = dict() - self.objects_by_name = dict() + # all layers in proper order + self.layers: list[TiledLayer] = [] + # TiledTileset objects + self.tilesets: list[TiledTileset] = [] + # tiles that have properties + self.tile_properties: dict[int, dict[str, str]] = {} + self.layernames: dict[str, TiledLayer] = {} + self.objects_by_id: dict[str, TiledObject] = {} + self.objects_by_name: dict[int, TiledObject] = {} # only used tiles are actually loaded, so there will be a difference # between the GIDs in the Tiled map data (tmx) and the data in this # object and the layers. This dictionary keeps track of that. - self.gidmap = defaultdict(list) - self.imagemap = dict() # mapping of gid and trans flags to real gids - self.tiledgidmap = dict() # mapping of tiledgid to pytmx gid + self.gidmap: defaultdict[int, list[tuple[int, Optional[TileFlags]]]] = ( + defaultdict(list) + ) + # mapping of gid and trans flags to real gids + self.imagemap: dict[tuple[int, TileFlags], tuple[int, TileFlags]] = {} + # mapping of tiledgid to pytmx gid + self.tiledgidmap: dict[int, int] = {} self.maxgid = 1 # should be filled in by a loader function @@ -535,13 +531,13 @@ def __init__( self.hexsidelength = 0 self.staggeraxis = None self.staggerindex = None - self.background_color = None + self.background_color: Optional[str] = None self.nextobjectid = 0 self.custom_types = dict() # initialize the gid mapping - self.imagemap[(0, 0)] = 0 + self.imagemap[(0, empty_flags)] = (0, empty_flags) if custom_property_filename: self.parse_json(json.load(open(custom_property_filename))) @@ -556,8 +552,10 @@ def __repr__(self) -> str: def __iter__(self): return chain(self.layers, self.objects) - def _set_properties(self, node: ElementTree.Element) -> None: - TiledElement._set_properties(self, node) + def _set_properties( + self, node: ElementTree.Element, customs: Optional[dict] = None + ) -> None: + TiledElement._set_properties(self, node, customs) # TODO: make class/layer-specific type casting # layer height and width must be int, but TiledElement.set_properties() @@ -570,7 +568,6 @@ def parse_json(self, data: dict) -> None: Args: data (dict): Dictionary from JSON object to parse - """ for custom_type in data: if custom_type["type"] == "class": @@ -583,7 +580,6 @@ def parse_xml(self, node: ElementTree.Element) -> None: Args: node (ElementTree.Element): ElementTree xml node to parse. - """ self._set_properties(node) self.background_color = node.get("backgroundcolor", self.background_color) @@ -639,7 +635,6 @@ def reload_images(self) -> None: This method will use the image loader passed in the constructor to do the loading or will use a generic default, in which case no images will be loaded. - """ self.images = [None] * self.maxgid @@ -692,7 +687,7 @@ def reload_images(self) -> None: # load image layer images for layer in (i for i in self.layers if isinstance(i, TiledImageLayer)): source = getattr(layer, "source", None) - if source: + if source and self.filename: colorkey = getattr(layer, "trans", None) real_gid = len(self.images) gid = self.register_gid(real_gid) @@ -707,7 +702,7 @@ def reload_images(self) -> None: # was loaded from the tileset for real_gid, props in self.tile_properties.items(): source = props.get("source", None) - if source: + if source and self.filename: colorkey = props.get("trans", None) path = os.path.join(os.path.dirname(self.filename), source) loader = self.image_loader(path, colorkey) @@ -728,7 +723,6 @@ def get_tile_image(self, x: int, y: int, layer: int): Raises: TypeError: if coordinates are not integers. ValueError: if the coordinates are out of bounds, or GID not found. - """ if not (x >= 0 and y >= 0): raise ValueError( @@ -766,7 +760,6 @@ def get_tile_image_by_gid(self, gid: int): Raises: TypeError: if `gid` is not an integer. ValueError: if there is no image for this GID. - """ try: assert int(gid) >= 0 @@ -793,7 +786,6 @@ def get_tile_gid(self, x: int, y: int, layer: int) -> int: Raises: ValueError: If coordinates are out of bounds. - """ if not (x >= 0 and y >= 0 and layer >= 0): raise ValueError( @@ -809,7 +801,9 @@ def get_tile_gid(self, x: int, y: int, layer: int) -> int: logger.debug(msg.format(x, y, layer)) raise ValueError(msg.format(x, y, layer)) - def get_tile_properties(self, x: int, y: int, layer: int) -> Optional[dict]: + def get_tile_properties( + self, x: int, y: int, layer: int + ) -> Optional[dict[str, str]]: """Return the tile image GID for this location. Args: @@ -822,7 +816,6 @@ def get_tile_properties(self, x: int, y: int, layer: int) -> Optional[dict]: Raises: ValueError: If coordinates are out of bounds - """ if not (x >= 0 and y >= 0 and layer >= 0): raise ValueError( @@ -858,13 +851,12 @@ def get_tile_locations_by_gid(self, gid: int) -> Iterable[MapPoint]: Returns: Iterable[MapPoint]: (int, int, int) tuples, where the layer is index of the visible tile layers. - """ for l in self.visible_tile_layers: for x, y, _gid in [i for i in self.layers[l].iter_data() if i[2] == gid]: yield x, y, l - def get_tile_properties_by_gid(self, gid: int) -> Optional[dict]: + def get_tile_properties_by_gid(self, gid: int) -> Optional[dict[str, str]]: """Get the tile properties of a tile GID. Args: @@ -872,20 +864,18 @@ def get_tile_properties_by_gid(self, gid: int) -> Optional[dict]: Returns: Optional[dict]: Dictionary of properties for GID, or None. - """ try: return self.tile_properties[gid] except KeyError: return None - def set_tile_properties(self, gid: int, properties: dict) -> None: + def set_tile_properties(self, gid: int, properties: dict[str, str]) -> None: """Set the tile properties of a tile GID. Args: gid (int): GID. properties (dict): Python dictionary of properties for GID. - """ self.tile_properties[gid] = properties @@ -897,7 +887,6 @@ def get_tile_properties_by_layer(self, layer: int): Returns: ???: ??? - """ try: assert int(layer) >= 0 @@ -924,14 +913,14 @@ def add_layer( Args: layer (TiledLayer): The layer. - """ assert isinstance( layer, (TiledGroupLayer, TiledTileLayer, TiledImageLayer, TiledObjectGroup) ) self.layers.append(layer) - self.layernames[layer.name] = layer + if layer.name: + self.layernames[layer.name] = layer def add_tileset(self, tileset: TiledTileset) -> None: """Add a tileset to the map.""" @@ -949,7 +938,6 @@ def get_layer_by_name(self, name: str) -> TiledLayer: Raises: ValueError: if layer by name does not exist - """ try: return self.layernames[name] @@ -966,7 +954,6 @@ def get_object_by_id(self, obj_id: int) -> TiledObject: Returns: TiledObject: The found object. - """ return self.objects_by_id[obj_id] @@ -978,7 +965,6 @@ def get_object_by_name(self, name: str) -> TiledObject: Returns: TiledObject: The found object. - """ return self.objects_by_name[name] @@ -996,7 +982,6 @@ def get_tileset_from_gid(self, gid: int) -> TiledTileset: Raises: ValueError: if the tileset for gid is not found - """ try: tiled_gid = self.tiledgidmap[gid] @@ -1009,12 +994,11 @@ def get_tileset_from_gid(self, gid: int) -> TiledTileset: raise ValueError("Tileset not found") - def get_tile_colliders(self) -> Iterable[tuple[int, list[dict]]]: + def get_tile_colliders(self) -> Iterable[tuple[int, str]]: """Return iterator of (gid, dict) pairs of tiles with colliders. Returns: Iterable[Tuple[int, List[Dict]]]: The tile colliders. - """ for gid, props in self.tile_properties.items(): colliders = props.get("colliders") @@ -1030,7 +1014,6 @@ def objectgroups(self) -> Iterable[TiledObjectGroup]: Returns: Iterable[TiledObjectGroup]: ???. - """ return (layer for layer in self.layers if isinstance(layer, TiledObjectGroup)) @@ -1040,7 +1023,6 @@ def objects(self) -> Iterable[TiledObject]: Returns: Iterable[TiledObject]: All objects associated with the map. - """ return chain(*self.objectgroups) @@ -1050,7 +1032,6 @@ def visible_layers(self) -> Iterable[TiledLayer]: Returns: Iterable[TiledLayer]: Layer objects that are set "visible". - """ return (l for l in self.layers if l.visible) @@ -1061,7 +1042,6 @@ def visible_tile_layers(self) -> Iterable[TiledTileLayer]: Returns: Iterable[TiledTileLayer]: A list of layer indexes. - """ return ( i @@ -1075,7 +1055,6 @@ def visible_object_groups(self) -> Iterable[TiledObjectGroup]: Returns: Iterable[TiledObjectGroup]: A list of object group indexes that are set to "visible". - """ return ( i @@ -1096,10 +1075,9 @@ def register_gid( Returns: int: New or existing GID for pytmx use. - """ if flags is None: - flags = TileFlags(0, 0, 0) + flags = empty_flags if tiled_gid: try: @@ -1128,7 +1106,6 @@ def register_gid_check_flags( Returns: int: New or existing GID for pytmx use. - """ # NOTE: the register* methods are getting really spaghetti-like if tiled_gid == 0: @@ -1138,7 +1115,9 @@ def register_gid_check_flags( else: return self.register_gid(*decode_gid(tiled_gid)) - def map_gid(self, tiled_gid: int) -> Optional[list[int]]: + def map_gid( + self, tiled_gid: int + ) -> Optional[list[tuple[int, Optional[TileFlags]]]]: """Used to lookup a GID read from a TMX file's data. Args: @@ -1146,10 +1125,9 @@ def map_gid(self, tiled_gid: int) -> Optional[list[int]]: Returns: Optional[List[int]]: ??? - """ try: - return self.gidmap[int(tiled_gid)] + return self.gidmap[tiled_gid] except KeyError: return None except TypeError: @@ -1157,10 +1135,8 @@ def map_gid(self, tiled_gid: int) -> Optional[list[int]]: logger.debug(msg) raise TypeError(msg) - def map_gid2(self, tiled_gid: int) -> list[tuple[int, Optional[int]]]: + def map_gid2(self, tiled_gid: int) -> list[tuple[int, Optional[TileFlags]]]: """WIP. need to refactor the gid code""" - tiled_gid = int(tiled_gid) - # gidmap is a default dict, so cannot trust to raise KeyError if tiled_gid in self.gidmap: return self.gidmap[tiled_gid] @@ -1174,7 +1150,6 @@ class TiledTileset(TiledElement): External tilesets are supported. GID/ID's from Tiled are not guaranteed to be the same after loaded. - """ def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: @@ -1183,7 +1158,6 @@ def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: Args: parent (TiledMap): ???. node (ElementTree.Element): ???. - """ TiledElement.__init__(self) self.parent = parent @@ -1191,8 +1165,8 @@ def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: # defaults from the specification self.firstgid = 0 - self.source = None - self.name = None + self.source: Optional[str] = None + self.name: Optional[str] = None self.tilewidth = 0 self.tileheight = 0 self.spacing = 0 @@ -1201,13 +1175,13 @@ def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: self.columns = 0 # image properties - self.trans = None + self.trans: Optional[str] = None self.width = 0 self.height = 0 self.parse_xml(node) - def parse_xml(self, node: ElementTree.Element) -> TiledTileset: + def parse_xml(self, node: ElementTree.Element) -> "TiledTileset": """Parse a Tileset from ElementTree xml element. A bit of mangling is done here so that tilesets that have @@ -1218,14 +1192,13 @@ def parse_xml(self, node: ElementTree.Element) -> TiledTileset: Returns: TiledTileset: - """ # if true, then node references an external tileset source = node.get("source", None) if source: - if source[-4:].lower() == ".tsx": + if source[-4:].lower() == ".tsx" and self.parent.filename: # external tilesets don't save this, store it for later - self.firstgid = int(node.get("firstgid")) + self.firstgid = int(node.get("firstgid", 0)) # we need to mangle the path - tiled stores relative paths dirname = os.path.dirname(self.parent.filename) @@ -1255,7 +1228,7 @@ def parse_xml(self, node: ElementTree.Element) -> TiledTileset: # we store it separately in the parent (a TiledMap instance) register_gid = self.parent.register_gid for child in node.iter("tile"): - tiled_gid = int(child.get("id")) + tiled_gid = int(child.get("id", 0)) p = {k: types[k](v) for k, v in child.items()} p.update(parse_properties(child)) @@ -1267,26 +1240,26 @@ def parse_xml(self, node: ElementTree.Element) -> TiledTileset: # handle tiles that have their own image image = child.find("image") if image is None: - p["width"] = self.tilewidth - p["height"] = self.tileheight + p["width"] = str(self.tilewidth) + p["height"] = str(self.tileheight) else: - tile_source = image.get("source") + tile_source = image.get("source", "") # images are listed as relative to the .tsx file, not the .tmx file: if source and tile_source: tile_source = os.path.join(os.path.dirname(source), tile_source) p["source"] = tile_source - p["trans"] = image.get("trans", None) - p["width"] = image.get("width", None) - p["height"] = image.get("height", None) + p["trans"] = image.get("trans", "") + p["width"] = image.get("width", "0") + p["height"] = image.get("height", "0") # handle tiles with animations anim = child.find("animation") - frames = list() + frames: list[AnimationFrame] = [] p["frames"] = frames if anim is not None: for frame in anim.findall("frame"): - duration = int(frame.get("duration")) - gid = register_gid(int(frame.get("tileid")) + self.firstgid) + duration = int(frame.get("duration", 0)) + gid = register_gid(int(frame.get("tileid", 0)) + self.firstgid) frames.append(AnimationFrame(gid, duration)) for objgrp_node in child.findall("objectgroup"): @@ -1297,23 +1270,25 @@ def parse_xml(self, node: ElementTree.Element) -> TiledTileset: self.parent.set_tile_properties(gid, p) # handle the optional 'tileoffset' node - self.offset = node.find("tileoffset") - if self.offset is None: + _offset = node.find("tileoffset") + if _offset is None: self.offset = (0, 0) else: - self.offset = (self.offset.get("x", 0), self.offset.get("y", 0)) + _x = int(_offset.get("x", 0)) + _y = int(_offset.get("y", 0)) + self.offset = (_x, _y) image_node = node.find("image") if image_node is not None: self.source = image_node.get("source") # When loading from tsx, tileset image path is relative to the tsx file, not the tmx: - if source: + if source and self.source: self.source = os.path.join(os.path.dirname(source), self.source) self.trans = image_node.get("trans", None) - self.width = int(image_node.get("width")) - self.height = int(image_node.get("height")) + self.width = int(image_node.get("width", 0)) + self.height = int(image_node.get("height", 0)) return self @@ -1325,15 +1300,14 @@ def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: Args: parent (TiledMap): ???. node (ElementTree.Element): ???. - """ TiledElement.__init__(self) self.parent = parent - self.name = None + self.name: Optional[str] = None self.visible = True self.parse_xml(node) - def parse_xml(self, node: ElementTree.Element) -> TiledGroupLayer: + def parse_xml(self, node: ElementTree.Element) -> "TiledGroupLayer": """ Parse a TiledGroup layer from ElementTree xml node. @@ -1342,7 +1316,6 @@ def parse_xml(self, node: ElementTree.Element) -> TiledGroupLayer: Returns: TiledGroupLayer: The parsed TiledGroup layer. - """ self._set_properties(node) self.name = node.get("name", None) @@ -1353,7 +1326,6 @@ class TiledTileLayer(TiledElement): """Represents a TileLayer. To just get the tile images, use TiledTileLayer.tiles(). - """ def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: @@ -1362,7 +1334,7 @@ def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: self.data: list[list[int]] = [] # defaults from the specification - self.name = None + self.name: Optional[str] = None self.width = 0 self.height = 0 self.opacity = 1.0 @@ -1380,25 +1352,25 @@ def iter_data(self) -> Iterable[tuple[int, int, int]]: Returns: Iterable[Tuple[int, int, int]]: Iterator of X, Y, GID tuples for each tile in the layer. - """ for y, row in enumerate(self.data): for x, gid in enumerate(row): yield x, y, gid - def tiles(self) -> Generator[tuple[int, int, Any], Any, None]: + def tiles(self): """Yields X, Y, Image tuples for each tile in the layer. Yields: ???: Iterator of X, Y, Image tuples for each tile in the layer - """ images = self.parent.images for x, y, gid in [i for i in self.iter_data() if i[2]]: yield x, y, images[gid] - def _set_properties(self, node) -> None: - TiledElement._set_properties(self, node) + def _set_properties( + self, node: ElementTree.Element, customs: Optional[dict] = None + ) -> None: + TiledElement._set_properties(self, node, customs) # TODO: make class/layer-specific type casting # layer height and width must be int, but TiledElement.set_properties() @@ -1406,7 +1378,7 @@ def _set_properties(self, node) -> None: self.height = int(self.height) self.width = int(self.width) - def parse_xml(self, node: ElementTree.Element) -> TiledTileLayer: + def parse_xml(self, node: ElementTree.Element) -> "TiledTileLayer": """Parse a Tile Layer from ElementTree xml node. Args: @@ -1414,32 +1386,31 @@ def parse_xml(self, node: ElementTree.Element) -> TiledTileLayer: Returns: TiledTileLayer: The parsed tile layer. - """ self._set_properties(node) + data_node = node.find("data") + if data_node is None: + raise ValueError("Missing 'data' node in tile layer") + chunk_nodes = data_node.findall("chunk") if chunk_nodes: - msg = "TMX map size: infinite is not supported." - logger.error(msg) - raise Exception + logger.error("TMX map size: infinite is not supported.") + raise Exception("Infinite map size is not supported") - child = data_node.find("tile") - if child is not None: + if data_node.find("tile") is not None: raise ValueError( "XML tile elements are no longer supported. Must use base64 or csv map formats." ) - temp = [ - self.parent.register_gid_check_flags(gid) - for gid in unpack_gids( - text=data_node.text.strip(), - encoding=data_node.get("encoding", None), - compression=data_node.get("compression", None), - ) - ] + gids = unpack_gids( + text=data_node.text.strip(), + encoding=data_node.get("encoding", None), + compression=data_node.get("compression", None), + ) + registered_gids = [self.parent.register_gid_check_flags(gid) for gid in gids] - self.data = reshape_data(temp, self.width) + self.data = reshape_data(registered_gids, self.width) return self @@ -1447,7 +1418,6 @@ class TiledObjectGroup(TiledElement, list): """Represents a Tiled ObjectGroup Supports any operation of a normal list. - """ def __init__( @@ -1471,12 +1441,11 @@ def __init__( self.parse_xml(node) - def parse_xml(self, node: ElementTree.Element) -> TiledObjectGroup: + def parse_xml(self, node: ElementTree.Element) -> "TiledObjectGroup": """Parse an Object Group from ElementTree xml node Args: node (ElementTree.Element): Node to parse. - """ self._set_properties(node, self.custom_types) self.extend( @@ -1491,7 +1460,6 @@ class TiledObject(TiledElement): """Represents any Tiled Object. Supported types: Box, Ellipse, Tile Object, Polyline, Polygon. - """ def __init__( @@ -1521,18 +1489,17 @@ def __init__( self.parse_xml(node) @property - def image(self): + def image(self) -> Optional[Any]: """Image for the object, if assigned. Returns: ???: The image object type will depend on the loader (ie. pygame.Surface). - """ if self.gid: return self.parent.images[self.gid] return None - def parse_xml(self, node: ElementTree.Element) -> TiledObject: + def parse_xml(self, node: ElementTree.Element) -> "TiledObject": """Parse an Object from ElementTree xml node. Args: @@ -1540,10 +1507,9 @@ def parse_xml(self, node: ElementTree.Element) -> TiledObject: Returns: TiledObject: The parsed xml node. - """ - def read_points(text): + def read_points(text: Optional[str]) -> tuple[tuple[float, ...], ...]: """ Parse a text string of float tuples and return [(x,...),...] @@ -1608,44 +1574,43 @@ class TiledImageLayer(TiledElement): """Represents Tiled Image Layer. The image associated with this layer will be loaded and assigned a GID. - """ def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: TiledElement.__init__(self) self.parent = parent - self.source = None - self.trans = None + self.source: Optional[str] = None + self.trans: Optional[str] = None self.gid = 0 # defaults from the specification - self.name = None + self.name: Optional[str] = None self.opacity = 1 self.visible = True self.parse_xml(node) @property - def image(self): + def image(self) -> Optional[Any]: """Image for the object, if assigned. Returns: ???: the image object type will depend on the loader (ie. pygame.Surface). - """ if self.gid: return self.parent.images[self.gid] return None - def parse_xml(self, node: ElementTree.Element): + def parse_xml(self, node: ElementTree.Element) -> "TiledImageLayer": """Parse an Image Layer from ElementTree xml node.""" self._set_properties(node) self.name = node.get("name", None) - self.opacity = node.get("opacity", self.opacity) + self.opacity = int(node.get("opacity", self.opacity)) self.visible = bool(node.get("visible", self.visible)) image_node = node.find("image") - self.source = image_node.get("source", None) - self.trans = image_node.get("trans", None) + if image_node is not None: + self.source = image_node.get("source", None) + self.trans = image_node.get("trans", None) return self diff --git a/pytmx/util_pyglet.py b/pytmx/util_pyglet.py index 6e2334a..0a791c5 100644 --- a/pytmx/util_pyglet.py +++ b/pytmx/util_pyglet.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Copyright (C) 2012-2024, Leif Theden +Copyright (C) 2012-2025, Leif Theden This file is part of pytmx. diff --git a/pytmx/util_pysdl2.py b/pytmx/util_pysdl2.py index 7c2de85..97fb940 100644 --- a/pytmx/util_pysdl2.py +++ b/pytmx/util_pysdl2.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Copyright (C) 2012-2024, Leif Theden +Copyright (C) 2012-2025, Leif Theden This file is part of pytmx. diff --git a/setup.py b/setup.py deleted file mode 100755 index ff36f24..0000000 --- a/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -# pip install wheel -# python3 setup.py sdist bdist_wheel -# python3 -m twine upload --repository pypi dist/* -from setuptools import setup - -setup()