Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for Python 3.6+, Tiled 1.2 #31

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
14 changes: 7 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

language: python
python:
- "2.6"
- "2.7"
- "3.3"
- "3.6"
- "3.7"
dist: xenial
sudo: required
install:
- python setup.py develop
- '[ "$USE_LXML" = "yes" ] && pip install --use-mirrors lxml; :'
- '[ "$USE_PIL" = "yes" ] && pip install --use-mirrors pillow; :'
- pip install pytest-cov --use-mirrors
- pip install coveralls --use-mirrors
- '[ "$USE_LXML" = "yes" ] && pip install lxml; :'
- '[ "$USE_PIL" = "yes" ] && pip install pillow; :'
- pip install pytest-cov coveralls formencode
env:
- USE_LXML=no USE_PIL=no PYTMXLIB_TEST_SKIP_IMAGE=yes
- USE_LXML=yes USE_PIL=yes PYTMXLIB_TEST_COVERALLS=yes
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Legend:

0.3 [unreleased]
** Images no longer support pixel assignment or the set_pixel method.
** Python 2 is no longer supported.
** Supported Python versions are 3.6+. Tests run on 3.6 and 3.7.
** Several arguments are now keyword-only.

+ Image slicing now creates ImageRegions
+ Images are now displayed graphically in IPython Notebook
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[![Build Status](https://secure.travis-ci.org/encukou/pytmxlib.png?branch=master)](http://travis-ci.org/encukou/pytmxlib)
[![Coverage Status](https://coveralls.io/repos/encukou/pytmxlib/badge.png?branch=tests)](https://coveralls.io/r/encukou/pytmxlib?branch=tests)
[![PyPI](https://pypip.in/v/tmxlib/badge.png)](https://crate.io/package/tmxlib)
[![PyPI](https://img.shields.io/pypi/v/tmxlib.svg)](https://pypi.org/project/tmxlib/)

tmxlib
======
Expand Down
2 changes: 1 addition & 1 deletion doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Tests
To run tests, ``pip install pytest-cov``, and run ``py.test``.

Tests can be run using tox_, to ensure cross-Python compatibility. Make sure
you have all supported Pythons (2.6, 2.7, 3.3) installed, and run ``tox``.
you have all supported Pythons (3.6, 3.7) installed, and run ``tox``.

Nowadays we use Travis CI and Coveralls to run tests after each commit:
|ci-status| |coveralls-badge|
Expand Down
7 changes: 3 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
'pytest-pep8',
'numpy',
'pillow',
'formencode',
]

test_requirements = [
Expand All @@ -31,11 +32,9 @@
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Programming Language :: Python :: 2
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Topic :: Games/Entertainment
""".splitlines() if x.strip()],
install_requires=[
Expand Down
101 changes: 79 additions & 22 deletions tmxlib/fileio.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from weakref import WeakValueDictionary
import sys
import warnings
import csv

import six
try:
Expand Down Expand Up @@ -91,6 +92,22 @@ def loader(self, *args, **kwargs):
return loader


def int_or_none(value):
if value is None:
return None
return int(value)


def int_or_float(value):
if isinstance(value, str):
if '.' in value:
return float(value)
return int(value)
if isinstance(value, int):
return value
return float(value)


class TMXSerializer(object):
def __init__(self):
import tmxlib
Expand Down Expand Up @@ -188,6 +205,13 @@ def map_from_element(self, cls, root, base_path):
orientation=root.attrib.pop('orientation'),
base_path=base_path,
background_color=background_color,
infinite=root.attrib.pop('infinite', None),
staggeraxis=root.attrib.pop('staggeraxis', None),
staggerindex=root.attrib.pop('staggerindex', None),
hexsidelength=root.attrib.pop('hexsidelength', None),
nextobjectid=int_or_none(root.attrib.pop('nextobjectid', None)),
nextlayerid=int_or_none(root.attrib.pop('nextlayerid', None)),
tiledversion=root.attrib.pop('tiledversion', None),
)
render_order = root.attrib.pop('renderorder', None)
if render_order:
Expand Down Expand Up @@ -262,13 +286,17 @@ def tileset_from_element(self, cls, elem, base_path):
kwargs['margin'] = int(elem.attrib.pop('margin', 0))
kwargs['spacing'] = int(elem.attrib.pop('spacing', 0))
kwargs['image'] = None
columns = elem.attrib.pop('columns', None)
if columns:
kwargs['columns'] = int(columns)
tileset = cls(
name=elem.attrib.pop('name'),
tile_size=(int(elem.attrib.pop('tilewidth')),
int(elem.attrib.pop('tileheight'))),
**kwargs
)
tileset._read_first_gid = int(elem.attrib.pop('firstgid', 0))
elem.attrib.pop('tilecount', None)
assert not elem.attrib, (
'Unexpected tileset attributes: %s' % elem.attrib)
for subelem in elem:
Expand Down Expand Up @@ -318,6 +346,12 @@ def tileset_from_element(self, cls, elem, base_path):
elif subelem.tag == 'tileoffset':
tileset.tile_offset = (
int(subelem.attrib['x']), int(subelem.attrib['y']))
elif subelem.tag == 'wangsets':
# XXX: Not implemented
pass
elif subelem.tag == 'grid':
# XXX: Not implemented
pass
else:
raise ValueError('Unknown tag %s' % subelem.tag)
if tileset.type == 'image' and not tileset.image:
Expand Down Expand Up @@ -417,9 +451,12 @@ def image_to_element(self, image, base_path):

@load_method
def tile_layer_from_element(self, cls, elem, map):
layer = cls(map, elem.attrib.pop('name'),
opacity=float(elem.attrib.pop('opacity', 1)),
visible=bool(int(elem.attrib.pop('visible', 1))))
layer = cls(
map, elem.attrib.pop('name'),
opacity=float(elem.attrib.pop('opacity', 1)),
visible=bool(int(elem.attrib.pop('visible', 1))),
id=int_or_none(elem.attrib.pop('id', None)),
)
layer_size = (int(elem.attrib.pop('width')),
int(elem.attrib.pop('height')))
assert layer_size == map.size
Expand All @@ -436,6 +473,9 @@ def tile_layer_from_element(self, cls, elem, map):
if encoding == 'base64':
data = base64.b64decode(data)
layer.encoding = 'base64'
elif encoding == 'csv':
# Handled below
pass
else:
raise ValueError('Bad encoding %s' % encoding)
compression = subelem.attrib.pop('compression', None)
Expand All @@ -453,13 +493,20 @@ def tile_layer_from_element(self, cls, elem, map):
'Bad compression %s' % compression)
else:
layer.compression = None
layer.data = array.array('L', [(
ord_(a) +
(ord_(b) << 8) +
(ord_(c) << 16) +
(ord_(d) << 24)) for
a, b, c, d in
zip(*(data[x::4] for x in range(4)))])
if encoding == 'csv':
result = []
for line in csv.reader(data.decode().splitlines()):
result.append(int(i) for i in line)
layer.data = result
layer.encoding = 'csv'
else:
layer.data = array.array('L', [(
ord_(a) +
(ord_(b) << 8) +
(ord_(c) << 16) +
(ord_(d) << 24)) for
a, b, c, d in
zip(*(data[x::4] for x in range(4)))])
data_set = True
else:
raise ValueError('Unknown tag %s' % subelem.tag)
Expand Down Expand Up @@ -534,13 +581,17 @@ def object_layer_from_element(self, cls, elem, map):
color = elem.attrib.pop('color', None)
if color:
color = from_hexcolor(color)
layer = cls(map, elem.attrib.pop('name'),
opacity=float(elem.attrib.pop('opacity', 1)),
visible=bool(int(elem.attrib.pop('visible', 1))),
color=color)
layer_size = (int(elem.attrib.pop('width')),
int(elem.attrib.pop('height')))
assert layer_size == map.size
layer = cls(
map, elem.attrib.pop('name'),
opacity=float(elem.attrib.pop('opacity', 1)),
visible=bool(int(elem.attrib.pop('visible', 1))),
color=color,
id=int_or_none(elem.attrib.pop('id', None))
)
if 'width' in elem.attrib:
layer_size = (int(elem.attrib.pop('width')),
int(elem.attrib.pop('height')))
assert layer_size == map.size
assert not elem.attrib, (
'Unexpected object layer attributes: %s' % elem.attrib)
for subelem in elem:
Expand All @@ -550,8 +601,8 @@ def object_layer_from_element(self, cls, elem, map):
kwargs = dict(
layer=layer,
)
x = int(subelem.attrib.pop('x'))
y = int(subelem.attrib.pop('y'))
x = int_or_float(subelem.attrib.pop('x'))
y = int_or_float(subelem.attrib.pop('y'))

def put(attr_type, attr_name, arg_name):
attr = subelem.attrib.pop(attr_name, None)
Expand All @@ -568,6 +619,8 @@ def put(attr_type, attr_name, arg_name):
if not kwargs.get('value'):
y += height
kwargs['pixel_pos'] = x, y
if 'id' in subelem.attrib:
kwargs['id'] = int(subelem.attrib.pop('id'))
assert not subelem.attrib, (
'Unexpected object attributes: %s' % subelem.attrib)
properties = {}
Expand Down Expand Up @@ -636,9 +689,12 @@ def object_layer_to_element(self, layer):

@load_method
def image_layer_from_element(self, cls, elem, map, base_path):
layer = cls(map, elem.attrib.pop('name'),
opacity=float(elem.attrib.pop('opacity', 1)),
visible=bool(int(elem.attrib.pop('visible', 1))))
layer = cls(
map, elem.attrib.pop('name'),
opacity=float(elem.attrib.pop('opacity', 1)),
visible=bool(int(elem.attrib.pop('visible', 1))),
id=int_or_none(elem.attrib.pop('id', None)),
)
layer_size = (int(elem.attrib.pop('width')),
int(elem.attrib.pop('height')))
assert layer_size == map.size
Expand Down Expand Up @@ -680,6 +736,7 @@ def read_properties(self, elem):
for prop in elem:
assert prop.tag == 'property'
name = prop.attrib.pop('name')
prop_type = prop.attrib.pop('type', 'string')
value = prop.attrib.pop('value')
properties[name] = value
assert not prop.attrib, (
Expand Down
4 changes: 2 additions & 2 deletions tmxlib/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import division

import functools
import collections
import collections.abc
import contextlib

import six
Expand Down Expand Up @@ -132,7 +132,7 @@ def size(self, value):
self.pixel_size = value[0] * px_parent[0], value[1] * px_parent[1]


class NamedElementList(collections.MutableSequence):
class NamedElementList(collections.abc.MutableSequence):
"""A list that supports indexing by element name, as a convenience, etc

``lst[some_name]`` means the first `element` where
Expand Down
29 changes: 22 additions & 7 deletions tmxlib/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class Layer(object):

Name of the layer

.. attribute:: id

Unique numeric ID of the layer.

.. attribute:: visible

A boolean setting whether the layer is visible at all. (Actual
Expand Down Expand Up @@ -66,13 +70,18 @@ class Layer(object):
A Layer is false in a boolean context iff it is empty, that is, if all
tiles of a tile layer are false, or if an object layer contains no objects.
"""
def __init__(self, map, name, visible=True, opacity=1):
# XXX: Implement `id` -- "Even if a layer is deleted, no layer ever gets
# the same ID."
def __init__(
self, map, name, *, visible=True, opacity=1, id=None,
):
super(Layer, self).__init__()
self.map = map
self.name = name
self.visible = visible
self.opacity = opacity
self.properties = {}
self.id = None

@property
def index(self):
Expand Down Expand Up @@ -139,9 +148,11 @@ class TileLayer(Layer):
layer, as one long list in row-major order.
See :class:`TileLikeObject.value` for what the numbers will mean.
"""
def __init__(self, map, name, visible=True, opacity=1, data=None):
def __init__(
self, map, name, *, visible=True, opacity=1, data=None, id=None,
):
super(TileLayer, self).__init__(map=map, name=name,
visible=visible, opacity=opacity)
visible=visible, opacity=opacity, id=id)
data_size = map.width * map.height
if data is None:
self.data = array.array('L', [0] * data_size)
Expand Down Expand Up @@ -271,9 +282,11 @@ class ImageLayer(Layer):
"""
type = 'image'

def __init__(self, map, name, visible=True, opacity=1, image=None):
def __init__(
self, map, name, *, visible=True, opacity=1, image=None, id=None,
):
super(ImageLayer, self).__init__(map=map, name=name,
visible=visible, opacity=opacity)
visible=visible, opacity=opacity, id=id)
self.image = image

def __nonzero__(self):
Expand Down Expand Up @@ -335,9 +348,11 @@ class ObjectLayer(Layer, helpers.NamedElementList):
The intended color of objects in this layer, as a triple of
floats (0..1)
"""
def __init__(self, map, name, visible=True, opacity=1, color=None):
def __init__(
self, map, name, *, visible=True, opacity=1, color=None, id=None
):
super(ObjectLayer, self).__init__(map=map, name=name,
visible=visible, opacity=opacity)
visible=visible, opacity=opacity, id=id)
self.type = 'objects'
self.color = color

Expand Down