Merge branch 'master' of github.com:Beefy-Swain/pytiled_parser

# Conflicts:
#	pytiled_parser/objects.py
#	pytiled_parser/xml_parser.py
This commit is contained in:
Paul Vincent Craven
2019-07-12 10:16:11 -05:00
18 changed files with 1302 additions and 509 deletions

586
.pylintrc Normal file
View File

@@ -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*(# )?<?https?://\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

View File

@@ -1,3 +0,0 @@
[style]
based_on_style = google
column_limit = 78

View File

@@ -1,2 +1,4 @@
# pytiled_parser
Python Library for parsing Tiled Map Editor maps.
NOT READY FOR USE

View File

@@ -1,4 +1,5 @@
-e .
black
pytest
sphinx
coverage

View File

@@ -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

21
mypy.ini Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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 tiles 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] <codebot> __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] <codebot> __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] <codebot> __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] <codebot> __m4ch1n3__: 10
[22:23] <__m4ch1n3__> !py3 max([i for i in [1, 10, 100] if i < 242])
[22:23] <codebot> __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)
"""

View File

@@ -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

View File

@@ -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)

View File

@@ -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",

View File

@@ -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'}

22
test/test_attr.py Normal file
View File

@@ -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)

178
test/test_output.py Normal file
View File

@@ -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

View File

@@ -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__)

View File

@@ -1,3 +1,3 @@
import pytest
pytest.main(["-x", "tests/unit2"])
pytest.main(["--tb=native", "-s", "tests"])

View File

@@ -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 = [
(
'<layer id="1" name="Tile Layer 1" width="10" height="10">'
"</layer>",
+ "</layer>",
(int(1), "Tile Layer 1", None, None, None),
),
(
'<layer id="2" name="Tile Layer 2" width="10" height="10" opacity="0.5">'
"</layer>",
+ "</layer>",
(int(2), "Tile Layer 2", None, float(0.5), None),
),
(
'<layer id="5" name="Tile Layer 4" width="10" height="10" offsetx="49" offsety="-50">'
"<properties>"
"</properties>"
"</layer>",
+ "<properties>"
+ "</properties>"
+ "</layer>",
(
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

View File

@@ -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