diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..1747dc6 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,586 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins=pylint.extensions.bad_builtin, + pylint.extensions.check_elif, + pylint.extensions.comparetozero, + pylint.extensions.docparams, + pylint.extensions.docstyle, + pylint.extensions.emptystring, + pylint.extensions.overlapping_exceptions, + pylint.extensions.redefined_variable_type + +accept-no-param-doc=n +accept-no-raise-doc=n +accept-no-return-doc=n +accept-no-yields-doc=n +default-docstring-type=google + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + c0330 #https://github.com/PyCQA/pylint/issues/289 + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + x, + y, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=79 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.style.yapf b/.style.yapf deleted file mode 100644 index 5666f47..0000000 --- a/.style.yapf +++ /dev/null @@ -1,3 +0,0 @@ -[style] -based_on_style = google -column_limit = 78 diff --git a/README.md b/README.md index 81bc1d0..0fd8ba6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # pytiled_parser Python Library for parsing Tiled Map Editor maps. + +NOT READY FOR USE diff --git a/dev_requirements.txt b/dev_requirements.txt index de3f832..0f6ddbb 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,4 +1,5 @@ -e . +black pytest sphinx coverage diff --git a/make.sh b/make.sh index 529ac3f..5ae6d85 100755 --- a/make.sh +++ b/make.sh @@ -11,5 +11,5 @@ do pip3 install $file done # sphinx-build -b html doc doc/build/html -coverage run --source arcade setup.py test +coverage run --source pytiled_parser setup.py test coverage report -m diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..af3f209 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,21 @@ +# Global options: + +[mypy] +python_version = 3.6 +warn_unused_configs = False +disallow_any_unimported = True +disallow_any_expr = True +disallow_any_decorated = True +disallow_any_explicit = True +disallow_any_generics = True +disallow_subclassing_any = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +check_untyped_defs = True +disallow_untyped_decorators = True +warn_return_any = True + +# Per-module options: +[mypy-tests.*] +ignore_errors = True diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index bcfa056..d58a1b1 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -1,4 +1,4 @@ -from . import utilities -from . import objects +"""init for pytiled_parser""" +from . import objects, utilities from .xml_parser import parse_tile_map diff --git a/pytiled_parser/objects.py b/pytiled_parser/objects.py index 4526059..42daa6c 100644 --- a/pytiled_parser/objects.py +++ b/pytiled_parser/objects.py @@ -1,27 +1,22 @@ -""" -pytiled_parser objects for Tiled maps. +"""pytiled_parser objects for Tiled maps. """ -import dataclasses -import functools -import re +# pylint: disable=too-few-public-methods -from collections import OrderedDict from pathlib import Path +from typing import Dict, List, NamedTuple, Optional, Union -import xml.etree.ElementTree as etree - -from typing import NamedTuple, Union, Optional, List, Dict +import attr class Color(NamedTuple): """Color object. Attributes: - :red (int): Red, between 1 and 255. - :green (int): Green, between 1 and 255. - :blue (int): Blue, between 1 and 255. - :alpha (int): Alpha, between 1 and 255. + red (int): Red, between 1 and 255. + green (int): Green, between 1 and 255. + blue (int): Blue, between 1 and 255. + alpha (int): Alpha, between 1 and 255. """ red: int @@ -66,24 +61,22 @@ class Size(NamedTuple): height: Union[int, float] +@attr.s(auto_attribs=True) class Template: - """ - FIXME TODO - """ + """FIXME TODO""" -@dataclasses.dataclass +@attr.s(auto_attribs=True) class Chunk: - """ - Chunk object for infinite maps. + """Chunk object for infinite maps. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#chunk Attributes: - :location (OrderedPair): Location of chunk in tiles. - :width (int): The width of the chunk in tiles. - :height (int): The height of the chunk in tiles. - :layer_data (List[List(int)]): The global tile IDs in chunky + location (OrderedPair): Location of chunk in tiles. + width (int): The width of the chunk in tiles. + height (int): The height of the chunk in tiles. + layer_data (List[List(int)]): The global tile IDs in chunky according to row. """ @@ -93,34 +86,32 @@ class Chunk: chunk_data: List[List[int]] -@dataclasses.dataclass +@attr.s(auto_attribs=True) class Image: - """ - Image object. + """Image object. This module does not support embedded data in image elements. Attributes: - :source (Optional[str]): The reference to the tileset image file. + source (Optional[str]): The reference to the tileset image file. Not that this is a relative path compared to FIXME - :trans (Optional[Color]): Defines a specific color that is treated + trans (Optional[Color]): Defines a specific color that is treated as transparent. - :width (Optional[str]): The image width in pixels + width (Optional[str]): The image width in pixels (optional, used for tile index correction when the image changes). - :height (Optional[str]): The image height in pixels (optional). + height (Optional[str]): The image height in pixels (optional). """ source: str size: Optional[Size] = None - trans: Optional[Color] = None + trans: Optional[str] = None Properties = Dict[str, Union[int, float, Color, Path, str]] class Grid(NamedTuple): - """ - Contains info for isometric maps. + """Contains info for isometric maps. This element is only used in case of isometric orientation, and determines how tile overlays for terrain and collision information @@ -133,12 +124,11 @@ class Grid(NamedTuple): class Terrain(NamedTuple): - """ - Terrain object. + """Terrain object. Args: - :name (str): The name of the terrain type. - :tile (int): The local tile-id of the tile that represents the + name (str): The name of the terrain type. + tile (int): The local tile-id of the tile that represents the terrain visually. """ @@ -147,15 +137,14 @@ class Terrain(NamedTuple): class Frame(NamedTuple): - """ - Animation Frame object. + """Animation Frame object. This is only used as a part of an animation for Tile objects. Args: - :tile_id (int): The local ID of a tile within the parent tile set + tile_id (int): The local ID of a tile within the parent tile set object. - :duration (int): How long in milliseconds this frame should be + duration (int): How long in milliseconds this frame should be displayed before advancing to the next frame. """ @@ -163,19 +152,18 @@ class Frame(NamedTuple): duration: int -@dataclasses.dataclass +@attr.s(auto_attribs=True) class TileTerrain: - """ - Defines each corner of a tile by Terrain index in + """Defines each corner of a tile by Terrain index in 'TileSet.terrain_types'. Defaults to 'None'. 'None' means that corner has no terrain. Attributes: - :top_left (Optional[int]): Top left terrain type. - :top_right (Optional[int]): Top right terrain type. - :bottom_left (Optional[int]): Bottom left terrain type. - :bottom_right (Optional[int]): Bottom right terrain type. + top_left (Optional[int]): Top left terrain type. + top_right (Optional[int]): Top right terrain type. + bottom_left (Optional[int]): Bottom left terrain type. + bottom_right (Optional[int]): Bottom right terrain type. """ top_left: Optional[int] = None @@ -184,7 +172,7 @@ class TileTerrain: bottom_right: Optional[int] = None -@dataclasses.dataclass +@attr.s(auto_attribs=True, kw_only=True) class Layer: """Class that all layers inherret from. @@ -207,7 +195,7 @@ class Layer: for more info. """ - id: int + id_: int name: str offset: Optional[OrderedPair] @@ -216,15 +204,14 @@ class Layer: LayerData = Union[List[List[int]], List[Chunk]] -""" -The tile data for one layer. +"""The tile data for one layer. Either a 2 dimensional array of integers representing the global tile IDs for a map layer, or a lists of chunks for an infinite map layer. """ -@dataclasses.dataclass +@attr.s(auto_attribs=True, kw_only=True) class TileLayer(Layer): """Tile map layer containing tiles. @@ -242,15 +229,34 @@ class TileLayer(Layer): data: LayerData -@dataclasses.dataclass -class _TiledObjectBase: - id: int +@attr.s(auto_attribs=True, kw_only=True) +class TiledObject: + """TiledObject object. + + See: + https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object + + Args: + id_ (int): Unique ID of the object. Each object that is placed on a + map gets a unique id. Even if an object was deleted, no object + gets the same ID. + gid (Optional[int]): Global tiled object ID + location (OrderedPair): The location of the object in pixels. + size (Size): The width of the object in pixels + (default: (0, 0)). + rotation (int): The rotation of the object in degrees clockwise + (default: 0). + opacity (int): The opacity of the object. (default: 255) + name (Optional[str]): The name of the object. + type (Optional[str]): The type of the object. + properties (Properties): The properties of the TiledObject. + template Optional[Template]: A reference to a Template object FIXME + """ + + id_: int + gid: Optional[int] = None + location: OrderedPair - - -@dataclasses.dataclass -class _TiledObjectDefaults: - gid: int = None size: Size = Size(0, 0) rotation: int = 0 opacity: float = 1 @@ -262,36 +268,9 @@ class _TiledObjectDefaults: template: Optional[Template] = None -@dataclasses.dataclass -class TiledObject(_TiledObjectDefaults, _TiledObjectBase): - """ - TiledObject object. - - See: - https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object - - Args: - :id (int): Unique ID of the object. Each object that is placed on a - map gets a unique id. Even if an object was deleted, no object - gets the same ID. - :location (OrderedPair): The location of the object in pixels. - :size (Size): The width of the object in pixels - (default: (0, 0)). - :rotation (int): The rotation of the object in degrees clockwise - (default: 0). - :opacity (int): The opacity of the object. (default: 255) - :name (Optional[str]): The name of the object. - :type (Optional[str]): The type of the object. - :properties (Properties): The properties of the TiledObject. - :template Optional[Template]: A reference to a Template object - FIXME - """ - - -@dataclasses.dataclass +@attr.s() class RectangleObject(TiledObject): - """ - Rectangle shape defined by a point, width, and height. + """Rectangle shape defined by a point, width, and height. See: https://doc.mapeditor.org/en/stable/manual/objects/#insert-rectangle (objects in tiled are rectangles by default, so there is no specific @@ -299,79 +278,87 @@ class RectangleObject(TiledObject): """ -@dataclasses.dataclass +@attr.s() class ElipseObject(TiledObject): - """ - Elipse shape defined by a point, width, and height. + """Elipse shape defined by a point, width, and height. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#ellipse """ -@dataclasses.dataclass +@attr.s() class PointObject(TiledObject): - """ - Point defined by a point (x,y). + """Point defined by a point (x,y). See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#point """ -@dataclasses.dataclass -class _TileImageObjectBase(_TiledObjectBase): - gid: int - - -@dataclasses.dataclass -class TileImageObject(TiledObject, _TileImageObjectBase): - """ - Polygon shape defined by a set of connections between points. +@attr.s(auto_attribs=True, kw_only=True) +class TileImageObject(TiledObject): + """Polygon shape defined by a set of connections between points. See: https://doc.mapeditor.org/en/stable/manual/objects/#insert-tile Attributes: - :gid (int): Refference to a global tile id. + gid (int): Refference to a global tile id. """ - -@dataclasses.dataclass -class _PointsObjectBase(_TiledObjectBase): - points: List[OrderedPair] + gid: int -@dataclasses.dataclass -class PolygonObject(TiledObject, _PointsObjectBase): - """ - Polygon shape defined by a set of connections between points. +@attr.s(auto_attribs=True, kw_only=True) +class PolygonObject(TiledObject): + """Polygon shape defined by a set of connections between points. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polygon Attributes: - :points (List[OrderedPair]) + points (List[OrderedPair]): FIXME """ + points: List[OrderedPair] -@dataclasses.dataclass -class PolylineObject(TiledObject, _PointsObjectBase): - """ - Polyline defined by a set of connections between points. + +@attr.s(auto_attribs=True, kw_only=True) +class PolylineObject(TiledObject): + """Polyline defined by a set of connections between points. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline Attributes: - :points (List[Tuple[int, int]]): List of coordinates relative to \ + points (List[Tuple[int, int]]): List of coordinates relative to \ the location of the object. """ + points: List[OrderedPair] + + +@attr.s(auto_attribs=True, kw_only=True) +class TextObject(TiledObject): + """Text object with associated settings. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#text + and https://doc.mapeditor.org/en/stable/manual/objects/#insert-text + + Attributes: + font_family (str): The font family used (default: "sans-serif") + font_size (int): The size of the font in pixels. (default: 16) + wrap (bool): Whether word wrapping is enabled. (default: False) + color (Color): Color of the text. (default: #000000) + bold (bool): Whether the font is bold. (default: False) + italic (bool): Whether the font is italic. (default: False) + underline (bool): Whether the text is underlined. (default: False) + strike_out (bool): Whether the text is striked-out. (default: False) + kerning (bool): Whether kerning should be used while rendering the \ + text. (default: False) + horizontal_align (str): Horizontal alignment of the text \ + (default: "left") + vertical_align (str): Vertical alignment of the text (defalt: "top") + """ -@dataclasses.dataclass -class _TextObjectBase(_TiledObjectBase): text: str - - -@dataclasses.dataclass -class _TextObjectDefaults(_TiledObjectDefaults): font_family: str = "sans-serif" font_size: int = 16 wrap: bool = False @@ -385,38 +372,12 @@ class _TextObjectDefaults(_TiledObjectDefaults): vertical_align: str = "top" -@dataclasses.dataclass -class TextObject(TiledObject, _TextObjectDefaults, _TextObjectBase): - """ - Text object with associated settings. - - See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#text - and https://doc.mapeditor.org/en/stable/manual/objects/#insert-text - - Attributes: - :font_family (str): The font family used (default: “sans-serif”) - :font_size (int): The size of the font in pixels. (default: 16) - :wrap (bool): Whether word wrapping is enabled. (default: False) - :color (Color): Color of the text. (default: #000000) - :bold (bool): Whether the font is bold. (default: False) - :italic (bool): Whether the font is italic. (default: False) - :underline (bool): Whether the text is underlined. (default: False) - :strike_out (bool): Whether the text is striked-out. (default: False) - :kerning (bool): Whether kerning should be used while rendering the \ - text. (default: False) - :horizontal_align (str): Horizontal alignment of the text \ - (default: "left") - :vertical_align (str): Vertical alignment of the text (defalt: "top") - """ - - -@dataclasses.dataclass +@attr.s(auto_attribs=True, kw_only=True) class ObjectLayer(Layer): - """ - TiledObject Group Object. + """TiledObject Group Object. The object group is in fact a map layer, and is hence called \ - “object layer” in Tiled. + "object layer" in Tiled. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup @@ -439,10 +400,9 @@ class ObjectLayer(Layer): draw_order: Optional[str] = "topdown" -@dataclasses.dataclass +@attr.s(auto_attribs=True, kw_only=True) class LayerGroup(Layer): - """ - Layer Group. + """Layer Group. A LayerGroup can be thought of as a layer that contains layers (potentially including other LayerGroups). @@ -452,33 +412,64 @@ class LayerGroup(Layer): See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#group Attributes: - + Layers (Optional[List[Union["LayerGroup", Layer, ObjectLayer]]]): + Layers in group. """ layers: Optional[List[Union["LayerGroup", Layer, ObjectLayer]]] -@dataclasses.dataclass -class Hitbox: - """ Hitbox - """ - id: int - x: int - y: int - width: int - height: int - hitbox_type: str - points: str - - +@attr.s(auto_attribs=True) class TileSet: - pass + """Object for storing a TSX with all associated collision data. - -@dataclasses.dataclass -class Tile: + Args: + name (str): The name of this tileset. + max_tile_size (Size): The maximum size of a tile in this + tile set in pixels. + spacing (int): The spacing in pixels between the tiles in this + tileset (applies to the tileset image). + margin (int): The margin around the tiles in this tileset + (applies to the tileset image). + tile_count (int): The number of tiles in this tileset. + columns (int): The number of tile columns in the tileset. + For image collection tilesets it is editable and is used when + displaying the tileset. + grid (Grid): Only used in case of isometric orientation, and + determines how tile overlays for terrain and collision information + are rendered. + tileoffset (Optional[OrderedPair]): Used to specify an offset in + pixels when drawing a tile from the tileset. When not present, no + offset is applied. + image (Image): Used for spritesheet tile sets. + terrain_types (Dict[str, int]): List of of terrain types which + can be referenced from the terrain attribute of the tile object. + Ordered according to the terrain element's appearance in the TSX + file. + tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id. """ - Individual tile object. + + name: str + max_tile_size: Size + + spacing: Optional[int] = None + margin: Optional[int] = None + tile_count: Optional[int] = None + columns: Optional[int] = None + tile_offset: Optional[OrderedPair] = None + grid: Optional[Grid] = None + properties: Optional[Properties] = None + image: Optional[Image] = None + terrain_types: Optional[List[Terrain]] = None + tiles: Optional[Dict[int, "Tile"]] = None + + +TileSetDict = Dict[int, TileSet] + + +@attr.s(auto_attribs=True, kw_only=True) +class Tile: + """Individual tile object. Args: :id (int): The local tile ID within its tileset. @@ -489,68 +480,18 @@ class Tile: associated with it. """ - id: int - type: Optional[str] - terrain: Optional[TileTerrain] - animation: Optional[List[Frame]] - image: Optional[Image] - hitboxes: Optional[List[Hitbox]] - properties: Optional[List[Property]] - tileset: Optional[TileSet] + id_: int + type_: Optional[str] = None + terrain: Optional[TileTerrain] = None + animation: Optional[List[Frame]] = None + image: Optional[Image] = None + properties: Optional[List[Property]] = None + tileset: Optional[TileSet] = None -@dataclasses.dataclass -class TileSet: - """ - Object for storing a TSX with all associated collision data. - - Args: - :name (str): The name of this tileset. - :max_tile_size (Size): The maximum size of a tile in this - tile set in pixels. - :spacing (int): The spacing in pixels between the tiles in this - tileset (applies to the tileset image). - :margin (int): The margin around the tiles in this tileset - (applies to the tileset image). - :tile_count (int): The number of tiles in this tileset. - :columns (int): The number of tile columns in the tileset. - For image collection tilesets it is editable and is used when - displaying the tileset. - :grid (Grid): Only used in case of isometric orientation, and - determines how tile overlays for terrain and collision information - are rendered. - :tileoffset (Optional[OrderedPair]): Used to specify an offset in - pixels when drawing a tile from the tileset. When not present, no - offset is applied. - :image (Image): Used for spritesheet tile sets. - :terrain_types (Dict[str, int]): List of of terrain types which - can be referenced from the terrain attribute of the tile object. - Ordered according to the terrain element's appearance in the TSX - file. - :tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id. - """ - - name: str - max_tile_size: Size - spacing: Optional[int] - margin: Optional[int] - tile_count: Optional[int] - columns: Optional[int] - tile_offset: Optional[OrderedPair] - grid: Optional[Grid] - properties: Optional[Properties] - image: Optional[Image] - terrain_types: Optional[List[Terrain]] - tiles: Optional[Dict[int, Tile]] - - -TileSetDict = Dict[int, TileSet] - - -@dataclasses.dataclass +@attr.s(auto_attribs=True) class TileMap: - """ - Object for storing a TMX with all associated layers and properties. + """Object for storing a TMX with all associated layers and properties. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map @@ -560,8 +501,8 @@ class TileMap: :version (str): The TMX format version. :tiledversion (str): The Tiled version used to save the file. May be a date (for snapshot builds). - :orientation (str): Map orientation. Tiled supports “orthogonal”, - “isometric”, “staggered” and “hexagonal” + :orientation (str): Map orientation. Tiled supports "orthogonal", + "isometric", "staggered" and "hexagonal" :renderorder (str): The order in which tiles on tile layers are rendered. Valid values are right-down, right-up, left-down and left-up. In all cases, the map is drawn row-by-row. (only @@ -570,12 +511,12 @@ class TileMap: :tile_size (Size): The width of a tile. :infinite (bool): If the map is infinite or not. :hexsidelength (int): Only for hexagonal maps. Determines the width or - height (depending on the staggered axis) of the tile’s edge, in + height (depending on the staggered axis) of the tile's edge, in pixels. :stagger_axis (str): For staggered and hexagonal maps, determines - which axis (“x” or “y”) is staggered. + which axis ("x" or "y") is staggered. :staggerindex (str): For staggered and hexagonal maps, determines - whether the “even” or “odd” indexes along the staggered axis are + whether the "even" or "odd" indexes along the staggered axis are shifted. :backgroundcolor (##FIXME##): The background color of the map. :nextlayerid (int): Stores the next available ID for new layers. @@ -595,7 +536,7 @@ class TileMap: map_size: Size tile_size: Size infinite: bool - next_layer_id: int + next_layer_id: Optional[int] next_object_id: int tile_sets: TileSetDict @@ -607,22 +548,3 @@ class TileMap: background_color: Optional[str] = None properties: Optional[Properties] = None - - -""" -[22:16] <__m4ch1n3__> i would "[i for i in int_list if i < littler_then_value]" -[22:16] <__m4ch1n3__> it returns a list of integers below "littler_then_value" -[22:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 3] -[22:17] __m4ch1n3__: [1, 2, 1, 2] -[22:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 4] -[22:17] __m4ch1n3__: [1, 2, 3, 1, 2, 3] -[22:22] <__m4ch1n3__> !py3 max([i for i in [1,2,3,4,1,2,3,4] if i < 4]) -[22:22] __m4ch1n3__: 3 -[22:22] <__m4ch1n3__> max(...) would return the maximum of resulting list -[22:23] <__m4ch1n3__> !py3 max([i for i in [1, 10, 100] if i < 20]) -[22:23] __m4ch1n3__: 10 -[22:23] <__m4ch1n3__> !py3 max([i for i in [1, 10, 100] if i < 242]) -[22:23] __m4ch1n3__: 100 -[22:23] == markb1 [~mbiggers@45.36.35.206] has quit [Ping timeout: 245 seconds] -[22:23] <__m4ch1n3__> !py3 max(i for i in [1, 10, 100] if i < 242) -""" diff --git a/pytiled_parser/utilities.py b/pytiled_parser/utilities.py index 32d308e..b43c367 100644 --- a/pytiled_parser/utilities.py +++ b/pytiled_parser/utilities.py @@ -1,15 +1,22 @@ +"""Helper unitilies for pytiled_parser.""" + +from typing import List, Optional + import pytiled_parser.objects as objects def parse_color(color: str) -> objects.Color: - """ - Converts the color formats that Tiled uses into ones that Arcade accepts. + """Convert Tiled color format into Arcade's. + + Args: + color (str): Tiled formatted color string. Returns: :Color: Color object in the format that Arcade understands. """ - # strip initial '#' character - if not len(color) % 2 == 0: + # the actual part we care about is always an even number + if len(color) % 2: + # strip initial '#' character color = color[1:] if len(color) == 6: @@ -27,8 +34,25 @@ def parse_color(color: str) -> objects.Color: return objects.Color(red, green, blue, alpha) -def get_tile_by_gid(tile_sets: objects.TileSetDict, gid: int) -> objects.Tile: - """Gets Tile from a global tile ID. +def _get_tile_set_key(gid: int, tile_set_keys: List[int]) -> int: + """Gets tile set key given a tile GID. + + Args: + gid (int): Global ID of the tile. + tile_set_keys (List[int]): List of tile set keys. + + Returns: + int: The key of the tile set that contains the tile for the GID. + """ + + # credit to __m4ch1n3__ on ##learnpython for this idea + return max([key for key in tile_set_keys if key <= gid]) + + +def get_tile_by_gid( + gid: int, tile_sets: objects.TileSetDict +) -> Optional[objects.Tile]: + """Gets correct Tile for a given global ID. Args: tile_sets (objects.TileSetDict): TileSetDict from TileMap. @@ -36,10 +60,13 @@ def get_tile_by_gid(tile_sets: objects.TileSetDict, gid: int) -> objects.Tile: Returns: objects.Tile: The Tile object reffered to by the global tile ID. + None: If there is no objects.Tile object in the tile_set.tiles dict + for the associated gid. """ - for tileset_key, tileset in tile_sets.items(): - for tile_key, tile in tileset.tiles.items(): - tile_gid = tile.id + tileset_key - if tile_gid == gid: - return tile + tile_set_key = _get_tile_set_key(gid, list(tile_sets.keys())) + tile_set = tile_sets[tile_set_key] + + if tile_set.tiles is not None: + return tile_set.tiles.get(gid - tile_set_key) + return None diff --git a/pytiled_parser/xml_parser.py b/pytiled_parser/xml_parser.py index dce8fe0..63b0796 100644 --- a/pytiled_parser/xml_parser.py +++ b/pytiled_parser/xml_parser.py @@ -1,21 +1,33 @@ -import functools +"""Handle XML parsing for TMX files""" + import base64 +import functools import gzip import re -import zlib - -from pathlib import Path - -from typing import Callable, Dict, List, Optional, Tuple, Union import xml.etree.ElementTree as etree +import zlib +from pathlib import Path +from typing import Callable, Dict, List, Optional, Tuple, Union import pytiled_parser.objects as objects -import pytiled_parser.utilities as utilities def _decode_base64_data( data_text: str, layer_width: int, compression: Optional[str] = None ) -> List[List[int]]: + """Decode base64 data. + + Args: + data_text (str): Data to be decoded. + layer_width (int): Width of each layer in tiles. + compression (Optional[str]): The type of compression for the data. + + Raises: + ValueError: If compression type is unsupported. + + Returns: + :List[List[int]]: Tile grid. + """ tile_grid: List[List[int]] = [[]] unencoded_data = base64.b64decode(data_text) @@ -36,12 +48,12 @@ def _decode_base64_data( for byte in unzipped_data: int_value += byte << (byte_count * 8) byte_count += 1 - if byte_count % 4 == 0: + if not byte_count % 4: byte_count = 0 int_count += 1 tile_grid[row_count].append(int_value) int_value = 0 - if int_count % layer_width == 0: + if not int_count % layer_width: row_count += 1 tile_grid.append([]) @@ -52,7 +64,11 @@ def _decode_base64_data( def _decode_csv_data(data_text: str) -> List[List[int]]: """Decodes csv encoded layer data. - Credit: + Args: + data_text (str): Data to be decoded. + + Returns: + List[List[int]]: Tile grid. """ tile_grid = [] lines: List[str] = data_text.split("\n") @@ -73,29 +89,38 @@ def _decode_data( element: etree.Element, layer_width: int, encoding: str, - compression: Optional[str], + compression: Optional[str] = None, ) -> List[List[int]]: """Decodes data or chunk data. Args: - :element (Element): Element to have text decoded. - :layer_width (int): Number of tiles per column in this layer. Used + element (Element): Element to have text decoded. + layer_width (int): Number of tiles per column in this layer. Used for determining when to cut off a row when decoding base64 encoding layers. - :encoding (str): Encoding format of the layer data. - :compression (str): Compression format of the layer data. + encoding (str): Encoding format of the layer data. + compression (str): Compression format of the layer data. + + Raises: + ValueError: Encoding type is not supported. + ValueError: Compression is not supported for this encoding type. + ValueError: Compression type is not supported + AttributeError: No data in element. + + Returns: + List[List[int]]: Tile grid. """ # etree.Element.text comes with an appended and a prepended '\n' supported_encodings = ["base64", "csv"] if encoding not in supported_encodings: - raise ValueError("{encoding} is not a valid encoding") + raise ValueError(f"{encoding} is not a supported encoding") supported_compression = [None, "gzip", "zlib"] if compression is not None: if encoding != "base64": - raise ValueError("{encoding} does not support compression") + raise ValueError(f"{encoding} does not support compression") if compression not in supported_compression: - raise ValueError("{compression} is not a valid compression type") + raise ValueError(f"{compression} is not a supported compression") try: data_text: str = element.text # type: ignore @@ -108,19 +133,17 @@ def _decode_data( return _decode_base64_data(data_text, layer_width, compression) -def _parse_data( - element: etree.Element, layer_width: int -) -> objects.LayerData: +def _parse_data(element: etree.Element, layer_width: int) -> objects.LayerData: """Parses layer data. Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data. Args: - :element (Element): Data element to parse. - :width (int): Layer width. Used for base64 decoding. + element (Element): Data element to parse. + layer_width (int): Layer width. Used for base64 decoding. Returns: - :LayerData: Data object containing layer data or chunks of data. + LayerData: Data object containing layer data or chunks of data. """ encoding = element.attrib["encoding"] compression = None @@ -210,6 +233,9 @@ def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer: Args: element: The layer element to be parsed. + Raises: + ValueError: Element has no chile data element. + Returns: TileLayer: The tile layer object. """ @@ -226,7 +252,13 @@ def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer: raise ValueError(f"{element} has no child data element.") return objects.TileLayer( - id_, name, offset, opacity, properties, size, data + id_=id_, + name=name, + offset=offset, + opacity=opacity, + properties=properties, + size=size, + data=data, ) @@ -249,7 +281,7 @@ def _parse_objects( location_y = float(object_element.attrib["y"]) location = objects.OrderedPair(location_x, location_y) - tiled_object = objects.TiledObject(id_, location) + tiled_object = objects.TiledObject(id_=id_, location=location) try: tiled_object.gid = int(object_element.attrib["gid"]) @@ -259,12 +291,12 @@ def _parse_objects( try: width = float(object_element.attrib["width"]) except KeyError: - width = 0 + width = 0.0 try: height = float(object_element.attrib["height"]) except KeyError: - height = 0 + height = 0.0 tiled_object.size = objects.Size(width, height) @@ -303,12 +335,7 @@ def _parse_object_layer(element: etree.Element,) -> objects.ObjectLayer: """Parse the objectgroup element given. Args: - layer_type (objects.LayerType): - id: The id of the layer. - name: The name of the layer. - offset: The offset of the layer. - opacity: The opacity of the layer. - properties: The Properties object of the layer. + element (etree.Element): Element to be parsed. Returns: ObjectLayer: The object layer object. @@ -330,14 +357,14 @@ def _parse_object_layer(element: etree.Element,) -> objects.ObjectLayer: pass return objects.ObjectLayer( - id_, - name, - offset, - opacity, - properties, - tiled_objects, - color, - draw_order, + id_=id_, + name=name, + offset=offset, + opacity=opacity, + properties=properties, + tiled_objects=tiled_objects, + color=color, + draw_order=draw_order, ) @@ -345,12 +372,7 @@ def _parse_layer_group(element: etree.Element,) -> objects.LayerGroup: """Parse the objectgroup element given. Args: - layer_type (objects.LayerType): - id: The id of the layer. - name: The name of the layer. - offset: The offset of the layer. - opacity: The opacity of the layer. - properties: The Properties object of the layer. + element (etree.Element): Element to be parsed. Returns: LayerGroup: The layer group object. @@ -359,7 +381,14 @@ def _parse_layer_group(element: etree.Element,) -> objects.LayerGroup: layers = _get_layers(element) - return objects.LayerGroup(id_, name, offset, opacity, properties, layers) + return objects.LayerGroup( + id_=id_, + name=name, + offset=offset, + opacity=opacity, + properties=properties, + layers=layers, + ) def _get_layer_parser( @@ -381,12 +410,11 @@ def _get_layer_parser( """ if layer_tag == "layer": return _parse_tile_layer - elif layer_tag == "objectgroup": + if layer_tag == "objectgroup": return _parse_object_layer - elif layer_tag == "group": + if layer_tag == "group": return _parse_layer_group - else: - return None + return None def _get_layers(map_element: etree.Element) -> List[objects.Layer]: @@ -417,6 +445,13 @@ def _parse_external_tile_set( """Parses an external tile set. Caches the results to speed up subsequent maps with identical tilesets. + + Args: + parent_dir (Path): Directory that TMX is in. + tile_set_element (etree.Element): Tile set element. + + Returns: + objects.Tileset: The tileset being parsed. """ source = Path(tile_set_element.attrib["source"]) tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot() @@ -424,87 +459,30 @@ def _parse_external_tile_set( return _parse_tile_set(tile_set_tree) -def _parse_points(point_string: str) -> List[objects.OrderedPair]: - str_pairs = point_string.split(" ") - - points = [] - for str_pair in str_pairs: - xys = str_pair.split(",") - x = float(xys[0]) - y = float(xys[1]) - points.append((x, y)) - - return points - - -def _parse_hitboxes(element: etree.Element) -> List[objects.TiledObject]: - """Parses all hitboxes for a given tile.""" - hitbox_elements = element.findall("./object") - - hitboxes = [] - for hitbox_element in hitbox_elements: - - id_ = None - if "id" in hitbox_element.attrib: - id_ = hitbox_element.attrib["id"] - - x = None - if "x" in hitbox_element.attrib: - x = float(hitbox_element.attrib["x"]) - - y = None - if "y" in hitbox_element.attrib: - y = float(hitbox_element.attrib["y"]) - - width = None - if "width" in hitbox_element.attrib: - width = float(hitbox_element.attrib["width"]) - - height = None - if "height" in hitbox_element.attrib: - height = float(hitbox_element.attrib["height"]) - - # Default to rectangle as the type - hitbox_type = "Rectangle" - points = None - - child = hitbox_element.findall("polygon") - if child: - hitbox_type = "Polygon" - points = _parse_points(child[0].attrib["points"]) - child = hitbox_element.findall("ellipse") - if child: - hitbox_type = "Ellipse" - child = hitbox_element.findall("point") - if child: - hitbox_type = "Point" - child = hitbox_element.findall("polyline") - if child: - hitbox_type = "Polyline" - points = _parse_points(child[0].attrib["points"]) - - hitbox = objects.Hitbox(id_, x, y, width, height, hitbox_type, points) - hitboxes.append(hitbox) - - return hitboxes - - def _parse_tiles( tile_element_list: List[etree.Element] ) -> Dict[int, objects.Tile]: + """Parse a list of tile elements. + + Args: + tile_element_list (List[etree.Element]): List of tile elements. + + Returns: + Dict[int, objects.Tile]: Dictionary containing Tile objects by their + ID. + """ tiles: Dict[int, objects.Tile] = {} for tile_element in tile_element_list: # id is not optional id_ = int(tile_element.attrib["id"]) # optional attributes - tile_type = None + _type = None try: - tile_type = tile_element.attrib["type"] + _type = tile_element.attrib["type"] except KeyError: pass - tile_terrain = None try: tile_terrain_attrib = tile_element.attrib["terrain"] except KeyError: @@ -520,11 +498,11 @@ def _parse_tiles( terrain_list: List[Optional[int]] = [] # each index in terrain_list_attrib refers to a corner for corner in terrain_list_attrib: - if corner == "": + if not corner: terrain_list.append(None) else: terrain_list.append(int(corner)) - tile_terrain = objects.TileTerrain(*terrain_list) + terrain = objects.TileTerrain(*terrain_list) # tile element optional sub-elements properties: Optional[List[objects.Property]] = None @@ -546,25 +524,26 @@ def _parse_tiles( frames = tile_animation_element.findall("./frame") for frame in frames: # tileid refers to the Tile.id of the animation frame - tile_id = int(frame.attrib["tileid"]) + id_ = int(frame.attrib["tileid"]) # duration is in MS. Should perhaps be converted to seconds. # FIXME: make decision duration = int(frame.attrib["duration"]) - animation.append(objects.Frame(tile_id, duration)) + animation.append(objects.Frame(id_, duration)) # if this is None, then the Tile is part of a spritesheet - tile_image = None - tile_image_element = tile_element.find("./image") - if tile_image_element is not None: - tile_image = _parse_image_element(tile_image_element) - - hitboxes = None - tile_hitboxes_element = tile_element.find("./objectgroup") - if tile_hitboxes_element is not None: - hitboxes = _parse_hitboxes(tile_hitboxes_element) + image = None + image_element = tile_element.find("./image") + if image_element is not None: + image = _parse_image_element(image_element) tiles[id_] = objects.Tile( - id_, tile_type, tile_terrain, animation, tile_image, hitboxes, properties, tileset=None + id_=id_, + type_=_type, + terrain=terrain, + animation=animation, + image=image, + properties=properties, + tileset=None, ) return tiles @@ -573,9 +552,13 @@ def _parse_tiles( def _parse_image_element(image_element: etree.Element) -> objects.Image: """Parse image element given. + Args: + image_element (etree.Element): Image element to be parsed. + Returns: - : Color in Arcade's preferred format. + objects.Image: FIXME what is this? """ + # FIXME doc image = objects.Image(image_element.attrib["source"]) width_attrib = image_element.attrib.get("width") @@ -597,14 +580,19 @@ def _parse_properties_element( ) -> objects.Properties: """Adds Tiled property to Properties dict. - Args: - :name (str): Name of property. - :property_type (str): Type of property. Can be string, int, float, + Each property element has a number of attributes: + name (str): Name of property. + property_type (str): Type of property. Can be string, int, float, bool, color or file. Defaults to string. - :value (str): The value of the property. + value (str): The value of the property. + + Args: + properties_element (etree.Element): Element to be parsed. Returns: - :Properties: Properties Dict object. + objects.Properties: Dict of the property values by property name. + + """ properties: objects.Properties = {} for property_element in properties_element.findall("./property"): @@ -641,8 +629,13 @@ def _parse_properties_element( def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet: - """ - Parses a tile set that is embedded into a TMX. + """Parses a tile set that is embedded into a TMX. + + Args: + tile_set_element (etree.Element): Element to be parsed. + + Returns: + objects.TileSet: Tile Set from element. """ # get all basic attributes name = tile_set_element.attrib["name"] @@ -728,13 +721,61 @@ def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet: # Go back and create a circular link so tiles know what tileset they are # part of. Needed for animation. - for my_id, my_tile in tiles.items(): - my_tile.tileset = tileset + for id_, tile in tiles.items(): + tile.tileset = tileset return tileset +def _get_tile_sets( + map_element: etree.Element, parent_dir: Path +) -> objects.TileSetDict: + """Get tile sets. + + Args: + map_element (etree.Element): Element to be parsed. + parent_dir (Path): Directory that TMX is in. + + Returns: + objects.TileSetDict: Dict of tile sets in the TMX by first_gid + """ + # parse all tilesets + tile_sets: objects.TileSetDict = {} + tile_set_element_list = map_element.findall("./tileset") + for tile_set_element in tile_set_element_list: + # tiled docs are ambiguous about the 'firstgid' attribute + # current understanding is for the purposes of mapping the layer + # data to the tile set data, add the 'firstgid' value to each + # tile 'id'; this means that the 'firstgid' is specific to each, + # tile set as they pertain to the map, not tile set specific as + # the tiled docs can make it seem + # 'firstgid' the key for each TileMap + first_gid = int(tile_set_element.attrib["firstgid"]) + try: + # check if is an external TSX + source = tile_set_element.attrib["source"] + except KeyError: + # the tile set is embedded + name = tile_set_element.attrib["name"] + tile_sets[first_gid] = _parse_tile_set(tile_set_element) + else: + # tile set is external + tile_sets[first_gid] = _parse_external_tile_set( + parent_dir, tile_set_element + ) + + return tile_sets + + def parse_tile_map(tmx_file: Union[str, Path]) -> objects.TileMap: + """Parse tile map. + + Args: + tmx_file (Union[str, Path]): TMX file to be parsed. + + Returns: + objects.TileMap: TileMap object generated from the TMX file provided. + """ # setting up XML parsing map_tree = etree.parse(str(tmx_file)) map_element = map_tree.getroot() @@ -746,43 +787,22 @@ def parse_tile_map(tmx_file: Union[str, Path]) -> objects.TileMap: tiled_version = map_element.attrib["tiledversion"] orientation = map_element.attrib["orientation"] render_order = map_element.attrib["renderorder"] + map_width = int(map_element.attrib["width"]) map_height = int(map_element.attrib["height"]) map_size = objects.Size(map_width, map_height) + tile_width = int(map_element.attrib["tilewidth"]) tile_height = int(map_element.attrib["tileheight"]) tile_size = objects.Size(tile_width, tile_height) infinite_attribute = map_element.attrib["infinite"] - infinite = True if infinite_attribute == "true" else False + infinite = bool(infinite_attribute == "true") next_layer_id = int(map_element.attrib["nextlayerid"]) next_object_id = int(map_element.attrib["nextobjectid"]) - # parse all tilesets - tile_sets: Dict[int, objects.TileSet] = {} - tile_set_element_list = map_element.findall("./tileset") - for tile_set_element in tile_set_element_list: - # tiled docs are ambiguous about the 'firstgid' attribute - # current understanding is for the purposes of mapping the layer - # data to the tile set data, add the 'firstgid' value to each - # tile 'id'; this means that the 'firstgid' is specific to each, - # tile set as they pertain to the map, not tile set specific as - # the tiled docs can make it seem - # 'firstgid' is saved beside each TileMap - firstgid = int(tile_set_element.attrib["firstgid"]) - try: - # check if is an external TSX - source = tile_set_element.attrib["source"] - except KeyError: - # the tile set in embedded - name = tile_set_element.attrib["name"] - tile_sets[firstgid] = _parse_tile_set(tile_set_element) - else: - # tile set is external - tile_sets[firstgid] = _parse_external_tile_set( - parent_dir, tile_set_element - ) + tile_sets = _get_tile_sets(map_element, parent_dir) layers = _get_layers(map_element) diff --git a/setup.py b/setup.py index d8b13db..dd337da 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ -from os import path import sys -from setuptools import setup +from os import path + +from setuptools import setup # type: ignore BUILD = 0 VERSION = "0.0.1" @@ -21,7 +22,7 @@ if __name__ == "__main__": license="MIT", url="https://github.com/Beefy-Swain/pytiled_parser", download_url="https://github.com/Beefy-Swain/pytiled_parser", - install_requires=["dataclasses"], + install_requires=["attrs"], packages=["pytiled_parser"], classifiers=[ "Development Status :: 1 - Planning", diff --git a/test/output.py b/test/output.py deleted file mode 100644 index 7190a30..0000000 --- a/test/output.py +++ /dev/null @@ -1,20 +0,0 @@ -{ 'background_color': None, - 'hex_side_length': None, - 'infinite': False, - 'layers': [ TileLayer(id=1, name='Tile Layer 1', offset=None, opacity=None, properties=None, size=Size(width=10, height=10), data=[[1, 2, 3, 4, 5, 6, 7, 8, 30, 30], [9, 10, 11, 12, 13, 14, 15, 16, 30, 30], [17, 18, 19, 20, 21, 22, 23, 24, 30, 30], [25, 26, 27, 28, 29, 30, 31, 32, 30, 30], [33, 34, 35, 36, 37, 38, 39, 40, 30, 30], [41, 42, 43, 44, 45, 46, 47, 48, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), - TileLayer(id=2, name='Tile Layer 2', offset=None, opacity=0.5, properties=None, size=Size(width=10, height=10), data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 46, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 6, 7, 7, 7, 7, 7, 8, 0], [0, 0, 14, 15, 15, 15, 15, 15, 16, 0], [0, 0, 22, 23, 23, 23, 23, 23, 24, 0]]), - LayerGroup(id=3, name='Group 1', offset=None, opacity=None, properties={'bool property': True}, layers=[TileLayer(id=5, name='Tile Layer 4', offset=OrderedPair(x=49.0, y=-50.0), opacity=None, properties=None, size=Size(width=10, height=10), data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 31, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), TileLayer(id=4, name='Tile Layer 3', offset=None, opacity=None, properties=None, size=Size(width=10, height=10), data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 2, 3, 0, 0, 0, 0, 0, 0, 0], [9, 10, 11, 0, 0, 0, 0, 0, 0, 0], [17, 18, 19, 0, 0, 0, 0, 0, 0, 0]])]), - ObjectLayer(id=6, name='Object Layer 1', offset=OrderedPair(x=4.66667, y=-4.33333), opacity=0.9, properties=None, tiled_objects=[TiledObject(id=1, location=OrderedPair(x=200.25, y=210.75), size=Size(width=47.25, height=25.0), rotation=15, opacity=1, name='rectangle 1', type='rectangle type', properties=None, template=None), TiledObject(id=2, location=OrderedPair(x=252.5, y=87.75), size=Size(width=0, height=0), rotation=-21, opacity=1, name='polygon 1', type='polygon type', properties=None, template=None), TiledObject(id=3, location=OrderedPair(x=198.75, y=102.5), size=Size(width=17.75, height=14.25), rotation=0, opacity=1, name='elipse 1', type='elipse type', properties=None, template=None), TiledObject(id=4, location=OrderedPair(x=174.25, y=186.0), size=Size(width=0, height=0), rotation=0, opacity=1, name='point 1', type='point type', properties=None, template=None), TiledObject(id=7, location=OrderedPair(x=11.3958, y=48.5833), size=Size(width=107.625, height=27.25), rotation=0, opacity=1, name='insert text 1', type='insert text type', properties=None, template=None), TiledObject(id=6, location=OrderedPair(x=47.25, y=72.5), size=Size(width=47.0, height=53.0), rotation=31, opacity=1, name='inserted tile 1', type='inserted tile type', properties={'tile property bool': True}, template=None), TiledObject(id=8, location=OrderedPair(x=144.667, y=112.0), size=Size(width=0, height=0), rotation=0, opacity=1, name='polyline 1', type='polyline type', properties=None, template=None), TiledObject(id=9, location=OrderedPair(x=69.8333, y=168.333), size=Size(width=0, height=0), rotation=0, opacity=1, name='polygon 2', type='polygon type', properties=None, template=None)], color=Color(red=0, green=0, blue=0, alpha=255), draw_order='index')], - 'map_size': Size(width=10, height=10), - 'next_layer_id': 16, - 'next_object_id': 10, - 'orientation': 'orthogonal', - 'parent_dir': PosixPath('/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data'), - 'properties': None, - 'render_order': 'right-down', - 'stagger_axis': None, - 'stagger_index': None, - 'tile_sets': { 1: TileSet(name='tile_set_image', max_tile_size=Size(width=32, height=32), spacing=1, margin=1, tile_count=48, columns=8, tile_offset=None, grid=None, properties=None, image=Image(source='images/tmw_desert_spacing.png', size=Size(width=265, height=199), trans=None), terrain_types=None, tiles={})}, - 'tile_size': Size(width=32, height=32), - 'tiled_version': '1.2.3', - 'version': '1.2'} diff --git a/test/test_attr.py b/test/test_attr.py new file mode 100644 index 0000000..e3de7d1 --- /dev/null +++ b/test/test_attr.py @@ -0,0 +1,22 @@ +import attr + + +@attr.s(auto_attribs=True, kw_only=True) +class Foo: + x: int + y: int + + a: int = 5 + + +@attr.s(auto_attribs=True, kw_only=True) +class Bar(Foo): + z: int + + +foo = Foo(x=1, y=2) + +bar = Bar(x=1, y=2, z=3) + +print(foo) +print(bar) diff --git a/test/test_output.py b/test/test_output.py new file mode 100644 index 0000000..e6228d5 --- /dev/null +++ b/test/test_output.py @@ -0,0 +1,178 @@ +{ + "parent_dir": PosixPath( + "/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data" + ), + "version": "1.2", + "tiled_version": "1.2.3", + "orientation": "orthogonal", + "render_order": "right-down", + "map_size": Size(width=8, height=6), + "tile_size": Size(width=32, height=32), + "infinite": False, + "next_layer_id": 2, + "next_object_id": 1, + "tile_sets": { + 1: TileSet( + name="tile_set_image", + max_tile_size=Size(width=32, height=32), + spacing=1, + margin=1, + tile_count=48, + columns=8, + tile_offset=None, + grid=None, + properties=None, + image=Image( + source="images/tmw_desert_spacing.png", + size=Size(width=265, height=199), + trans=None, + ), + terrain_types=None, + tiles={ + 9: Tile( + id=9, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + TiledObject( + id=2, + location=OrderedPair(x=1.0, y=1.0), + size=Size(width=32.0, height=32.0), + rotation=1, + opacity=1, + name="wall", + type="rectangle type", + properties=None, + template=None, + ) + ], + ), + 19: Tile( + id=19, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + TiledObject( + id=1, + location=OrderedPair(x=32.0, y=1.0), + size=Size(width=0, height=0), + rotation=1, + opacity=1, + name="wall corner", + type="polygon type", + properties=None, + template=None, + ) + ], + ), + 20: Tile( + id=20, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + TiledObject( + id=1, + location=OrderedPair(x=1.45455, y=1.45455), + size=Size(width=0, height=0), + rotation=1, + opacity=1, + name="polyline", + type="polyline type", + properties=None, + template=None, + ) + ], + ), + 31: Tile( + id=31, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + TiledObject( + id=1, + location=OrderedPair(x=5.09091, y=2.54545), + size=Size(width=19.6364, height=19.2727), + rotation=1, + opacity=1, + name="rock 1", + type="elipse type", + properties=None, + template=None, + ), + TiledObject( + id=2, + location=OrderedPair(x=16.1818, y=22.0), + size=Size(width=8.54545, height=8.36364), + rotation=-1, + opacity=1, + name="rock 2", + type="elipse type", + properties=None, + template=None, + ), + ], + ), + 45: Tile( + id=45, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + TiledObject( + id=1, + location=OrderedPair(x=14.7273, y=26.3636), + size=Size(width=0, height=0), + rotation=0, + opacity=1, + name="sign", + type="point type", + properties=None, + template=None, + ) + ], + ), + }, + ) + }, + "layers": [ + TileLayer( + id=1, + name="Tile Layer 1", + offset=None, + opacity=None, + properties=None, + size=Size(width=8, height=6), + data=[ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ], + ) + ], + "hex_side_length": None, + "stagger_axis": None, + "stagger_index": None, + "background_color": None, + "properties": { + "bool property - false": False, + "bool property - true": True, + "color property": "#ff49fcff", + "file property": PosixPath("../../../../../../../../var/log/syslog"), + "float property": 1.23456789, + "int property": 13, + "string property": "Hello, World!!", + }, +} +e diff --git a/test/test_tiled.py b/test/test_tiled.py index 235d112..589aa21 100644 --- a/test/test_tiled.py +++ b/test/test_tiled.py @@ -1,17 +1,11 @@ -import pprint import pickle - +import pprint from io import StringIO import pytiled_parser - -pp = pprint.PrettyPrinter(indent=4, compact=True, width=100) - -pp = pp.pprint - -MAP_NAME = "/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data/test_map_image_tile_set.tmx" +MAP_NAME = "/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data/test_map_simple_hitboxes.tmx" map = pytiled_parser.parse_tile_map(MAP_NAME) -pp(map.__dict__) +print(map.__dict__) diff --git a/tests/__init__.py b/tests/__init__.py index 0bb7ca5..3aeeee8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,3 @@ import pytest -pytest.main(["-x", "tests/unit2"]) +pytest.main(["--tb=native", "-s", "tests"]) diff --git a/tests/unit2/test_parser.py b/tests/test_parser.py similarity index 69% rename from tests/unit2/test_parser.py rename to tests/test_parser.py index 3d58ab1..360ddf3 100644 --- a/tests/unit2/test_parser.py +++ b/tests/test_parser.py @@ -1,11 +1,10 @@ -import pytest +"""Unit tests for pytiled_parser""" import xml.etree.ElementTree as etree - from contextlib import contextmanager -from typing import Callable, List, Optional, Tuple -from pytiled_parser import objects, xml_parser, utilities +import pytest +from pytiled_parser import objects, utilities, xml_parser @contextmanager @@ -14,25 +13,33 @@ def does_not_raise(): def _get_root_element(xml: str) -> etree.Element: + """Get root element of string of XML. + + Args: + xml (str): String of XML to be parsed into etree. + + Returns: + etree.Element: Root element of XML given. + """ return etree.fromstring(xml) -layer_data = [ +LAYER_DATA = [ ( '' - "", + + "", (int(1), "Tile Layer 1", None, None, None), ), ( '' - "", + + "", (int(2), "Tile Layer 2", None, float(0.5), None), ), ( '' - "" - "" - "", + + "" + + "" + + "", ( int(5), "Tile Layer 4", @@ -44,9 +51,9 @@ layer_data = [ ] -@pytest.mark.parametrize("xml,expected", layer_data) +@pytest.mark.parametrize("xml,expected", LAYER_DATA) def test_parse_layer(xml, expected, monkeypatch): - def mockreturn(properties): + def mockreturn(*args): return "properties" monkeypatch.setattr(xml_parser, "_parse_properties_element", mockreturn) @@ -65,9 +72,6 @@ def test_parse_layer(xml, expected, monkeypatch): ], ) def test_color_parsing(test_input, expected): - """ - Tiled has a few different types of color representations. - """ assert utilities.parse_color(test_input) == expected @@ -97,7 +101,7 @@ def test_decode_csv_data(data_csv, expected): assert xml_parser._decode_csv_data(data_csv) == expected -data_base64 = [ +DATA_BASE64 = [ ( "AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAhAAAAIgAAACMAAAAkAAAAJQAAACYAAAAnAAAAKAAAACkAAAAqAAAAKwAAACwAAAAtAAAALgAAAC8AAAAwAAAA", 8, @@ -158,13 +162,55 @@ data_base64 = [ @pytest.mark.parametrize( - "data_base64,width,compression,expected,raises", data_base64 + "data_base64,width,compression,expected,raises", DATA_BASE64 ) -def test_decode_base64_data( - data_base64, width, compression, expected, raises -): +def test_decode_base64_data(data_base64, width, compression, expected, raises): with raises: assert ( xml_parser._decode_base64_data(data_base64, width, compression) == expected ) + + +# FIXME: use hypothesis for this +def create_tile_set(qty_of_tiles): + tile_set = objects.TileSet(None, None) + + if qty_of_tiles == 0: + return tile_set + + tiles = {} + + for tile_id in range(qty_of_tiles): + tiles[tile_id] = objects.Tile(id_=tile_id) + + tile_set.tiles = tiles + + return tile_set + + +tile_by_gid = [ + (1, {1: create_tile_set(0)}, None), + (1, {1: create_tile_set(1)}, objects.Tile(id_=0)), + (1, {1: create_tile_set(2)}, objects.Tile(id_=0)), + (2, {1: create_tile_set(1)}, None), + (10, {1: create_tile_set(10)}, objects.Tile(id_=9)), + (1, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(id_=0)), + (2, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(id_=0)), + (3, {1: create_tile_set(1), 2: create_tile_set(1)}, None), + (15, {1: create_tile_set(5), 6: create_tile_set(10)}, objects.Tile(id_=9)), + ( + 20, + { + 1: create_tile_set(5), + 6: create_tile_set(10), + 16: create_tile_set(10), + }, + objects.Tile(id_=4), + ), +] + + +@pytest.mark.parametrize("gid,tile_sets,expected", tile_by_gid) +def test_get_tile_by_gid(gid, tile_sets, expected): + assert utilities.get_tile_by_gid(gid, tile_sets) == expected diff --git a/tests/unit2/test_pytiled_parser_integration.py.bax b/tests/test_pytiled_parser_integration.py similarity index 92% rename from tests/unit2/test_pytiled_parser_integration.py.bax rename to tests/test_pytiled_parser_integration.py index 904548a..081fb1d 100644 --- a/tests/unit2/test_pytiled_parser_integration.py.bax +++ b/tests/test_pytiled_parser_integration.py @@ -1,9 +1,7 @@ import os - from pathlib import Path import pytest - import pytiled_parser print(os.path.dirname(os.path.abspath(__file__))) @@ -15,13 +13,11 @@ def test_map_simple(): """ TMX with a very simple spritesheet tile set and some properties. """ - map = pytiled_parser.parse_tile_map( - Path("../test_data/test_map_simple.tmx") - ) + map = pytiled_parser.parse_tile_map(Path("test_data/test_map_simple.tmx")) # map # unsure how to get paths to compare propperly - assert str(map.parent_dir) == "../test_data" + assert str(map.parent_dir) == "test_data" assert map.version == "1.2" assert map.tiled_version == "1.2.3" assert map.orientation == "orthogonal" @@ -41,7 +37,7 @@ def test_map_simple(): assert map.properties == { "bool property - false": False, "bool property - true": True, - "color property": (0x49, 0xFC, 0xFF, 0xFF), + "color property": "#ff49fcff", "file property": Path("/var/log/syslog"), "float property": 1.23456789, "int property": 13, @@ -78,7 +74,7 @@ def test_map_simple(): [33, 34, 35, 36, 37, 38, 39, 40], [41, 42, 43, 44, 45, 46, 47, 48], ] - assert map.layers[0].id == 1 + assert map.layers[0].id_ == 1 assert map.layers[0].name == "Tile Layer 1" assert map.layers[0].offset == None assert map.layers[0].opacity == None