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