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 9a20edc..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 * @@ -29,20 +31,20 @@ 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 """ - 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 @@ -55,7 +57,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 +86,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 +97,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,26 +129,28 @@ 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: + 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) - 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 +167,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 +193,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 @@ -208,8 +219,7 @@ def run(self): if __name__ == "__main__": - import glob - import os.path + import time import pytmx @@ -218,22 +228,28 @@ def run(self): 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 5e8c375..64e2c32 100644 --- a/apps/pygame_demo.py +++ b/apps/pygame_demo.py @@ -172,7 +172,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: 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 fec4feb..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. @@ -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 @@ -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 7d40621..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 @@ -32,7 +33,7 @@ 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 @@ -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 @@ -136,10 +135,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: @@ -148,7 +144,6 @@ def reshape_data( Returns: List[List[int]]: 2D nested list object. - """ return [gids[i : i + width] for i in range(0, len(gids), width)] @@ -167,7 +162,6 @@ def unpack_gids( Returns: List[int]: List of all the GIDs in the layer. - """ if encoding == "base64": data = b64decode(text) @@ -196,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: @@ -222,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]) @@ -241,7 +233,6 @@ def rotate( Returns: List[Point]: List of rotated points. - """ sin_t = sin(radians(angle)) cos_t = cos(radians(angle)) @@ -339,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"): @@ -385,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)) @@ -418,13 +407,14 @@ 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 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) @@ -449,7 +439,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: @@ -465,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: @@ -491,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 @@ -506,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() # all layers in proper order - self.tilesets = list() # 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 @@ -536,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))) @@ -550,15 +545,17 @@ 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 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() @@ -571,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": @@ -584,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) @@ -640,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 @@ -693,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) @@ -708,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) @@ -729,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( @@ -767,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 @@ -794,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( @@ -810,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: @@ -823,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( @@ -859,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: @@ -873,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 @@ -898,7 +887,6 @@ def get_tile_properties_by_layer(self, layer: int): Returns: ???: ??? - """ try: assert int(layer) >= 0 @@ -925,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.""" @@ -950,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] @@ -967,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] @@ -979,7 +965,6 @@ def get_object_by_name(self, name: str) -> TiledObject: Returns: TiledObject: The found object. - """ return self.objects_by_name[name] @@ -997,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] @@ -1010,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") @@ -1031,7 +1014,6 @@ def objectgroups(self) -> Iterable[TiledObjectGroup]: Returns: Iterable[TiledObjectGroup]: ???. - """ return (layer for layer in self.layers if isinstance(layer, TiledObjectGroup)) @@ -1041,17 +1023,15 @@ def objects(self) -> Iterable[TiledObject]: Returns: Iterable[TiledObject]: All objects associated with the map. - """ 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". """ return (l for l in self.layers if l.visible) @@ -1062,7 +1042,6 @@ def visible_tile_layers(self) -> Iterable[TiledTileLayer]: Returns: Iterable[TiledTileLayer]: A list of layer indexes. - """ return ( i @@ -1076,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 @@ -1097,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: @@ -1129,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: @@ -1139,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: @@ -1147,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: @@ -1158,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] @@ -1175,16 +1150,14 @@ 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, node) -> None: + def __init__(self, parent: TiledMap, node: ElementTree.Element) -> None: """Represents a Tiled Tileset Args: - parent (???): ???. + parent (TiledMap): ???. node (ElementTree.Element): ???. - """ TiledElement.__init__(self) self.parent = parent @@ -1192,8 +1165,8 @@ def __init__(self, parent, node) -> 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 @@ -1202,7 +1175,7 @@ def __init__(self, parent, node) -> None: self.columns = 0 # image properties - self.trans = None + self.trans: Optional[str] = None self.width = 0 self.height = 0 @@ -1219,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) @@ -1256,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)) @@ -1268,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"): @@ -1298,43 +1270,44 @@ 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 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.name: Optional[str] = None + 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. @@ -1343,7 +1316,6 @@ def parse_xml(self, node) -> "TiledGroupLayer": Returns: TiledGroupLayer: The parsed TiledGroup layer. - """ self._set_properties(node) self.name = node.get("name", None) @@ -1354,16 +1326,15 @@ class TiledTileLayer(TiledElement): """Represents a TileLayer. To just get the tile images, use TiledTileLayer.tiles(). - """ - 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 + self.name: Optional[str] = None self.width = 0 self.height = 0 self.opacity = 1.0 @@ -1373,7 +1344,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]]: @@ -1381,7 +1352,6 @@ 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): @@ -1392,14 +1362,15 @@ def tiles(self): 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() @@ -1415,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 @@ -1448,10 +1418,14 @@ class TiledObjectGroup(TiledElement, list): """Represents a Tiled ObjectGroup Supports any operation of a normal 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 +1433,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 @@ -1472,7 +1446,6 @@ def parse_xml(self, node: ElementTree.Element) -> "TiledObjectGroup": Args: node (ElementTree.Element): Node to parse. - """ self._set_properties(node, self.custom_types) self.extend( @@ -1487,10 +1460,14 @@ class TiledObject(TiledElement): """Represents any Tiled Object. Supported types: Box, Ellipse, Tile Object, Polyline, Polygon. - """ - 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 +1481,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 @@ -1512,12 +1489,11 @@ def __init__(self, parent, node, custom_types) -> None: 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] @@ -1531,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,...),...] @@ -1599,51 +1574,50 @@ class TiledImageLayer(TiledElement): """Represents Tiled Image Layer. The image associated with this layer will be loaded and assigned a GID. - """ - 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 - 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 = 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.visible = node.get("visible", self.visible) + 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 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)) 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()