diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..1be6e86
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "scripts/git-tools"]
+ path = scripts/git-tools
+ url = https://github.com/alexhayes/git-tools.git
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..aaedd65
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,37 @@
+language: python
+matrix:
+ include:
+ # Python 3.4
+ - python: 3.4
+ env: TOXENV=py34
+
+ # Python 3.3
+ - python: 3.3
+ env: TOXENV=py33
+
+ # Python 3.5
+ - python: 3.5
+ env: TOXENV=py35
+
+ # pypy
+ - python: pypy
+ env: TOXENV=pypy
+
+ # pypy
+ - python: pypy3
+ env: TOXENV=pypy3
+
+install:
+ - pip install tox
+ - pip install codecov
+ - pip install Django==1.10
+ - pip install -r requirements/test.txt
+ - pip install -r requirements/docs.txt
+
+script:
+ - py.test --cov=schematics_xml
+ - pylint schematics_xml
+ - cd docs && make clean html
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e69de29
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..4885f9b
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,13 @@
+include VERSION
+include CHANGELOG.md
+include LICENSE
+include README.rst
+include MANIFEST.in
+include setup.py
+
+recursive-include docs *
+recursive-include requirements *.txt *.rst
+
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]
+recursive-exclude * .*.sw[a-z]
diff --git a/README.md b/README.md
deleted file mode 100644
index 5a3419a..0000000
--- a/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# schematics-xml
-Python schematics models for converting from and to XML.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..75b37e0
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,77 @@
+==============
+schematics-xml
+==============
+
+Python schematics_ models for converting to and from XML.
+
+.. image:: https://travis-ci.org/alexhayes/schematics-xml.png?branch=master
+ :target: https://travis-ci.org/alexhayes/schematics-xml
+ :alt: Build Status
+
+.. image:: https://landscape.io/github/alexhayes/schematics-xml/master/landscape.png
+ :target: https://landscape.io/github/alexhayes/schematics-xml/
+ :alt: Code Health
+
+.. image:: https://codecov.io/github/alexhayes/schematics-xml/coverage.svg?branch=master
+ :target: https://codecov.io/github/alexhayes/schematics-xml?branch=master
+ :alt: Code Coverage
+
+.. image:: https://readthedocs.org/projects/schematics-xml/badge/
+ :target: http://schematics-xml.readthedocs.org/en/latest/
+ :alt: Documentation Status
+
+.. image:: https://img.shields.io/pypi/v/schematics-xml.svg
+ :target: https://pypi.python.org/pypi/schematics-xml
+ :alt: Latest Version
+
+.. image:: https://img.shields.io/pypi/pyversions/schematics-xml.svg
+ :target: https://pypi.python.org/pypi/schematics-xml/
+ :alt: Supported Python versions
+
+
+Install
+-------
+
+.. code-block:: bash
+
+ pip install schematics-xml
+
+
+Example Usage
+-------------
+
+Simply inherit XMLModel.
+
+.. code-block:: python
+
+ from schematics_xml import XMLModel
+
+ class Person(XMLModel):
+ name = StringType()
+
+ john = Person(dict(name='John'))
+
+ xml = john.to_xml()
+
+XML now contains;
+
+.. code-block:: xml
+
+
+
+ John
+
+
+And back the other way;
+
+.. code-block:: python
+
+ john = Person.from_xml(xml)
+
+
+Author
+------
+
+Alex Hayes
+
+.. _schematics: https://schematics.readthedocs.io
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..e69de29
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..9345fc1
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,9 @@
+codecov:
+ branch: develop
+ bot: alexhayes
+
+comment:
+ layout: "header, diff, changes, sunburst, uncovered, tree"
+ branches:
+ - *
+ behavior: default
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..ab43e2b
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,140 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+PROJECT = schematics-xml
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+ @echo "Please use \`make ' where is one of"
+ @echo " apidoc to regenerate API docs.
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+apidoc:
+ sphinx-apidoc -f -o internals/reference ../schematics_xml
+ cat internals/reference/index.tpl > internals/reference/index.rst
+ ls -1 internals/reference | fgrep -v index.rst | fgrep -v index.tpl | sed -e 's/.rst//g' -e 's/^/ /g' >> internals/reference/index.rst
+ @echo "Auto generated internals/reference/index.rst"
+ @echo
+ @echo "Build finished. The autogenerate API docs are in internals/reference."
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/$(PROJECT).qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/$(PROJECT).qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/$(PROJECT)"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/$(PROJECT)"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..4f46e98
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,60 @@
+import pkg_resources
+import sys
+import os
+import sphinx_rtd_theme
+
+this = os.path.dirname(os.path.abspath(__file__))
+
+
+# If your extensions are in another directory, add it here. If the directory
+# is relative to the documentation root, use os.path.abspath to make it
+# absolute, like shown here.
+sys.path.insert(0, os.path.join(this, os.pardir))
+
+from schematics_xml import __version__
+
+# Monkey patch to get around https://github.com/sphinx-doc/sphinx/issues/1254
+
+extensions = ['sphinx.ext.autodoc',
+ 'sphinx.ext.viewcode',
+ #'sphinx.ext.intersphinx',
+ ]
+
+templates_path = []
+source_suffix = '.rst'
+master_doc = 'index'
+project = 'schematics-xml'
+copyright_holder = 'Alex Hayes'
+copyright = u'2016, %s' % copyright_holder
+exclude_patterns = ['_build']
+pygments_style = 'sphinx'
+html_theme = "sphinx_rtd_theme"
+html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+htmlhelp_basename = '%sdoc' % project
+
+latex_documents = [
+ ('index', '%s.tex' % project, u'%s Documentation' % project, copyright_holder, 'manual'),
+]
+man_pages = [
+ ('index', project, u'%s Documentation' % project,
+ [copyright_holder], 1)
+]
+
+version = __version__
+release = version
+
+# autodoc_default_flags = ['members', 'private-members', 'special-members',
+# 'undoc-members',
+# 'show-inheritance']
+
+def autodoc_skip_member(app, what, name, obj, skip, options):
+ exclusions = ('__weakref__', # special-members
+ '__doc__', '__module__', '__dict__', # undoc-members
+ )
+ exclude = name in exclusions
+ return skip or exclude
+
+def setup(app):
+ app.connect('autodoc-skip-member', autodoc_skip_member)
+
+
diff --git a/docs/developer.rst b/docs/developer.rst
new file mode 100644
index 0000000..17b4406
--- /dev/null
+++ b/docs/developer.rst
@@ -0,0 +1,43 @@
+Developer Documentation
+=======================
+
+Contributions
+-------------
+
+Contributions are more than welcome!
+
+To get setup do the following;
+
+.. code-block:: bash
+
+ mkvirtualenv --python=/usr/bin/python3.5 schematics-xml
+ git clone https://github.com/alexhayes/schematics-xml.git
+ cd schematics-xml
+ pip install -r requirements/dev.txt
+ pip install Django>=1.9,<1.10
+
+
+Running Tests
+-------------
+
+Once you've checked out you should be able to run the tests;
+
+.. code-block:: bash
+
+ tox
+
+Or run all environments at once using detox;
+
+.. code-block:: bash
+
+ detox
+
+
+Creating Documentation
+----------------------
+
+.. code-block:: bash
+
+ cd docs
+ make clean html
+
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..5d710ba
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,60 @@
+==============
+schematics-xml
+==============
+
+Python schematics_ models for converting to and from XML.
+
+.. image:: https://travis-ci.org/alexhayes/schematics-xml.png?branch=master
+ :target: https://travis-ci.org/alexhayes/schematics-xml
+ :alt: Build Status
+
+.. image:: https://landscape.io/github/alexhayes/schematics-xml/master/landscape.png
+ :target: https://landscape.io/github/alexhayes/schematics-xml/
+ :alt: Code Health
+
+.. image:: https://codecov.io/github/alexhayes/schematics-xml/coverage.svg?branch=master
+ :target: https://codecov.io/github/alexhayes/schematics-xml?branch=master
+ :alt: Code Coverage
+
+.. image:: https://readthedocs.org/projects/schematics-xml/badge/
+ :target: http://schematics-xml.readthedocs.org/en/latest/
+ :alt: Documentation Status
+
+.. image:: https://img.shields.io/pypi/v/schematics-xml.svg
+ :target: https://pypi.python.org/pypi/schematics-xml
+ :alt: Latest Version
+
+.. image:: https://img.shields.io/pypi/pyversions/schematics-xml.svg
+ :target: https://pypi.python.org/pypi/schematics-xml/
+ :alt: Supported Python versions
+
+.. image:: https://img.shields.io/pypi/dd/schematics-xml.svg
+ :target: https://pypi.python.org/pypi/schematics-xml/
+ :alt: Downloads
+
+
+Contents
+--------
+
+.. toctree::
+ :maxdepth: 1
+
+ installation
+ usage
+ developer
+ internals/reference/index
+
+
+License
+-------
+
+This software is licensed under the `MIT License`. See the `LICENSE`_.
+
+
+Author
+------
+
+Alex Hayes
+
+.. _schematics: schematics.readthedocs.org
+.. _LICENSE: https://github.com/alexhayes/schematics-xml/blob/master/LICENSE
diff --git a/docs/installation.rst b/docs/installation.rst
new file mode 100644
index 0000000..3ecc900
--- /dev/null
+++ b/docs/installation.rst
@@ -0,0 +1,23 @@
+============
+Installation
+============
+
+You can install schematics-xml either via the Python Package Index (PyPI)
+or from github.
+
+To install using pip;
+
+.. code-block:: bash
+
+ $ pip install schematics-xml
+
+From github;
+
+.. code-block:: bash
+
+ $ pip install git+https://github.com/alexhayes/schematics-xml.git
+
+
+Currently `schematics-xml` only supports Python 3 - if you'd like feel free to
+submit a PR adding support for Python 2, however note that I love type
+annotations and any Python 2 PR will need to include a stub file.
diff --git a/docs/internals/reference/index.rst b/docs/internals/reference/index.rst
new file mode 100644
index 0000000..4356687
--- /dev/null
+++ b/docs/internals/reference/index.rst
@@ -0,0 +1,13 @@
+Internal Module Reference
+=========================
+
+:Release: |version|
+:Date: |today|
+
+
+.. toctree::
+ :maxdepth: 1
+
+ schematics_xml
+ schematics_xml.tests
+ modules
diff --git a/docs/internals/reference/index.tpl b/docs/internals/reference/index.tpl
new file mode 100644
index 0000000..864f0f6
--- /dev/null
+++ b/docs/internals/reference/index.tpl
@@ -0,0 +1,10 @@
+Internal Module Reference
+=========================
+
+:Release: |version|
+:Date: |today|
+
+
+.. toctree::
+ :maxdepth: 1
+
diff --git a/docs/internals/reference/modules.rst b/docs/internals/reference/modules.rst
new file mode 100644
index 0000000..d60547f
--- /dev/null
+++ b/docs/internals/reference/modules.rst
@@ -0,0 +1,7 @@
+schematics_xml
+==============
+
+.. toctree::
+ :maxdepth: 4
+
+ schematics_xml
diff --git a/docs/internals/reference/schematics_xml.rst b/docs/internals/reference/schematics_xml.rst
new file mode 100644
index 0000000..06d2aa0
--- /dev/null
+++ b/docs/internals/reference/schematics_xml.rst
@@ -0,0 +1,29 @@
+schematics_xml package
+======================
+
+Subpackages
+-----------
+
+.. toctree::
+
+ schematics_xml.tests
+
+Submodules
+----------
+
+schematics_xml.models module
+----------------------------
+
+.. automodule:: schematics_xml.models
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+
+Module contents
+---------------
+
+.. automodule:: schematics_xml
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/internals/reference/schematics_xml.tests.rst b/docs/internals/reference/schematics_xml.tests.rst
new file mode 100644
index 0000000..18a6fe6
--- /dev/null
+++ b/docs/internals/reference/schematics_xml.tests.rst
@@ -0,0 +1,22 @@
+schematics_xml.tests package
+============================
+
+Submodules
+----------
+
+schematics_xml.tests.test_models module
+---------------------------------------
+
+.. automodule:: schematics_xml.tests.test_models
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+
+Module contents
+---------------
+
+.. automodule:: schematics_xml.tests
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/usage.rst b/docs/usage.rst
new file mode 100644
index 0000000..536b776
--- /dev/null
+++ b/docs/usage.rst
@@ -0,0 +1,61 @@
+=====
+Usage
+=====
+
+Simply inherit ``XMLModel``.
+
+.. code-block:: python
+
+ from schematics_xml import XMLModel
+
+ class Person(XMLModel):
+ name = StringType()
+
+ john = Person(dict(name='John'))
+
+ xml = john.to_xml()
+
+XML now contains;
+
+.. code-block:: xml
+
+
+
+ John
+
+
+And back the other way;
+
+.. code-block:: python
+
+ john = Person.from_xml(xml)
+
+
+Root Node
+---------
+
+To set the root node simply define class property ``xml_root``, as follows;
+
+.. code-block:: python
+
+ from schematics_xml import XMLModel
+
+ class Animal(XMLModel):
+ kind = StringType()
+
+ @property
+ def xml_root(self):
+ return self.kind
+
+ garfield = Animal(dict(kind='cat'))
+
+ xml = garfield.to_xml()
+
+XML now contains;
+
+.. code-block:: xml
+
+
+
+ cat
+
diff --git a/pylintrc b/pylintrc
new file mode 100644
index 0000000..f16c146
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,20 @@
+[MASTER]
+max-line-length=140
+
+[MESSAGES CONTROL]
+
+disable=
+ missing-docstring
+
+[REPORTS]
+
+output-format=colorized
+
+[TYPECHECK]
+
+generated-members=
+ content,
+ DoesNotExist,
+ MultipleObjectsReturned,
+ objects,
+ status_code,
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..e69de29
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..64c9401
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+-r requirements/default.txt
diff --git a/requirements/default.txt b/requirements/default.txt
new file mode 100644
index 0000000..4d55812
--- /dev/null
+++ b/requirements/default.txt
@@ -0,0 +1,3 @@
+schematics==2.0.0a1
+lxml==3.6.4
+xmltodict==0.10.2
diff --git a/requirements/docs.txt b/requirements/docs.txt
new file mode 100644
index 0000000..ab3f3dd
--- /dev/null
+++ b/requirements/docs.txt
@@ -0,0 +1,2 @@
+Sphinx
+sphinx_rtd_theme
diff --git a/requirements/pkgutils.txt b/requirements/pkgutils.txt
new file mode 100644
index 0000000..29de696
--- /dev/null
+++ b/requirements/pkgutils.txt
@@ -0,0 +1,4 @@
+setuptools>=1.3.2
+wheel
+tox
+detox
diff --git a/requirements/rtd.txt b/requirements/rtd.txt
new file mode 100644
index 0000000..4525e28
--- /dev/null
+++ b/requirements/rtd.txt
@@ -0,0 +1,2 @@
+-r default.txt
+Django==1.9
diff --git a/requirements/test.txt b/requirements/test.txt
new file mode 100644
index 0000000..f33b733
--- /dev/null
+++ b/requirements/test.txt
@@ -0,0 +1,4 @@
+-r default.txt
+
+py==1.4.31
+pytest==3.0.3
diff --git a/schematics_xml/__init__.py b/schematics_xml/__init__.py
new file mode 100644
index 0000000..b9ad016
--- /dev/null
+++ b/schematics_xml/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+"""Python schematics models for converting to and from XML."""
+# :copyright: (c) 2016 Alex Hayes,
+# All rights reserved.
+# :license: MIT License, see LICENSE for more details.
+from __future__ import absolute_import, print_function, unicode_literals
+from collections import namedtuple
+
+VersionInfo = namedtuple(
+ 'VersionInfo', ('major', 'minor', 'micro', 'releaselevel', 'serial'),
+)
+
+VERSION = VersionInfo(0, 1, 0, '', '')
+__version__ = '{0.major}.{0.minor}.{0.micro}{0.releaselevel}'.format(VERSION)
+__author__ = 'Alex Hayes'
+__contact__ = 'alex@alution.com'
+__homepage__ = 'http://github.com/alexhayes/schematics-xml'
+__docformat__ = 'restructuredtext'
+
+# -eof meta-
+
+from schematics_xml.models import XMLModel
diff --git a/schematics_xml/models.py b/schematics_xml/models.py
new file mode 100644
index 0000000..10bece3
--- /dev/null
+++ b/schematics_xml/models.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+"""
+ schematics_xml.models
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ Base models that provide to/from XML methods.
+"""
+
+import collections
+import numbers
+
+import lxml.builder
+import lxml.etree
+from schematics import Model
+from schematics.types import BaseType, ModelType, CompoundType
+from schematics.types.base import MultilingualStringType
+from schematics.types.compound import PolyModelType
+from xmltodict import parse
+
+
+class XMLModel(Model):
+ """
+ A model that can convert it's fields to and from XML.
+ """
+ @property
+ def xml_root(self):
+ return type(self).__name__.lower()
+
+ def to_xml(self, *, role: str=None, app_data: dict=None, **kwargs) -> str:
+ """
+ Return a string of XML that represents this model.
+
+ Currently all arguments are passed through to schematics.Model.to_primitive.
+
+ :param role: schematics Model to_primitive role parameter.
+ :param app_data: schematics Model to_primitive app_data parameter.
+ :param kwargs: schematics Model to_primitive kwargs parameter.
+ """
+ primitive = self.to_primitive(role=role, app_data=app_data, **kwargs)
+ root = self.primitive_to_xml(primitive)
+ return lxml.etree.tostring( # pylint: disable=no-member
+ root,
+ pretty_print=True,
+ xml_declaration=True,
+ encoding='ISO-8859-1'
+ )
+
+ def primitive_to_xml(self, primitive: dict, parent: 'lxml.etree._Element'=None):
+ element_maker = lxml.builder.ElementMaker()
+
+ if parent is None:
+ parent = getattr(element_maker, self.xml_root)()
+
+ for key, value in primitive.items():
+ self.primitive_value_to_xml(key, parent, value)
+
+ return parent
+
+ def primitive_value_to_xml(self, key, parent, value):
+ element_maker = lxml.builder.ElementMaker()
+
+ if isinstance(value, bool):
+ parent.append(getattr(element_maker, key)('1' if value else '0'))
+
+ elif isinstance(value, numbers.Number) or isinstance(value, str):
+ parent.append(getattr(element_maker, key)(str(value)))
+
+ elif value is None:
+ parent.append(getattr(element_maker, key)(''))
+
+ elif isinstance(value, dict):
+ _parent = getattr(element_maker, key)()
+ parent.append(self.primitive_to_xml(value, _parent))
+
+ elif isinstance(value, collections.abc.Iterable):
+ for _value in value:
+ self.primitive_value_to_xml(key, parent, _value)
+
+ else:
+ raise TypeError('Unsupported data type: %s (%s)' % (value, type(value).__name__))
+
+ @classmethod
+ def from_xml(cls, xml: str) -> Model:
+ """
+ Convert XML into a model.
+
+ :param xml: A string of XML that represents this Model.
+ """
+ if model_has_field_type(MultilingualStringType, cls):
+ raise NotImplementedError("Field type 'MultilingualStringType' is not supported.")
+ primitive = parse(xml)
+ if len(primitive) != 1:
+ raise NotImplementedError
+ for _, raw_data in primitive.items():
+ return cls(raw_data=raw_data)
+
+
+def model_has_field_type(needle: BaseType, haystack: Model) -> bool:
+ """
+ Return True if haystack contains a field of type needle.
+
+ Iterates over all fields (and into field if appropriate) and searches for field type *needle* in model
+ *haystack*.
+
+ :param needle: A schematics field class to search for.
+ :param haystack: A schematics model to search within.
+ """
+ for _, field in haystack._field_list: # pylint: disable=protected-access
+ if field_has_type(needle, field):
+ return True
+ return False
+
+
+def field_has_type(needle: BaseType, field: BaseType) -> bool: # pylint: disable=too-many-return-statements, too-many-branches
+ """
+ Return True if field haystack contains a field of type needle.
+
+ :param needle: A schematics field class to search for.
+ :param haystack: An instance of a schematics field within a model.
+ """
+ if isinstance(field, needle):
+ return True
+
+ elif isinstance(field, ModelType):
+ if model_has_field_type(needle, field.model_class):
+ return True
+
+ elif isinstance(field, PolyModelType):
+ if needle in [type(obj) for obj in field.model_classes]:
+ return True
+
+ for obj in [obj for obj in field.model_classes if isinstance(obj, ModelType)]:
+ if model_has_field_type(needle, obj.model_class):
+ return True
+
+ elif isinstance(field, CompoundType):
+ if needle == type(field.field):
+ return True
+
+ try:
+ if needle == field.model_class:
+ return True
+
+ except AttributeError:
+ pass
+
+ else:
+ if model_has_field_type(needle, field.model_class):
+ return True
+
+ if field_has_type(needle, field.field):
+ return True
+
+ return False
diff --git a/schematics_xml/tests/__init__.py b/schematics_xml/tests/__init__.py
new file mode 100644
index 0000000..40a96af
--- /dev/null
+++ b/schematics_xml/tests/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/schematics_xml/tests/test_models.py b/schematics_xml/tests/test_models.py
new file mode 100644
index 0000000..ff6a68e
--- /dev/null
+++ b/schematics_xml/tests/test_models.py
@@ -0,0 +1,784 @@
+# -*- coding: utf-8 -*-
+"""
+ schematics_xml.tests.test_models
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Tests for XMLModel
+"""
+from datetime import date
+from datetime import datetime
+from decimal import Decimal
+
+import pytest
+from schematics import Model
+from schematics.types import StringType, IntType, FloatType, DecimalType, ModelType, DictType, LongType, UUIDType, \
+ MD5Type, SHA1Type, BooleanType, DateType, DateTimeType, UTCDateTimeType, TimestampType, GeoPointType, \
+ MultilingualStringType, ListType, IPv4Type, IPv6Type, URLType, EmailType, UnionType
+from schematics.types.compound import PolyModelType
+
+from schematics_xml.models import XMLModel, model_has_field_type
+
+
+class TestUUIDType:
+
+ class Person(XMLModel):
+ pk = UUIDType() # pylint: disable=invalid-name
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 32c5548e-ddee-4b23-a06e-f387a15bcac9\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ john = self.Person(dict(pk='32c5548e-ddee-4b23-a06e-f387a15bcac9'))
+ actual = john.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ john = self.Person.from_xml(self.xml)
+ assert isinstance(john, self.Person)
+ assert str(john.pk) == '32c5548e-ddee-4b23-a06e-f387a15bcac9'
+
+
+class TestStringType:
+
+ class Person(XMLModel):
+ name = StringType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' John\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ john = self.Person(dict(name='John'))
+ actual = john.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ john = self.Person.from_xml(self.xml)
+ assert isinstance(john, self.Person)
+ assert john.name == 'John'
+
+
+class TestIntType:
+
+ class Person(XMLModel):
+ age = IntType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 18\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ john = self.Person(dict(age=18))
+ actual = john.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ john = self.Person.from_xml(self.xml)
+ assert isinstance(john, self.Person)
+ assert john.age == 18
+
+
+class TestLongType:
+
+ class Person(XMLModel):
+ pk = LongType() # pylint: disable=invalid-name
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 1832932875982759827298\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ john = self.Person(dict(pk=1832932875982759827298))
+ actual = john.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ john = self.Person.from_xml(self.xml)
+ assert isinstance(john, self.Person)
+ assert john.pk == 1832932875982759827298
+
+
+class TestFloatType:
+
+ class Person(XMLModel):
+ height = FloatType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 12.2\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ john = self.Person(dict(height=12.2))
+ actual = john.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ john = self.Person.from_xml(self.xml)
+ assert isinstance(john, self.Person)
+ assert john.height == 12.2
+
+
+class TestDecimalType:
+
+ class Person(XMLModel):
+ height = DecimalType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 12.2\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ john = self.Person(dict(height=12.2))
+ actual = john.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ john = self.Person.from_xml(self.xml)
+ assert isinstance(john, self.Person)
+ assert john.height == Decimal('12.2')
+
+
+class TestMD5Type:
+
+ class File(XMLModel):
+ md5 = MD5Type()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' efe2d5fd46824508b8a0082c8279bbae\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ file = self.File(dict(md5='efe2d5fd46824508b8a0082c8279bbae'))
+ actual = file.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ file = self.File.from_xml(self.xml)
+ assert isinstance(file, self.File)
+ assert file.md5 == 'efe2d5fd46824508b8a0082c8279bbae'
+
+
+class TestSHA1Type:
+
+ class File(XMLModel):
+ sha1 = SHA1Type()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 2eE84Ef6301cCEc5926C4ADBF3E9B51c6c42ade3\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ file = self.File(dict(sha1='2eE84Ef6301cCEc5926C4ADBF3E9B51c6c42ade3'))
+ actual = file.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ file = self.File.from_xml(self.xml)
+ assert isinstance(file, self.File)
+ assert file.sha1 == '2eE84Ef6301cCEc5926C4ADBF3E9B51c6c42ade3'
+
+
+class TestBooleanType:
+
+ class User(XMLModel):
+ active = BooleanType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 1\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ user = self.User(dict(active=True))
+ actual = user.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ file = self.User.from_xml(self.xml)
+ assert isinstance(file, self.User)
+ assert file.active is True
+
+
+class TestDateType:
+
+ class User(XMLModel):
+ birthdate = DateType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 2016-01-01\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ user = self.User(dict(birthdate=date(2016, 1, 1)))
+ actual = user.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ user = self.User.from_xml(self.xml)
+ assert isinstance(user, self.User)
+ assert user.birthdate == date(2016, 1, 1)
+
+
+class TestDateTimeType:
+
+ class User(XMLModel):
+ created = DateTimeType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 2016-01-01T08:30:32.000000\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ user = self.User(dict(created=datetime(2016, 1, 1, 8, 30, 32)))
+ actual = user.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ user = self.User.from_xml(self.xml)
+ assert isinstance(user, self.User)
+ assert user.created == datetime(2016, 1, 1, 8, 30, 32)
+
+
+class TestUTCDateTimeType:
+
+ class User(XMLModel):
+ created = UTCDateTimeType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 2016-01-01T08:30:32.000000Z\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ user = self.User(dict(created=datetime(2016, 1, 1, 8, 30, 32)))
+ actual = user.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ user = self.User.from_xml(self.xml)
+ assert isinstance(user, self.User)
+ assert user.created == datetime(2016, 1, 1, 8, 30, 32)
+
+
+class TestTimestampType:
+
+ class User(XMLModel):
+ created = TimestampType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 1451637032\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ user = self.User(dict(created=datetime(2016, 1, 1, 8, 30, 32, tzinfo=DateTimeType.UTC)))
+ actual = user.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ user = self.User.from_xml(self.xml)
+
+ assert isinstance(user, self.User)
+ assert user.created == datetime(2016, 1, 1, 8, 30, 32, tzinfo=DateTimeType.UTC)
+
+
+class TestGeoPointType:
+
+ class Place(XMLModel):
+ point = GeoPointType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 23\n'
+ b' 170\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ place = self.Place(dict(point=(23, 170)))
+ actual = place.to_xml()
+ assert actual == self.xml
+
+ @pytest.mark.xfail(reason="Schematics should convert string types to numeric.")
+ def test_from_xml(self):
+ place = self.Place.from_xml(self.xml)
+ assert isinstance(place, self.Place)
+ assert place.point == (23, 170)
+
+
+class TestMultilingualStringType:
+
+ class Animal(XMLModel):
+ text = MultilingualStringType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' serpent\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ animal = self.Animal(dict(text={
+ 'en_US': 'snake',
+ 'fr_FR': 'serpent'
+ }))
+ actual = animal.to_xml(app_data=dict(locale='fr_FR'))
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ with pytest.raises(NotImplementedError):
+ self.Animal.from_xml(self.xml)
+ # MultilingualStringType is not two way
+
+ def test_from_xml_nested_raises(self):
+ """
+ Test that from_xml raises NotImplementedError for a nested MultilingualStringType
+ """
+ class Parent(XMLModel):
+ child = ModelType(self.Animal)
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' \n'
+ b' serpent\n'
+ b' \n'
+ b'\n'
+ )
+ with pytest.raises(NotImplementedError):
+ Parent.from_xml(xml)
+
+
+# schematics Compound Types
+
+class TestModelType:
+ _person_cls = None
+
+ class Pet(XMLModel):
+ name = StringType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' \n'
+ b' Garfield\n'
+ b' \n'
+ b'\n'
+ )
+
+ @property
+ def Person(self): # pylint: disable=invalid-name
+ if not self._person_cls:
+ class Person(XMLModel):
+ pet = ModelType(self.Pet)
+ self._person_cls = Person
+ return self._person_cls
+
+ def test_to_xml(self):
+ john = self.Person(dict(pet=dict(name='Garfield')))
+ actual = john.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ person = self.Person.from_xml(self.xml)
+ assert isinstance(person, self.Person)
+ assert isinstance(person.pet, self.Pet)
+ assert person.pet.name == 'Garfield'
+
+
+class TestListTypeOfIntType:
+
+ class Person(XMLModel):
+ favorite_numbers = ListType(IntType())
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 1\n'
+ b' 2\n'
+ b' 3\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ john = self.Person(dict(favorite_numbers=[1, 2, 3]))
+ actual = john.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ person = self.Person.from_xml(self.xml)
+ assert isinstance(person, self.Person)
+ assert person.favorite_numbers == [1, 2, 3]
+
+
+class TestListTypeOfModelType:
+ _person_cls = None
+
+ class Color(XMLModel):
+ name = StringType()
+
+ @property
+ def Person(self): # pylint: disable=invalid-name
+ if not self._person_cls:
+ class Person(XMLModel):
+ favorite_colors = ListType(ModelType(self.Color))
+ self._person_cls = Person
+ return self._person_cls
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' \n'
+ b' red\n'
+ b' \n'
+ b' \n'
+ b' green\n'
+ b' \n'
+ b' \n'
+ b' blue\n'
+ b' \n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ john = self.Person(dict(favorite_colors=[
+ self.Color(dict(name='red')),
+ self.Color(dict(name='green')),
+ self.Color(dict(name='blue'))
+ ]))
+ actual = john.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ person = self.Person.from_xml(self.xml)
+ assert isinstance(person, self.Person)
+ assert len(person.favorite_colors) == 3
+ assert person.favorite_colors[0].name == 'red' # pylint: disable=unsubscriptable-object
+ assert person.favorite_colors[1].name == 'green' # pylint: disable=unsubscriptable-object
+ assert person.favorite_colors[2].name == 'blue' # pylint: disable=unsubscriptable-object
+
+
+class TestDictType:
+
+ class Request(XMLModel):
+ payload = DictType(StringType())
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' \n'
+ b' bar\n'
+ b' \n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ request = self.Request(dict(payload=dict(foo='bar')))
+ actual = request.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ request = self.Request.from_xml(self.xml)
+ assert request.payload == dict(foo='bar')
+
+
+class TestPolyModelType:
+ _recipe_item_cls = None
+
+ class Eggs(XMLModel):
+ yolks = IntType()
+
+ class Sausage(XMLModel):
+ meat = StringType()
+
+ @property
+ def RecipeItem(self): # pylint: disable=invalid-name
+ if not self._recipe_item_cls:
+ class RecipeItem(XMLModel):
+ item = PolyModelType([self.Eggs, self.Sausage])
+ self._recipe_item_cls = RecipeItem
+ return self._recipe_item_cls
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' - \n'
+ b' 2\n'
+ b'
\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ recipe_type = self.RecipeItem(dict(item=self.Eggs(dict(yolks=2))))
+ actual = recipe_type.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ recipe_item = self.RecipeItem.from_xml(self.xml)
+ assert isinstance(recipe_item, self.RecipeItem)
+ assert isinstance(recipe_item.item, self.Eggs)
+ assert recipe_item.item.yolks == 2 # pylint: disable=no-member
+
+
+# Net tests
+class TestIPv4Type:
+
+ class Proxy(XMLModel):
+ ip_address = IPv4Type()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 8.8.8.8\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ proxy = self.Proxy(dict(ip_address='8.8.8.8'))
+ actual = proxy.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ request = self.Proxy.from_xml(self.xml)
+ assert request.ip_address == '8.8.8.8'
+
+
+class TestIPv6Type:
+
+ class Proxy(XMLModel):
+ ip_address = IPv6Type()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 2001:db8:85a3::8a2e:370:7334\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ proxy = self.Proxy(dict(ip_address='2001:db8:85a3::8a2e:370:7334'))
+ actual = proxy.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ request = self.Proxy.from_xml(self.xml)
+ assert request.ip_address == '2001:db8:85a3::8a2e:370:7334'
+
+
+class TestURLType:
+
+ class Site(XMLModel):
+ url = URLType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' https://github.com/alexhayes/schematics-xml\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ site = self.Site(dict(url='https://github.com/alexhayes/schematics-xml'))
+ actual = site.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ request = self.Site.from_xml(self.xml)
+ assert request.url == 'https://github.com/alexhayes/schematics-xml'
+
+
+class TestEmailType:
+
+ class User(XMLModel):
+ email = EmailType()
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' user@example.com\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ user = self.User(dict(email='user@example.com'))
+ actual = user.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ request = self.User.from_xml(self.xml)
+ assert request.email == 'user@example.com'
+
+
+# UnionType tests
+
+class TestUnionType:
+
+ class Foo(XMLModel):
+ union = UnionType([IntType, StringType])
+
+ xml = (
+ b"\n"
+ b'\n'
+ b' 2\n'
+ b'\n'
+ )
+
+ def test_to_xml(self):
+ obj = self.Foo(dict(union=2))
+ actual = obj.to_xml()
+ assert actual == self.xml
+
+ def test_from_xml(self):
+ obj = self.Foo.from_xml(self.xml)
+ assert isinstance(obj, self.Foo)
+ assert obj.union == 2
+
+
+class TestHasFieldType:
+
+ class TestModel(Model):
+ a = StringType() # pylint: disable=invalid-name
+ b = IntType() # pylint: disable=invalid-name
+ c = FloatType() # pylint: disable=invalid-name
+
+ def test_shallow(self):
+ assert model_has_field_type(StringType, self.TestModel) is True
+ assert model_has_field_type(IntType, self.TestModel) is True
+ assert model_has_field_type(FloatType, self.TestModel) is True
+ assert model_has_field_type(Decimal, self.TestModel) is False
+
+ def test_modeltype(self):
+ class Parent(Model):
+ child = ModelType(self.TestModel)
+
+ assert model_has_field_type(ModelType, Parent) is True
+ assert model_has_field_type(StringType, Parent) is True
+ assert model_has_field_type(IntType, Parent) is True
+ assert model_has_field_type(FloatType, Parent) is True
+ assert model_has_field_type(Decimal, Parent) is False
+
+ def test_listtype(self):
+ class Container(Model):
+ items = ListType(ModelType(self.TestModel))
+
+ assert model_has_field_type(ListType, Container) is True
+ assert model_has_field_type(ModelType, Container) is True
+ assert model_has_field_type(StringType, Container) is True
+ assert model_has_field_type(IntType, Container) is True
+ assert model_has_field_type(FloatType, Container) is True
+ assert model_has_field_type(Decimal, Container) is False
+
+ class Container(Model): # pylint: disable=function-redefined
+ items = ListType(IntType())
+
+ assert model_has_field_type(ListType, Container) is True
+ assert model_has_field_type(ModelType, Container) is False
+ assert model_has_field_type(StringType, Container) is False
+ assert model_has_field_type(IntType, Container) is True
+ assert model_has_field_type(FloatType, Container) is False
+ assert model_has_field_type(Decimal, Container) is False
+
+ def test_listtype_withlisttype(self):
+ class Container(Model):
+ items = ListType(ListType(ModelType(self.TestModel)))
+
+ assert model_has_field_type(ListType, Container) is True
+ assert model_has_field_type(ModelType, Container) is True
+ assert model_has_field_type(StringType, Container) is True
+ assert model_has_field_type(IntType, Container) is True
+ assert model_has_field_type(FloatType, Container) is True
+ assert model_has_field_type(Decimal, Container) is False
+
+ class Container(Model): # pylint: disable=function-redefined
+ items = ListType(ListType(IntType()))
+
+ assert model_has_field_type(ListType, Container) is True
+ assert model_has_field_type(ModelType, Container) is False
+ assert model_has_field_type(StringType, Container) is False
+ assert model_has_field_type(IntType, Container) is True
+ assert model_has_field_type(FloatType, Container) is False
+ assert model_has_field_type(Decimal, Container) is False
+
+ def test_dicttype(self):
+ class Container(Model):
+ items = DictType(ModelType(self.TestModel))
+
+ assert model_has_field_type(DictType, Container) is True
+ assert model_has_field_type(ModelType, Container) is True
+ assert model_has_field_type(StringType, Container) is True
+ assert model_has_field_type(IntType, Container) is True
+ assert model_has_field_type(FloatType, Container) is True
+ assert model_has_field_type(Decimal, Container) is False
+
+ class Container(Model): # pylint: disable=function-redefined
+ items = DictType(IntType())
+
+ assert model_has_field_type(DictType, Container) is True
+ assert model_has_field_type(ModelType, Container) is False
+ assert model_has_field_type(StringType, Container) is False
+ assert model_has_field_type(IntType, Container) is True
+ assert model_has_field_type(FloatType, Container) is False
+ assert model_has_field_type(Decimal, Container) is False
+
+ def test_polymodel_fieldtype(self):
+ class Container(Model):
+ item = PolyModelType([IntType(), StringType()])
+
+ assert model_has_field_type(PolyModelType, Container) is True
+ assert model_has_field_type(DictType, Container) is False
+ assert model_has_field_type(ModelType, Container) is False
+ assert model_has_field_type(StringType, Container) is True
+ assert model_has_field_type(IntType, Container) is True
+ assert model_has_field_type(FloatType, Container) is False
+ assert model_has_field_type(Decimal, Container) is False
+
+ class Container(Model): # pylint: disable=function-redefined
+ item = PolyModelType([ModelType(self.TestModel), DecimalType()])
+
+ assert model_has_field_type(PolyModelType, Container) is True
+ assert model_has_field_type(DictType, Container) is False
+ assert model_has_field_type(ModelType, Container) is True
+ assert model_has_field_type(StringType, Container) is True
+ assert model_has_field_type(IntType, Container) is True
+ assert model_has_field_type(FloatType, Container) is True
+ assert model_has_field_type(DecimalType, Container) is True
+ assert model_has_field_type(EmailType, Container) is False
diff --git a/scripts/git-tools b/scripts/git-tools
new file mode 160000
index 0000000..28fff02
--- /dev/null
+++ b/scripts/git-tools
@@ -0,0 +1 @@
+Subproject commit 28fff02d3ee7118041c0d321a968c9a7b89081b4
diff --git a/scripts/git-tools-hooks/git-on-release.sh b/scripts/git-tools-hooks/git-on-release.sh
new file mode 100755
index 0000000..ec562d3
--- /dev/null
+++ b/scripts/git-tools-hooks/git-on-release.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+VERSION=$1
+DIR="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+INIT_PATH="$DIR/../../schematics_xml/__init__.py"
+
+if [ -z "$VERSION" ]; then
+ echo "VERSION must be specified!"
+ exit 1
+fi
+
+echo "### MUNGING VERSION $VERSION INTO $INIT_PATH"
+
+PY_VERSION=`echo $VERSION | sed -e 's/\./, /g'`
+
+# Replace the version number
+sed --in-place -e "s/VERSION = VersionInfo(.*)/VERSION = VersionInfo($PY_VERSION, '', '')/g" $INIT_PATH
+
+# Ensure we haven't caused some kind of error
+python -m py_compile $INIT_PATH
+
+# Check the last commands return code
+rc=$?; if [[ $rc != 0 ]]; then exit $rc; fi
+
+# We're all good, add it to git for the commit
+git add $INIT_PATH
diff --git a/scripts/removepyc.sh b/scripts/removepyc.sh
new file mode 100755
index 0000000..9aaf365
--- /dev/null
+++ b/scripts/removepyc.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+(cd "${1:-.}";
+ find . -name "*.pyc" | xargs rm -- 2>/dev/null) || echo "ok"
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..7469488
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[metadata]
+description-file = README.rst
+
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..4657286
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+try:
+ from setuptools import setup, find_packages
+ from setuptools.command.test import test
+ is_setuptools = True
+except ImportError:
+ raise
+ from ez_setup import use_setuptools
+ use_setuptools()
+ from setuptools import setup, find_packages # noqa
+ from setuptools.command.test import test # noqa
+ is_setuptools = False
+
+import os
+import sys
+import codecs
+
+NAME = 'schematics-xml'
+entrypoints = {}
+extra = {}
+
+# -*- Classifiers -*-
+
+
+
+classes = """
+ Development Status :: 4 - Beta
+ License :: OSI Approved :: MIT License
+ Topic :: Other/Nonlisted Topic
+ Topic :: Software Development :: Libraries :: Python Modules
+ Intended Audience :: Developers
+ Programming Language :: Python
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.3
+ Programming Language :: Python :: 3.4
+ Programming Language :: Python :: 3.5
+ Programming Language :: Python :: Implementation :: CPython
+ Programming Language :: Python :: Implementation :: PyPy
+ Operating System :: OS Independent
+ Operating System :: POSIX
+ Operating System :: Microsoft :: Windows
+ Operating System :: MacOS :: MacOS X
+"""
+classifiers = [s.strip() for s in classes.split('\n') if s]
+
+# -*- Distribution Meta -*-
+
+import re
+re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)')
+re_vers = re.compile(r'VERSION\s*=.*?\((.*?)\)')
+re_doc = re.compile(r'^"""(.+?)"""')
+rq = lambda s: s.strip("\"'")
+
+
+def add_default(m):
+ attr_name, attr_value = m.groups()
+ return ((attr_name, rq(attr_value)), )
+
+
+def add_version(m):
+ v = list(map(rq, m.groups()[0].split(', ')))
+ return (('VERSION', '.'.join(v[0:3]) + ''.join(v[3:])), )
+
+
+def add_doc(m):
+ return (('doc', m.groups()[0]), )
+
+pats = {re_meta: add_default,
+ re_vers: add_version,
+ re_doc: add_doc}
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'schematics_xml/__init__.py')) as meta_fh:
+ meta = {}
+ for line in meta_fh:
+ if line.strip() == '# -eof meta-':
+ break
+ for pattern, handler in pats.items():
+ m = pattern.match(line.strip())
+ if m:
+ meta.update(handler(m))
+
+# -*- Installation Requires -*-
+
+py_version = sys.version_info
+
+
+def strip_comments(l):
+ return l.split('#', 1)[0].strip()
+
+
+def _pip_requirement(req):
+ if req.startswith('-r '):
+ _, path = req.split()
+ return reqs(*path.split('/'))
+ return [req]
+
+
+def _reqs(*f):
+ return [
+ _pip_requirement(r) for r in (
+ strip_comments(l) for l in open(
+ os.path.join(here, 'requirements', *f)).readlines()
+ ) if r]
+
+
+def reqs(*f):
+ return [req for subreq in _reqs(*f) for req in subreq]
+
+
+install_requires = reqs('default.txt')
+
+# -*- Tests Requires -*-
+
+tests_require = reqs('test.txt')
+
+# -*- Long Description -*-
+
+if os.path.exists('README.rst'):
+ long_description = codecs.open('README.rst', 'r', 'utf-8').read()
+else:
+ long_description = 'See http://pypi.python.org/pypi/schematics-xml'
+
+setup(
+ name=NAME,
+ version=meta['VERSION'],
+ description=meta['doc'],
+ author=meta['author'],
+ author_email=meta['contact'],
+ url=meta['homepage'],
+ platforms=['any'],
+ license='MIT',
+ packages=find_packages(),
+ package_data={'schematics_xml': ['tests/templates/*.html']},
+ zip_safe=False,
+ install_requires=install_requires,
+ tests_require=tests_require,
+ test_suite='nose.collector',
+ classifiers=classifiers,
+ entry_points=entrypoints,
+ long_description=long_description,
+ keywords=['schematics', 'xml', 'model', 'modelling', 'dicttoxml', 'xmltodict'],
+)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..ea823af
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,11 @@
+[tox]
+envlist =
+ py{33,34,35,py,py3}
+
+[testenv]
+sitepackages = False
+commands = pytest
+setenv = C_DEBUG_TEST = 1
+recreate = False
+deps =
+ -r{toxinidir}/requirements/test.txt