diff --git a/.gitignore b/.gitignore index 82e1c141..25df5c99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,141 @@ -*.pyc -*.pyo -*.wxs -test.log -/build/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv* +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# IDE files +.vscode/ + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/NOTICE b/NOTICE index 826a29e7..1a7c2e45 100644 --- a/NOTICE +++ b/NOTICE @@ -20,6 +20,7 @@ PyXB includes patches from the following Contributors: Michael van der Westhuizen Jon Foster + Eurofins Digital Testing (https://www.eurofins-digitaltesting.com/) For details on these patches see the git software configuration logs. diff --git a/README.md b/README.md new file mode 100644 index 00000000..872a12c7 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# PyXB Extended -- Python W3C XML Schema Bindings + +What is this fork for?: + +Essentially the exact same reasoning as Jon discusses below, but with the intention of having a PyPI-published copy available. + +## Installation + +`pip install PyXB-X` + +## Developer notes + +### Python 3.8 testing failures + +Currently, `python setup.py test` fails on Python 3.8+. This is a false failure caused by changes to the `toxml` method (https://docs.python.org/3/library/xml.dom.minidom.html#xml.dom.minidom.Node.toxml). + +In Python 3.8+ `toxml` preserves the original element order, which the current tests do not assume. Since < 3.8 produces a slightly different, but equally valid output, we can't easily just change the unit tests to match. + +This is something I'll work on sorting out soon. + + +--- + +Jon Foster (upstream) PyXB README follows: + +What is this fork for?: + +At work, I use a few closed-source tools that use PyXB - some of which I maintain. This repository has the necessary patches to make PyXB work for me. + +I don't have the time, inclination or the knowledge of PyXB's internals to be a proper open-source maintainer for PyXB. This fork is just getting bugfixes as I need them. I will accept small pull requests that fix bugs, but not anything big or risky or hard-to-test. I have no interest in doing formal public releases or submitting this to PyPy. + +My experience is that PyXB is very complex and seriously lacking documentation, and does not have a stable API, but it can be made to work with some trial-and-error. The concept is great, and it works, and I'm not aware of anything better (though I haven't looked for a couple of years). + +**-- Jon Foster** + +Original (upstream) PyXB README follows: + +The source releases includes pre-built bundles for common XML namespaces, +assorted web service namespaces, and SAML. A bundle with over 75 namespaces +related to Geographic Information Systems is also available; if you want +those, read pyxb/bundles/opengis/README.txt before installing PyXB. + +Installation: python setup.py install + +Documentation: doc/html or https://pabigot.github.io/pyxb/ + +Help Forum: http://sourceforge.net/forum/forum.php?forum_id=956708 + +Mailing list: https://lists.sourceforge.net/lists/listinfo/pyxb-users +Archive: http://www.mail-archive.com/pyxb-users@lists.sourceforge.net + +Bug reports: https://github.com/pabigot/pyxb/issues diff --git a/README.txt b/README.txt deleted file mode 100644 index 39c2c4d2..00000000 --- a/README.txt +++ /dev/null @@ -1,18 +0,0 @@ -PyXB -- Python W3C XML Schema Bindings -Version 1.2.6 - -The source releases includes pre-built bundles for common XML namespaces, -assorted web service namespaces, and SAML. A bundle with over 75 namespaces -related to Geographic Information Systems is also available; if you want -those, read pyxb/bundles/opengis/README.txt before installing PyXB. - -Installation: python setup.py install - -Documentation: doc/html or https://pabigot.github.io/pyxb/ - -Help Forum: http://sourceforge.net/forum/forum.php?forum_id=956708 - -Mailing list: https://lists.sourceforge.net/lists/listinfo/pyxb-users -Archive: http://www.mail-archive.com/pyxb-users@lists.sourceforge.net - -Bug reports: https://github.com/pabigot/pyxb/issues diff --git a/README.txt.in b/README.txt.in deleted file mode 100644 index d3516c56..00000000 --- a/README.txt.in +++ /dev/null @@ -1,18 +0,0 @@ -PyXB -- Python W3C XML Schema Bindings -Version @VERSION@ - -The source releases includes pre-built bundles for common XML namespaces, -assorted web service namespaces, and SAML. A bundle with over 75 namespaces -related to Geographic Information Systems is also available; if you want -those, read pyxb/bundles/opengis/README.txt before installing PyXB. - -Installation: python setup.py install - -Documentation: doc/html or https://pabigot.github.io/pyxb/ - -Help Forum: http://sourceforge.net/forum/forum.php?forum_id=956708 - -Mailing list: https://lists.sourceforge.net/lists/listinfo/pyxb-users -Archive: http://www.mail-archive.com/pyxb-users@lists.sourceforge.net - -Bug reports: https://github.com/pabigot/pyxb/issues diff --git a/doc/Images/BindingModel.jpg b/doc/Images/BindingModel.jpg old mode 100755 new mode 100644 diff --git a/doc/Images/CTDValidationExceptions.jpg b/doc/Images/CTDValidationExceptions.jpg old mode 100755 new mode 100644 diff --git a/doc/Images/ComponentModel.jpg b/doc/Images/ComponentModel.jpg old mode 100755 new mode 100644 diff --git a/doc/Images/ContentModel.jpg b/doc/Images/ContentModel.jpg old mode 100755 new mode 100644 diff --git a/doc/Images/FACAutomaton.jpg b/doc/Images/FACAutomaton.jpg old mode 100755 new mode 100644 diff --git a/doc/Images/Namespace.jpg b/doc/Images/Namespace.jpg old mode 100755 new mode 100644 diff --git a/doc/Images/NamespaceArchive.jpg b/doc/Images/NamespaceArchive.jpg old mode 100755 new mode 100644 diff --git a/doc/Images/NamespaceCore.jpg b/doc/Images/NamespaceCore.jpg old mode 100755 new mode 100644 diff --git a/doc/Images/RuntimeExceptions.jpg b/doc/Images/RuntimeExceptions.jpg old mode 100755 new mode 100644 diff --git a/doc/Images/ScopedDeclarations.jpg b/doc/Images/ScopedDeclarations.jpg old mode 100755 new mode 100644 diff --git a/doc/conf.py b/doc/conf.py index 36b75ea8..c006d84b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -43,7 +43,7 @@ # General information about the project. project = 'PyXB' -copyright = '2009-2017, Peter A. Bigot' +copyright = '2009-2021, Peter A. Bigot' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/doc/pyxb.eap b/doc/pyxb.eap old mode 100755 new mode 100644 diff --git a/editix-pyxb.pre b/editix-pyxb.pre old mode 100755 new mode 100644 diff --git a/examples/cablelabs/disabled-test.sh b/examples/cablelabs/disabled-test.sh index 5e84c356..07a534b8 100755 --- a/examples/cablelabs/disabled-test.sh +++ b/examples/cablelabs/disabled-test.sh @@ -1,3 +1,5 @@ +#! /bin/sh + rm -f cablelabs.wxs sh genbindings.sh \ && python demo.py diff --git a/examples/cablelabs/genbindings.sh b/examples/cablelabs/genbindings.sh index fb5b21f0..bf0e7fcc 100644 --- a/examples/cablelabs/genbindings.sh +++ b/examples/cablelabs/genbindings.sh @@ -1,3 +1,5 @@ +#! /bin/sh + # Note: Module order is in increasing dependency order so schema are # downloaded and saved only once. [ -f cablelabs.wxs ] \ diff --git a/examples/dictionary/genbindings.sh b/examples/dictionary/genbindings.sh index f6eea659..3cba58a0 100644 --- a/examples/dictionary/genbindings.sh +++ b/examples/dictionary/genbindings.sh @@ -1,3 +1,5 @@ +#! /bin/sh + URI='http://services.aonaware.com/DictService/DictService.asmx?WSDL' PREFIX='dict' WSDL="${PREFIX}.wsdl" diff --git a/examples/geocoder/disabled-test.sh b/examples/geocoder/disabled-test.sh index 949768a1..b97a61df 100755 --- a/examples/geocoder/disabled-test.sh +++ b/examples/geocoder/disabled-test.sh @@ -1,2 +1,4 @@ +#! /bin/sh + sh genbindings.sh \ && python client.py diff --git a/examples/geocoder/genbindings.sh b/examples/geocoder/genbindings.sh index 1397fe65..55a8a5a0 100644 --- a/examples/geocoder/genbindings.sh +++ b/examples/geocoder/genbindings.sh @@ -1,3 +1,5 @@ +#! /bin/sh + GEO_WSDL_URI='http://geocoder.us/dist/eg/clients/GeoCoder.wsdl' GEO_PREFIX=GeoCoder GEO_WSDL="${GEO_PREFIX}.wsdl" diff --git a/examples/kml/genbindings.sh b/examples/kml/genbindings.sh index 8971c549..51e1f7ce 100644 --- a/examples/kml/genbindings.sh +++ b/examples/kml/genbindings.sh @@ -1,3 +1,5 @@ +#! /bin/sh + PYTHONPATH=../.. export PYTHONPATH diff --git a/examples/manual/demo.sh b/examples/manual/demo.sh index c9bd86b4..1065a4cb 100644 --- a/examples/manual/demo.sh +++ b/examples/manual/demo.sh @@ -1,3 +1,5 @@ +#! /bin/sh + rm -f *.wxs po?.py *.pyc sh demo1.sh \ && python demo1.py > demo1.out \ diff --git a/examples/manual/demo1.sh b/examples/manual/demo1.sh index c1851dc3..e08b1777 100644 --- a/examples/manual/demo1.sh +++ b/examples/manual/demo1.sh @@ -1,2 +1,4 @@ +#! /bin/sh + pyxbgen \ -u po1.xsd -m po1 diff --git a/examples/manual/demo2.sh b/examples/manual/demo2.sh index 26c702ca..39cbaff4 100644 --- a/examples/manual/demo2.sh +++ b/examples/manual/demo2.sh @@ -1,2 +1,4 @@ +#! /bin/sh + pyxbgen \ -u po2.xsd -m po2 diff --git a/examples/manual/demo3a.sh b/examples/manual/demo3a.sh index bf71c4ed..db9a9b85 100644 --- a/examples/manual/demo3a.sh +++ b/examples/manual/demo3a.sh @@ -1,2 +1,4 @@ +#! /bin/sh + pyxbgen \ -u po3.xsd -m po3 diff --git a/examples/manual/demo3b.sh b/examples/manual/demo3b.sh index 34bfd4c6..eccddc42 100644 --- a/examples/manual/demo3b.sh +++ b/examples/manual/demo3b.sh @@ -1,3 +1,5 @@ +#! /bin/sh + pyxbgen \ -u po3.xsd -m po3 \ -u nsaddress.xsd -m address diff --git a/examples/manual/demo3c1.sh b/examples/manual/demo3c1.sh index 8fc69b91..68b6fbe6 100644 --- a/examples/manual/demo3c1.sh +++ b/examples/manual/demo3c1.sh @@ -1,3 +1,5 @@ +#! /bin/sh + pyxbgen \ -u nsaddress.xsd -m address \ --archive-to-file address.wxs diff --git a/examples/manual/demo3c2.sh b/examples/manual/demo3c2.sh index 94b22ed9..2798cc7c 100644 --- a/examples/manual/demo3c2.sh +++ b/examples/manual/demo3c2.sh @@ -1,3 +1,5 @@ +#! /bin/sh + pyxbgen \ -u po3.xsd -m po3 \ --archive-path .:+ diff --git a/examples/manual/demo4.sh b/examples/manual/demo4.sh index 7718e7d7..33a7721f 100644 --- a/examples/manual/demo4.sh +++ b/examples/manual/demo4.sh @@ -1,3 +1,5 @@ +#! /bin/sh + pyxbgen \ -u po4.xsd -m po4 \ --archive-path=.:+ diff --git a/examples/manual/demo4b.sh b/examples/manual/demo4b.sh index 94b22ed9..2798cc7c 100644 --- a/examples/manual/demo4b.sh +++ b/examples/manual/demo4b.sh @@ -1,3 +1,5 @@ +#! /bin/sh + pyxbgen \ -u po3.xsd -m po3 \ --archive-path .:+ diff --git a/examples/ndfd/forecast.py b/examples/ndfd/forecast.py index aff5c1eb..c669b90d 100644 --- a/examples/ndfd/forecast.py +++ b/examples/ndfd/forecast.py @@ -5,7 +5,7 @@ import pyxb.binding.datatypes as xsd from pyxb.utils.six.moves.urllib.request import urlopen import time -import collections +import collections.abc import sys # Get the next seven days forecast for two locations @@ -32,7 +32,7 @@ source = r.head.source print(", ".join(source.production_center.content())) data = r.data -if isinstance(data, collections.MutableSequence): +if isinstance(data, collections.abc.MutableSequence): data = data.pop(0) print(data) diff --git a/examples/ndfd/genbindings.sh b/examples/ndfd/genbindings.sh index 3b6e9886..288edf34 100644 --- a/examples/ndfd/genbindings.sh +++ b/examples/ndfd/genbindings.sh @@ -1,3 +1,5 @@ +#! /bin/sh + URI='http://graphical.weather.gov/xml/DWMLgen/schema/DWML.xsd' PREFIX='DWML' diff --git a/examples/tmsxtvd/disabled-test.sh b/examples/tmsxtvd/disabled-test.sh index 84d5e165..b9859018 100755 --- a/examples/tmsxtvd/disabled-test.sh +++ b/examples/tmsxtvd/disabled-test.sh @@ -1,2 +1,4 @@ +#! /bin/sh + sh genbindings.sh python dumpsample.py diff --git a/examples/tmsxtvd/genbindings.sh b/examples/tmsxtvd/genbindings.sh index 6f3a1571..21a7cc7b 100644 --- a/examples/tmsxtvd/genbindings.sh +++ b/examples/tmsxtvd/genbindings.sh @@ -1,3 +1,5 @@ +#! /bin/sh + URI='http://docs.tms.tribune.com/tech/xml/schemas/tmsxtvd.xsd' PREFIX='tmstvd' diff --git a/examples/weather/disabled-test.sh b/examples/weather/disabled-test.sh index 8c6e2635..f48285d6 100755 --- a/examples/weather/disabled-test.sh +++ b/examples/weather/disabled-test.sh @@ -1,2 +1,4 @@ +#! /bin/sh + sh genbindings.sh \ && python client_get.py diff --git a/examples/weather/genbindings.sh b/examples/weather/genbindings.sh index 74cd1cad..47e97d3d 100644 --- a/examples/weather/genbindings.sh +++ b/examples/weather/genbindings.sh @@ -1,3 +1,5 @@ +#! /bin/sh + WSDL_URI='http://ws.cdyne.com/WeatherWS/Weather.asmx?wsdl' WSDL_URI='http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL' wget -O weather.wsdl "${WSDL_URI}" diff --git a/examples/xsdprimer/genbindings.sh b/examples/xsdprimer/genbindings.sh index ad0a220b..1bb945c5 100644 --- a/examples/xsdprimer/genbindings.sh +++ b/examples/xsdprimer/genbindings.sh @@ -1,3 +1,5 @@ +#! /bin/sh + PYTHONPATH=../.. export PYTHONPATH rm -rf raw diff --git a/maintainer/bundlesupport.sh b/maintainer/bundlesupport.sh index 5cd3ae29..f72c396b 100644 --- a/maintainer/bundlesupport.sh +++ b/maintainer/bundlesupport.sh @@ -1,3 +1,5 @@ +#! /bin/sh + # This module is sourced by genbind scripts in bundle directories to # initialize a bundle area and provide a function to translate schema. diff --git a/maintainer/genpd.sh b/maintainer/genpd.sh index fa468e52..6b6bd6f5 100644 --- a/maintainer/genpd.sh +++ b/maintainer/genpd.sh @@ -1,3 +1,5 @@ +#! /bin/sh + find pyxb/bundles -name __init__.py \ | sed -e 's@^@"@' -e 's@/[^/]*$@",@' -e 's@/@.@g' \ | fmt diff --git a/maintainer/getw3c.sh b/maintainer/getw3c.sh index f441fa31..948f30d0 100755 --- a/maintainer/getw3c.sh +++ b/maintainer/getw3c.sh @@ -1,3 +1,5 @@ +#! /bin/sh + # Download a mirror of the W3C technical archive # --backup-converted diff --git a/maintainer/usepyxb.sh b/maintainer/usepyxb.sh index 62ff82f4..ae8f4298 100644 --- a/maintainer/usepyxb.sh +++ b/maintainer/usepyxb.sh @@ -1,3 +1,5 @@ +#! /bin/sh + PYXB_ROOT=${PYXB_ROOT:-/mnt/devel/pyxb} PYTHONPATH=${PYXB_ROOT}:${PYTHONPATH:+:${PYTHONPATH}} PATH="${PYXB_ROOT}/scripts:${PYXB_ROOT}/bin:${PATH}" diff --git a/pyxb/binding/basis.py b/pyxb/binding/basis.py index f624e0e6..4abe60b6 100644 --- a/pyxb/binding/basis.py +++ b/pyxb/binding/basis.py @@ -17,7 +17,7 @@ inherit, and that describe the content models of those schema.""" import logging -import collections +import collections.abc import xml.dom import pyxb from pyxb.utils import domutils, utility, six @@ -1023,7 +1023,8 @@ def XsdSuperType (cls): # otherwise directly descends from a Python type; return # the recorded XSD supertype. return cls._XsdBaseType - if issubclass(sc, simpleTypeDefinition): + if (not sc.__name__.startswith('_')) \ + and issubclass(sc, simpleTypeDefinition): return sc raise pyxb.LogicError('No supertype found for %s' % (cls,)) @@ -1163,7 +1164,7 @@ def _CheckValidValue (cls, value): raise pyxb.SimpleTypeValueError(cls, value) value_class = cls if issubclass(cls, STD_list): - if not isinstance(value, collections.Iterable): + if not isinstance(value, collections.abc.Iterable): raise pyxb.SimpleTypeValueError(cls, value) for v in value: if not cls._ItemType._IsValidValue(v): @@ -1363,7 +1364,7 @@ def _ConvertArguments_vx (cls, args, kw): if isinstance(arg1, six.string_types): args = (arg1.split(),) + args[1:] arg1 = args[0] - if isinstance(arg1, collections.Iterable): + if isinstance(arg1, collections.abc.Iterable): new_arg1 = [ cls._ValidatedItem(_v, kw) for _v in arg1 ] args = (new_arg1,) + args[1:] super_fn = getattr(super(STD_list, cls), '_ConvertArguments_vx', lambda *a,**kw: args) @@ -1646,7 +1647,7 @@ def compatibleValue (self, value, **kw): return self.__defaultValue is_plural = kw.pop('is_plural', False) if is_plural: - if not isinstance(value, collections.Iterable): + if not isinstance(value, collections.abc.Iterable): raise pyxb.SimplePluralValueError(self.typeDefinition(), value) return [ self.compatibleValue(_v) for _v in value ] compValue = self.typeDefinition()._CompatibleValue(value, **kw); diff --git a/pyxb/binding/content.py b/pyxb/binding/content.py index cce24c61..17a17440 100644 --- a/pyxb/binding/content.py +++ b/pyxb/binding/content.py @@ -796,7 +796,7 @@ def __init__ (self, wildcard_declaration, xsd_location): def __str__ (self): return 'xs:any per %s' % (self.xsdLocation(),) -import collections +import collections.abc # Do not inherit from list; that's obscene, and could cause problems with the # internal assumptions made by Python. Instead delegate everything to an @@ -804,7 +804,7 @@ def __str__ (self): # represents list-style data structures so we can identify both lists and # these things which are not lists. @pyxb.utils.utility.BackfillComparisons -class _PluralBinding (collections.MutableSequence): +class _PluralBinding (collections.abc.MutableSequence): """Helper for element content that supports multiple occurences. This is an adapter for Python list. Any operation that can mutate an item diff --git a/pyxb/binding/datatypes.py b/pyxb/binding/datatypes.py index 2aa1664e..f92e453f 100644 --- a/pyxb/binding/datatypes.py +++ b/pyxb/binding/datatypes.py @@ -53,6 +53,7 @@ import pyxb.namespace import pyxb.utils.unicode from pyxb.utils import six +from pyxb.utils.typeutils import make_base_dt from . import basis _log = logging.getLogger(__name__) @@ -578,7 +579,7 @@ def aslocal (self): @rtype: C{datetime.datetime} (B{NOT} C{xsd.dateTime}) """ - dt = self + dt = make_base_dt(self) if dt.tzinfo is None: dt = dt.replace(tzinfo=self._UTCTimeZone) return dt.astimezone(self._LocalTimeZone) @@ -669,6 +670,7 @@ def __new__ (cls, *args, **kw): except AttributeError: pass else: + print(args) fi = 0 while fi < len(cls._ValidFields): fn = cls._ValidFields[fi] @@ -749,22 +751,25 @@ def xsdRecoverableTzinfo (self): @classmethod def XsdLiteral (cls, value): + # Create a copy of `value` using datetime baseclass + # This allows us to do datetime arithmetic on Python 3.8+ + base_value = make_base_dt(value) # Work around strftime year restriction fmt = cls._Lexical_fmt rtz = value.xsdRecoverableTzinfo() if rtz is not None: # If the date is timezoned, convert it to UTC - value -= value.tzinfo.utcoffset(value) - value = value.replace(tzinfo=cls._UTCTimeZone) + base_value -= base_value.tzinfo.utcoffset(value) + base_value = base_value.replace(tzinfo=cls._UTCTimeZone) # Use the midpoint of the one-day interval to get the correct # month/day. - value += datetime.timedelta(minutes=cls.__MinutesPerHalfDay) - if value.year < 1900: - fmt = fmt.replace('%Y', '%04d' % (value.year,)) - value = value.replace(year=1900) + base_value += datetime.timedelta(minutes=cls.__MinutesPerHalfDay) + if base_value.year < 1900: + fmt = fmt.replace('%Y', '%04d' % (base_value.year,)) + base_value = base_value.replace(year=1900) if rtz is not None: - fmt += rtz.tzname(value) - return value.strftime(fmt) + fmt += rtz.tzname(base_value) + return base_value.strftime(fmt) _PrimitiveDatatypes.append(date) diff --git a/pyxb/binding/generate.py b/pyxb/binding/generate.py index a05b58eb..38c8f561 100644 --- a/pyxb/binding/generate.py +++ b/pyxb/binding/generate.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2009-2013, Peter A. Bigot +# Copyright 2018 Eurofins Digital Product Testing UK Ltd - https://www.eurofins-digitaltesting.com/ # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain a @@ -33,6 +34,9 @@ _log = logging.getLogger(__name__) +_StripFilePaths = False +_NoTimestamp = False + def PrefixModule (value, text=None): if text is None: text = value.__name__ @@ -42,6 +46,22 @@ def PrefixModule (value, text=None): return 'pyxb.binding.facets.%s' % (text,) raise ValueError('No standard name for module of value', value) +def _LocationRepr(loc): + '''Return the representation of a Location object, as a string. + + This will remove the directory part of file paths, if requested. + + There are two reasons for this: It hides potentially-sensitive file paths + (e.g. usernames if building in My Documents on Windows or in a user's home + directory on Unix systems). It also helps ensure that builds are + reproducible - if different users build the same bindings from the same + source files they should end up with the same binding Python files, even if + they build in different directories or on different OSs. + ''' + if _StripFilePaths and loc is not None: + loc = loc.withFilePathsRemoved() + return repr2to3(loc) + class ReferenceLiteral (object): """Base class for something that requires fairly complex activity in order to generate its literal value.""" @@ -193,7 +213,12 @@ def __init__ (self, **kw): def pythonLiteral (value, **kw): # For dictionaries, apply translation to all values (not keys) if isinstance(value, six.dictionary_type): - return ', '.join([ '%s=%s' % (k, pythonLiteral(v, **kw)) for (k, v) in six.iteritems(value) ]) + entries = [ (k, pythonLiteral(v, **kw)) for (k, v) in six.iteritems(value) ] + # It's important to sort the entries here, so we reproducibly generate + # the same code for the same input. It really doesn't matter *how* + # they're sorted. + entries.sort() + return ', '.join(('%s=%s' % (k, v)) for (k, v) in entries) # For lists, apply translation to all members if isinstance(value, six.list_type): @@ -306,7 +331,7 @@ def transitionSortKey (xit): for cc in sorted_counter_conditions: cc_id = 'cc_%u' % (len(counter_map),) counter_map[cc] = cc_id - au_src.append(' %s = fac.CounterCondition(min=%s, max=%s, metadata=%r)' % (cc_id, repr2to3(cc.min), repr2to3(cc.max), cc.metadata._location())) + au_src.append(' %s = fac.CounterCondition(min=%s, max=%s, metadata=%s)' % (cc_id, repr2to3(cc.min), repr2to3(cc.max), _LocationRepr(cc.metadata._location()))) au_src.append(' counters.add(%s)' % (cc_id,)) state_map = {} au_src.append(' states = []') @@ -325,14 +350,14 @@ def transitionSortKey (xit): for ui in sorted(st.finalUpdate, key=updateInstructionSortKey): au_src.append(' final_update.add(fac.UpdateInstruction(%s, %r))' % (counter_map[ui.counterCondition], ui.doIncrement)) if isinstance(st.symbol, xs.structures.ModelGroup): - au_src.append(' symbol = %r' % (st.symbol._location(),)) + au_src.append(' symbol = %s' % (_LocationRepr(st.symbol._location()),)) else: (particle, symbol) = st.symbol if isinstance(symbol, xs.structures.Wildcard): - au_src.append(templates.replaceInText(' symbol = pyxb.binding.content.WildcardUse(%{wildcard}, %{location})', wildcard=binding_module.literal(symbol, **kw), location=repr2to3(particle._location()))) + au_src.append(templates.replaceInText(' symbol = pyxb.binding.content.WildcardUse(%{wildcard}, %{location})', wildcard=binding_module.literal(symbol, **kw), location=_LocationRepr(particle._location()))) elif isinstance(symbol, xs.structures.ElementDeclaration): binding_module.importForDeclaration(symbol) - au_src.append(templates.replaceInText(' symbol = pyxb.binding.content.ElementUse(%{ctd}._UseForTag(%{field_tag}), %{location})', field_tag=binding_module.literal(symbol.expandedName(), **kw), location=repr2to3(particle._location()), **template_map)) + au_src.append(templates.replaceInText(' symbol = pyxb.binding.content.ElementUse(%{ctd}._UseForTag(%{field_tag}), %{location})', field_tag=binding_module.literal(symbol.expandedName(), **kw), location=_LocationRepr(particle._location()), **template_map)) au_src.append(' %s = fac.State(symbol, is_initial=%r, final_update=final_update, is_unordered_catenation=%r)' % (st_id, st.isInitial, st.isUnorderedCatenation)) if st.subAutomata is not None: au_src.append(' %s._set_subAutomata(*sub_automata)' % (st_id,)) @@ -406,8 +431,9 @@ def GenerateFacets (td, generator, **kw): if isinstance(fi, facets.CF_enumeration): argset['enum_prefix'] = fi.enumPrefix() facet_var = ReferenceFacetMember(type_definition=td, facet_class=fc, **kw) - outf.write("%s = %s(%s)\n" % binding_module.literal( (facet_var, fc, argset ), **kw)) - facet_instances.append(binding_module.literal(facet_var, **kw)) + facet_code = [] + facet_instances.append((binding_module.literal(facet_var, **kw), facet_code)) + facet_code.append("%s = %s(%s)\n" % binding_module.literal( (facet_var, fc, argset ), **kw)) if (fi is not None) and is_collection: for i in six.iteritems(fi): if isinstance(i, facets._EnumerationElement): @@ -417,13 +443,20 @@ def GenerateFacets (td, generator, **kw): enum_config = '%s.addEnumeration(unicode_value=%s, tag=%s)' % binding_module.literal( ( facet_var, i.unicodeValue(), i.tag() ), **kw) if gen_enum_tag and (i.tag() is not None): enum_member = ReferenceEnumerationMember(type_definition=td, facet_instance=fi, enumeration_element=i, **kw) - outf.write("%s = %s\n" % (binding_module.literal(enum_member, **kw), enum_config)) + facet_code.append("%s = %s\n" % (binding_module.literal(enum_member, **kw), enum_config)) if fi.enumPrefix() is not None: - outf.write("%s_%s = %s\n" % (fi.enumPrefix(), i.tag(), binding_module.literal(enum_member, **kw))) + facet_code.append("%s_%s = %s\n" % (fi.enumPrefix(), i.tag(), binding_module.literal(enum_member, **kw))) else: - outf.write("%s\n" % (enum_config,)) + facet_code.append("%s\n" % (enum_config,)) if isinstance(i, facets._PatternElement): - outf.write("%s.addPattern(pattern=%s)\n" % binding_module.literal( (facet_var, i.pattern ), **kw)) + facet_code.append("%s.addPattern(pattern=%s)\n" % binding_module.literal( (facet_var, i.pattern ), **kw)) + # It's important to sort the facets here, so we reproducibly generate + # the same code for the same input. It really doesn't matter *how* + # they're sorted. + facet_instances.sort() + for _, facet_code in facet_instances: + for s in facet_code: + outf.write(s) if gen_enum_tag and (xs.structures.SimpleTypeDefinition.VARIETY_union == td.variety()): # If the union has enumerations of its own, there's no need to # inherit anything, because they supersede anything implicitly @@ -444,9 +477,9 @@ def GenerateFacets (td, generator, **kw): outf.write("%-50s%s\n" % ('%s = %s' % binding_module.literal( (enum_member, i.unicodeValue()) ), '# originally %s.%s' % (binding_module.literal(etd), i.tag()))) if 2 <= len(facet_instances): - map_args = ",\n ".join(facet_instances) + map_args = ",\n ".join(facet_var for facet_var, _ in facet_instances) else: - map_args = ','.join(facet_instances) + map_args = ','.join(facet_var for facet_var, _ in facet_instances) outf.write("%s._InitializeFacetMap(%s)\n" % (binding_module.literal(td, **kw), map_args)) def _VCAppendAuxInit (vc_source, aux_init, binding_module, kw): @@ -497,7 +530,7 @@ def GenerateSTD (std, generator): else: template_map['qname'] = '[anonymous]' template_map['namespaceReference'] = binding_module.literal(std.bindingNamespace(), **kw) - template_map['xsd_location'] = repr2to3(std._location()) + template_map['xsd_location'] = _LocationRepr(std._location()) if std.annotation() is not None: template_map['documentation'] = std.annotation().asDocString() template_map['documentation_expr'] = binding_module.literal(std.annotation().text()) @@ -565,7 +598,7 @@ class %{std} (pyxb.binding.basis.STD_union): def elementDeclarationMap (ed, binding_module, **kw): template_map = { } template_map['qname'] = six.text_type(ed.expandedName()) - template_map['decl_location'] = repr2to3(ed._location()) + template_map['decl_location'] = _LocationRepr(ed._location()) template_map['namespaceReference'] = binding_module.literal(ed.bindingNamespace(), **kw) if (ed.SCOPE_global == ed.scope()): binding_name = template_map['class'] = binding_module.literal(ed, **kw) @@ -871,7 +904,7 @@ def GenerateCTD (ctd, generator, **kw): template_map['qname'] = six.text_type(ctd.expandedName()) else: template_map['qname'] = '[anonymous]' - template_map['xsd_location'] = repr2to3(ctd._location()) + template_map['xsd_location'] = _LocationRepr(ctd._location()) template_map['simple_base_type'] = binding_module.literal(None, **kw) template_map['contentTypeTag'] = content_type_tag template_map['is_abstract'] = repr2to3(not not ctd.abstract()) @@ -1024,8 +1057,8 @@ class %{ctd} (%{superclass}): assert ad.typeDefinition() is not None au_map['attr_type'] = binding_module.literal(ad.typeDefinition(), in_class=True, **kw) - au_map['decl_location'] = repr2to3(ad._location()) - au_map['use_location'] = repr2to3(au._location()) + au_map['decl_location'] = _LocationRepr(ad._location()) + au_map['use_location'] = _LocationRepr(au._location()) vc_source = ad if au.valueConstraint() is not None: @@ -1164,8 +1197,13 @@ def __init__ (self, binding_module, **kw): self.__postscript = [] self.__templateMap = kw.copy() encoding = kw.get('encoding', pyxb._OutputEncoding) + # It's important to replace the OS-dependent path separator with + # a fixed value here, so we reproducibly generate the same code + # for the same input regardless of what OS this is run on. + # It really doesn't matter what separator we use, so long as it's + # consistent, so we use '/'. self.__templateMap.update({ 'date' : str(datetime.datetime.now()), - 'filePath' : self.__bindingFilePath, + 'filePath' : self.__bindingFilePath.replace(os.path.sep, '/'), 'coding' : encoding, 'binding_module' : binding_module, 'binding_tag' : binding_module.bindingTag(), @@ -1174,11 +1212,19 @@ def __init__ (self, binding_module, **kw): 'pythonVersion' : '.'.join(map(str, sys.version_info))}) self.__stringIO = io.StringIO() if self.__bindingFile: - prefacet = self.expand('''# %{filePath} + if _NoTimestamp: + prefacet_template = '''# %{filePath} +# -*- coding: %{coding} -*- +# PyXB bindings for %{binding_tag} +# Generated by PyXB version %{pyxbVersion} +%{binding_preface}''' + else: + prefacet_template = '''# %{filePath} # -*- coding: %{coding} -*- # PyXB bindings for %{binding_tag} # Generated %{date} by PyXB version %{pyxbVersion} using Python %{pythonVersion} -%{binding_preface}''', binding_preface=binding_module.bindingPreface()) +%{binding_preface}''' + prefacet = self.expand(prefacet_template, binding_preface=binding_module.bindingPreface()) self.__bindingFile.write(prefacet.encode(encoding)) self.__bindingFile.flush() @@ -1196,9 +1242,6 @@ def appendPrologBoilerplate (self, tm): # Version of PyXB used to generate the bindings _PyXBVersion = %{pyxb_version} -# Generated bindings are not compatible across PyXB versions -if pyxb.__version__ != _PyXBVersion: - raise pyxb.PyXBVersionError(_PyXBVersion) # A holder for module-level binding classes so we can access them from # inside class definitions where property names may conflict. @@ -1362,6 +1405,10 @@ def moduleContents (self): aux_imports.append('import %s as %s' % (mr.modulePath(), as_path)) else: aux_imports.append('import %s' % (mr.modulePath(),)) + # It's important to sort the imports here, so we reproducibly generate + # the same code for the same input. It really doesn't matter *how* + # they're sorted. + aux_imports.sort() template_map['aux_imports'] = "\n".join(aux_imports) template_map['namespace_decls'] = "\n".join(self.__namespaceDeclarations) template_map['module_uid'] = self.moduleUID() @@ -1675,7 +1722,7 @@ def _finalizeModuleContents_vx (self, template_map): ''') self.bindingIO().appendPrologBoilerplate(template_map) self.bindingIO().prolog().append(self.bindingIO().expand(''' -def CreateFromDocument (xml_text, default_namespace=None, location_base=None): +def CreateFromDocument (xml_text, fallback_namespace=None, location_base=None, default_namespace=None): """Parse the given XML and use the document element to create a Python instance. @@ -1683,23 +1730,30 @@ def CreateFromDocument (xml_text, default_namespace=None, location_base=None): str or Python 3 bytes), or a text (Python 2 unicode or Python 3 str) in the L{pyxb._InputEncoding} encoding. - @keyword default_namespace The L{pyxb.Namespace} instance to use as the - default namespace where there is no default namespace in scope. - If unspecified or C{None}, the namespace of the module containing - this function will be used. + @keyword fallback_namespace An absent L{pyxb.Namespace} instance + to use for unqualified names when there is no default namespace in + scope. If unspecified or C{None}, the namespace of the module + containing this function will be used, if it is an absent + namespace. @keyword location_base: An object to be recorded as the base of all L{pyxb.utils.utility.Location} instances associated with events and objects handled by the parser. You might pass the URI from which the document was obtained. + + @keyword default_namespace An alias for @c fallback_namespace used + in PyXB 1.1.4 through 1.2.6. It behaved like a default namespace + only for absent namespaces. """ if pyxb.XMLStyle_saxer != pyxb._XMLStyle: dom = pyxb.utils.domutils.StringToDOM(xml_text) - return CreateFromDOM(dom.documentElement, default_namespace=default_namespace) - if default_namespace is None: - default_namespace = Namespace.fallbackNamespace() - saxer = pyxb.binding.saxer.make_parser(fallback_namespace=default_namespace, location_base=location_base) + return CreateFromDOM(dom.documentElement) + if fallback_namespace is None: + fallback_namespace = default_namespace + if fallback_namespace is None: + fallback_namespace = Namespace.fallbackNamespace() + saxer = pyxb.binding.saxer.make_parser(fallback_namespace=fallback_namespace, location_base=location_base) handler = saxer.getContentHandler() xmld = xml_text if isinstance(xmld, %{_TextType}): @@ -1708,14 +1762,16 @@ def CreateFromDocument (xml_text, default_namespace=None, location_base=None): instance = handler.rootObject() return instance -def CreateFromDOM (node, default_namespace=None): +def CreateFromDOM (node, fallback_namespace=None, default_namespace=None): """Create a Python instance from the given DOM node. The node tag must correspond to an element declaration in this module. @deprecated: Forcing use of DOM interface is unnecessary; use L{CreateFromDocument}.""" - if default_namespace is None: - default_namespace = Namespace.fallbackNamespace() - return pyxb.binding.basis.element.AnyCreateFromDOM(node, default_namespace) + if fallback_namespace is None: + fallback_namespace = default_namespace + if fallback_namespace is None: + fallback_namespace = Namespace.fallbackNamespace() + return pyxb.binding.basis.element.AnyCreateFromDOM(node, fallback_namespace) ''', **template_map)) @@ -2305,6 +2361,44 @@ def setLoggingConfigFile (self, logging_config_file): self.__loggingConfigFile = logging_config_file __loggingConfigFile = None + def stripFilePaths (self): + """Don't store schema file paths in the generated bindings, only store + filenames. This hides potentially-sensitive file paths (e.g. + usernames), and helps ensure that builds are reproducible. + + @rtype: C{bool}""" + return self.__stripFilePaths + def setStripFilePaths (self, value): + self.__stripFilePaths = value + global _StripFilePaths + _StripFilePaths = value + __stripFilePaths = None + + def noTimestamp (self): + """Don't store timestamps and Python version in comments in the + generated bindings. This helps ensure that builds are reproducible. + + @rtype: C{bool}""" + return self.__stripFilePaths + def setNoTimestamp (self, value): + self.__noTimestamp = value + global _NoTimestamp + _NoTimestamp = value + __noTimestamp = None + + def generationUID (self): + """A unique identifier associated with this Generator instance. + + This is an instance of L{pyxb.utils.utility.UniqueIdentifier}. + Its associated objects are + L{pyxb.namespace.archive._SchemaOrigin} instances, which + identify schema that contribute to the definition of a + namespace.""" + return self.__generationUID + def setGenerationUID (self, value): + self.__generationUID = pyxb.utils.utility.UniqueIdentifier(value) + __generationUID = None + def __init__ (self, *args, **kw): """Create a configuration to be used for generating bindings. @@ -2335,6 +2429,8 @@ def __init__ (self, *args, **kw): @keyword generate_to_files: Sets L{generateToFiles} @keyword uri_content_archive_directory: Invokes L{setUriContentArchiveDirectory} @keyword logging_config_file: Invokes L{setLoggingConfigFile} + @keyword strip_file_paths: Invokes L{setStripFilePaths} + @keyword no_timestamp: Invokes L{setNoTimestamp} """ argv = kw.get('argv') if argv is not None: @@ -2364,14 +2460,20 @@ def __init__ (self, *args, **kw): self.__generateToFiles = kw.get('generate_to_files', True) self.__uriContentArchiveDirectory = kw.get('uri_content_archive_directory') self.__loggingConfigFile = kw.get('logging_config_file') + self.__stripFilePaths = kw.get('strip_file_paths', False) + self.__noTimestamp = kw.get('no_timestamp', False) self.__unnamedModulePaths = set() + generationUID = kw.get('generation_uid', None) + if generationUID is None: + self.__generationUID = pyxb.utils.utility.UniqueIdentifier() + else: + self.__generationUID = pyxb.utils.utility.UniqueIdentifier(generationUID) + if argv is not None: self.applyOptionValues(*self.optionParser().parse_args(argv)) [ self.addSchemaLocation(_a) for _a in args ] - self.__generationUID = pyxb.utils.utility.UniqueIdentifier() - pyxb.namespace.XML.validateComponentModel() __stripSpaces_re = re.compile('\s\s\s+') @@ -2396,7 +2498,10 @@ def __stripSpaces (self, string): ('allow_builtin_generation', setAllowBuiltinGeneration), ('allow_absent_module', setAllowAbsentModule), ('uri_content_archive_directory', setUriContentArchiveDirectory), - ('logging_config_file', setLoggingConfigFile) + ('logging_config_file', setLoggingConfigFile), + ('strip_file_paths', setStripFilePaths), + ('no_timestamp', setNoTimestamp), + ('generation_uid', setGenerationUID) ) def applyOptionValues (self, options, args=None): for (tag, method) in self.__OptionSetters: @@ -2419,17 +2524,6 @@ def setFromCommandLine (self, argv=None): self.applyOptionValues(options, args) return self - def generationUID (self): - """A unique identifier associated with this Generator instance. - - This is an instance of L{pyxb.utils.utility.UniqueIdentifier}. - Its associated objects are - L{pyxb.namespace.archive._SchemaOrigin} instances, which - identify schema that contribute to the definition of a - namespace.""" - return self.__generationUID - __generationUID = None - def optionParser (self, reset=False): """Return an C{optparse.OptionParser} instance tied to this configuration. @@ -2510,6 +2604,18 @@ def optionParser (self, reset=False): help=self.__stripSpaces(self.validateChanges.__doc__ + ' This option turns off validation.')) parser.add_option_group(group) + group = optparse.OptionGroup(parser, 'Reproducible builds', "Options to help ensure that bindings are always generated the same.") + group.add_option('--strip-file-paths', + action="store_true", dest='strip_file_paths', + help=self.__stripSpaces(self.stripFilePaths.__doc__)) + group.add_option('--no-timestamp', + action="store_true", dest='no_timestamp', + help=self.__stripSpaces(self.noTimestamp.__doc__)) + group.add_option('--generation-uid', + dest='generation_uid', + help='Unique ID for this generation run. A urn:uuid: value. Defaults to a random value.') + parser.add_option_group(group) + group = optparse.OptionGroup(parser, 'Miscellaneous Options', "Anything else.") group.add_option('--logging-config-file', metavar="FILE", help=self.__stripSpaces(self.loggingConfigFile.__doc__)) diff --git a/pyxb/utils/domutils.py b/pyxb/utils/domutils.py index 9c062d58..e050c43f 100644 --- a/pyxb/utils/domutils.py +++ b/pyxb/utils/domutils.py @@ -550,6 +550,15 @@ def _deepClone (self, node, docnode): return clone_node if node.COMMENT_NODE == node.nodeType: return docnode.createComment(node.data) + if node.CDATA_SECTION_NODE == node.nodeType: + # Technically Python DOM allows the nodeValue to be None, though + # the `data` property of a CDATASection object is supposed to only + # be a string. Do the check anyway, so if a user encounters this + # we can provide a solution that meets the actual need rather than + # silently substituting something unexpected. + if isinstance(node.nodeValue, six.string_types): + return docnode.createTextNode(node.data) + raise ValueError('DOM node from non-text CDATA not supported in clone', node.nodeValue) raise ValueError('DOM node not supported in clone', node) def cloneIntoImplementation (self, node): diff --git a/pyxb/utils/typeutils.py b/pyxb/utils/typeutils.py new file mode 100644 index 00000000..2b946579 --- /dev/null +++ b/pyxb/utils/typeutils.py @@ -0,0 +1,21 @@ +import datetime + +def make_base_dt(value): + """ + Create a copy of `value` using datetime baseclass. This allows us to do datetime arithmetic on Python 3.8+ + Args: + value (pyxb.binding.datatypes.datetime): PyXB datetime object + + Returns: + datetime.datetime: Matching datetime base instance + """ + return datetime.datetime( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.microsecond, + tzinfo=value.tzinfo + ) diff --git a/pyxb/utils/unicode.py b/pyxb/utils/unicode.py index e330c982..e2dacc2c 100644 --- a/pyxb/utils/unicode.py +++ b/pyxb/utils/unicode.py @@ -80,8 +80,11 @@ class is used to represent a set of code points in a manner # codepoints 12, 13, 14, and everything above 199. __codepoints = None + # Whether this object is frozen (immutable) + __frozen = False + def _codepoints (self): - """For testing purrposes only, access to the codepoints + """For testing purposes only, access to the codepoints internal representation.""" return self.__codepoints @@ -90,7 +93,7 @@ def __hash__ (self): def __eq__ (self, other): """Equality is delegated to the codepoints list.""" - return self.__codepoints == other.__codepoints + return isinstance(other, CodePointSet) and self.__codepoints == other.__codepoints def __lt__ (self, other): return self.__codepoints < other.__codepoints @@ -113,7 +116,7 @@ def __mutate (self, value, do_add): (s, e) = value e += 1 elif isinstance(value, six.string_types): - if 1 < len(value): + if 1 != len(value): raise TypeError() s = ord(value) e = s+1 @@ -125,10 +128,12 @@ def __mutate (self, value, do_add): # Validate the range for the code points supported by this # Python interpreter. Recall that e is exclusive. - if s > self.MaxCodePoint: + if s > self.MaxCodePoint or e <= 0: return self + if s < 0: + s = 0 if e > self.MaxCodePoint: - e = self.MaxCodePoint+1 + e = self.MaxCodePoint + 1 # Index of first code point equal to or greater than s li = bisect.bisect_left(self.__codepoints, s) @@ -161,6 +166,7 @@ def add (self, value): tuple C{(s,e)} denoting the start and end (inclusive) code points in a range. @return: C{self}""" + assert not self.__frozen return self.__mutate(value, True) def extend (self, values): @@ -170,6 +176,7 @@ def extend (self, values): whose members are valid parameters to L{add}. @return: C{self}""" + assert not self.__frozen if isinstance(values, CodePointSet): self.extend(values.asTuples()) else: @@ -185,6 +192,7 @@ def subtract (self, value): range, or a L{CodePointSet}. @return: C{self}""" + assert not self.__frozen if isinstance(value, CodePointSet): for v in value.asTuples(): self.subtract(v) @@ -217,7 +225,7 @@ def subtract (self, value): def __unichr (self, code_point): rv = six.unichr(code_point) if 0 == code_point: - rv = six.u('x00') + rv = six.text_type('x00') if code_point in self.__XMLtoPythonREEscapedCodepoints: rv = six.unichr(0x5c) + rv return rv @@ -240,7 +248,7 @@ def asPattern (self, with_brackets=True): if s == e: rva.append(self.__unichr(s)) else: - rva.extend([self.__unichr(s), '-', self.__unichr(e)]) + rva.extend([self.__unichr(s), six.u('-'), self.__unichr(e)]) if with_brackets: rva.append(six.u(']')) return six.u('').join(rva) @@ -281,6 +289,22 @@ def asSingleCharacter (self): return None return six.unichr(self.__codepoints[0]) + def freeze (self): + """Freezes this object, preventing further changes. + + Attempting to modify a frozen object will assert if assertions are + enabled. If assertions are disabled then you can still modify the + object. + + Intended as a development/debugging aid, to prevent accidentally + changing objects that are used as global constants. + + @return: C{self} + """ + self.__frozen = True + return self + + from pyxb.utils.unicode_data import PropertyMap from pyxb.utils.unicode_data import BlockMap @@ -316,6 +340,7 @@ class XML1p0e2 (object): ) if SupportsWideUnicode: Char.add( ( 1+CodePointSet.MaxShortCodePoint, CodePointSet.MaxCodePoint ) ) + Char.freeze() BaseChar = CodePointSet( ( 0x0041, 0x005A ), @@ -520,15 +545,15 @@ class XML1p0e2 (object): ( 0x30A1, 0x30FA ), ( 0x3105, 0x312C ), ( 0xAC00, 0xD7A3 ) - ) + ).freeze() Ideographic = CodePointSet( ( 0x4E00, 0x9FA5 ), 0x3007, ( 0x3021, 0x3029 ) - ) + ).freeze() - Letter = CodePointSet(BaseChar).extend(Ideographic) + Letter = CodePointSet(BaseChar).extend(Ideographic).freeze() CombiningChar = CodePointSet( ( 0x0300, 0x0345 ), @@ -626,7 +651,7 @@ class XML1p0e2 (object): ( 0x302A, 0x302F ), 0x3099, 0x309A - ) + ).freeze() Digit = CodePointSet( ( 0x0030, 0x0039 ), @@ -644,7 +669,7 @@ class XML1p0e2 (object): ( 0x0E50, 0x0E59 ), ( 0x0ED0, 0x0ED9 ), ( 0x0F20, 0x0F29 ) - ) + ).freeze() Extender = CodePointSet( 0x00B7, @@ -658,15 +683,17 @@ class XML1p0e2 (object): ( 0x3031, 0x3035 ), ( 0x309D, 0x309E ), ( 0x30FC, 0x30FE ) - ) + ).freeze() # Not an explicit production, but used in Name production NameStartChar = CodePointSet(Letter) NameStartChar.add(ord('_')) NameStartChar.add(ord(':')) + NameStartChar.freeze() NCNameStartChar = CodePointSet(Letter) NCNameStartChar.add(ord('_')) + NCNameStartChar.freeze() NameChar = CodePointSet(Letter) NameChar.extend(Digit) @@ -676,6 +703,7 @@ class XML1p0e2 (object): NameChar.add(ord(':')) NameChar.extend(CombiningChar) NameChar.extend(Extender) + NameChar.freeze() NCNameChar = CodePointSet(Letter) NCNameChar.extend(Digit) @@ -684,6 +712,7 @@ class XML1p0e2 (object): NCNameChar.add(ord('_')) NCNameChar.extend(CombiningChar) NCNameChar.extend(Extender) + NCNameChar.freeze() Name_pat = '%s%s*' % (NameStartChar.asPattern(), NameChar.asPattern()) Name_re = re.compile('^%s$' % (Name_pat,)) @@ -695,36 +724,38 @@ class XML1p0e2 (object): QName_re = re.compile('^%s$' % (QName_pat,)) # Production 24 : Single Character Escapes -SingleCharEsc = { 'n' : CodePointSet(0x0A), - 'r' : CodePointSet(0x0D), - 't' : CodePointSet(0x09) } +SingleCharEsc = { 'n' : CodePointSet(0x0A).freeze(), + 'r' : CodePointSet(0x0D).freeze(), + 't' : CodePointSet(0x09).freeze() } for c in r'\|.-^?*+{}()[]': - SingleCharEsc[c] = CodePointSet(ord(c)) + SingleCharEsc[c] = CodePointSet(ord(c)).freeze() # Production 25 : Category Escapes # Production 26: Complemented Category Escapes catEsc = { } complEsc = { } for k, v in six.iteritems(PropertyMap): + v.freeze() catEsc[six.u('p{%s}') % (k,)] = v - catEsc[six.u('P{%s}') % (k,)] = v.negate() + complEsc[six.u('P{%s}') % (k,)] = v.negate().freeze() # Production 36 : IsBlock escapes IsBlockEsc = { } for k, v in six.iteritems(BlockMap): + v.freeze() IsBlockEsc[six.u('p{Is%s}') % (k,)] = v - IsBlockEsc[six.u('P{Is%s}') % (k,)] = v.negate() + IsBlockEsc[six.u('P{Is%s}') % (k,)] = v.negate().freeze() # Production 37 : Multi-Character Escapes -WildcardEsc = CodePointSet(ord('\n'), ord('\r')).negate() +WildcardEsc = CodePointSet(ord('\n'), ord('\r')).negate().freeze() MultiCharEsc = { } -MultiCharEsc['s'] = CodePointSet(0x20, ord('\t'), ord('\n'), ord('\r')) -MultiCharEsc['S'] = MultiCharEsc['s'].negate() -MultiCharEsc['i'] = CodePointSet(XML1p0e2.Letter).add(ord('_')).add(ord(':')) -MultiCharEsc['I'] = MultiCharEsc['i'].negate() -MultiCharEsc['c'] = CodePointSet(XML1p0e2.NameChar) -MultiCharEsc['C'] = MultiCharEsc['c'].negate() +MultiCharEsc['s'] = CodePointSet(0x20, ord('\t'), ord('\n'), ord('\r')).freeze() +MultiCharEsc['S'] = MultiCharEsc['s'].negate().freeze() +MultiCharEsc['i'] = CodePointSet(XML1p0e2.Letter).add(ord('_')).add(ord(':')).freeze() +MultiCharEsc['I'] = MultiCharEsc['i'].negate().freeze() +MultiCharEsc['c'] = CodePointSet(XML1p0e2.NameChar).freeze() +MultiCharEsc['C'] = MultiCharEsc['c'].negate().freeze() MultiCharEsc['d'] = PropertyMap['Nd'] -MultiCharEsc['D'] = MultiCharEsc['d'].negate() -MultiCharEsc['W'] = CodePointSet(PropertyMap['P']).extend(PropertyMap['Z']).extend(PropertyMap['C']) -MultiCharEsc['w'] = MultiCharEsc['W'].negate() +MultiCharEsc['D'] = MultiCharEsc['d'].negate().freeze() +MultiCharEsc['W'] = CodePointSet(PropertyMap['P']).extend(PropertyMap['Z']).extend(PropertyMap['C']).freeze() +MultiCharEsc['w'] = MultiCharEsc['W'].negate().freeze() diff --git a/pyxb/utils/utility.py b/pyxb/utils/utility.py index 549f8970..a84e3919 100644 --- a/pyxb/utils/utility.py +++ b/pyxb/utils/utility.py @@ -18,6 +18,8 @@ import re import os import errno +import uuid +import hashlib import pyxb from pyxb.utils.six.moves.urllib import parse as urlparse import time @@ -829,15 +831,6 @@ def OpenOrCreate (file_name, tag=None, preserve_contents=False): fp.seek(2) # os.SEEK_END return fp -# hashlib didn't show up until 2.5, and sha is deprecated in 2.6. -__Hasher = None -try: - import hashlib - __Hasher = hashlib.sha1 -except ImportError: - import sha - __Hasher = sha.new - def HashForText (text): """Calculate a cryptographic hash of the given string. @@ -850,26 +843,7 @@ def HashForText (text): """ if isinstance(text, six.text_type): text = text.encode('utf-8') - return __Hasher(text).hexdigest() - -# uuid didn't show up until 2.5 -__HaveUUID = False -try: - import uuid - __HaveUUID = True -except ImportError: - import random -def _NewUUIDString (): - """Obtain a UUID using the best available method. On a version of - python that does not incorporate the C{uuid} class, this creates a - string combining the current date and time (to the second) with a - random number. - - @rtype: C{str} - """ - if __HaveUUID: - return uuid.uuid1().urn - return '%s:%08.8x' % (time.strftime('%Y%m%d%H%M%S'), random.randint(0, 0xFFFFFFFF)) + return hashlib.sha1(text).hexdigest() class UniqueIdentifier (object): """Records a unique identifier, generally associated with a @@ -908,7 +882,7 @@ def __setstate__ (self, state): # Singleton-like def __new__ (cls, *args): if 0 == len(args): - uid = _NewUUIDString() + uid = uuid.uuid4().urn else: uid = args[0] if isinstance(uid, UniqueIdentifier): @@ -1300,6 +1274,27 @@ def __repr__ (self): ctor = '%s.%s' % (t.__module__, t.__name__) return '%s(%s, %r, %r)' % (ctor, repr2to3(self.__locationBase), self.__lineNumber, self.__columnNumber) + def withFilePathsRemoved(self): + '''Return a version of this Locator with any file paths removed. + Just the file name remains. + + If this Locator points to a HTTP or HTTPS URL, it is returned unchanged. + + This is used when generating bindings. There are two reasons for this: + It hides potentially-sensitive file paths (e.g. usernames if building + in My Documents on Windows or in a user's home directory on Unix + systems). It also helps ensure that builds are reproducible - if + different users build the same bindings from the same source files they + should end up with the same binding Python files, even if they build + in different directories or on different OSs. + ''' + proto = self.__locationBase.split(':', 1)[0] + if proto in ('http', 'https'): + return self + filename = self.__locationBase.rsplit('/', 1)[-1] + return type(self)(filename, self.__lineNumber, self.__columnNumber) + + class Locatable_mixin (pyxb.cscRoot): __location = None diff --git a/pyxb/utils/xmlre.py b/pyxb/utils/xmlre.py index 6e9e50e0..0c0d04b6 100644 --- a/pyxb/utils/xmlre.py +++ b/pyxb/utils/xmlre.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2009-2013, Peter A. Bigot -# Copyright 2012, Jon Foster +# Copyright 2012,2018 Jon Foster +# Copyright 2018 Eurofins Digital Product Testing UK Ltd - https://www.eurofins-digitaltesting.com/ # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain a @@ -41,6 +42,10 @@ _log = logging.getLogger(__name__) +# =========================================================================== +# XML Schema character class parsing/conversion functions +# =========================================================================== + # AllEsc maps all the possible escape codes and wildcards in an XML schema # regular expression into the corresponding CodePointSet. _AllEsc = { } @@ -49,8 +54,8 @@ def _InitializeAllEsc (): """Set the values in _AllEsc without introducing C{k} and C{v} into the module.""" - _AllEsc.update({ six.u('.'): pyxb.utils.unicode.WildcardEsc }) - bs = six.unichr(0x5c) + _AllEsc[six.u('.')] = pyxb.utils.unicode.WildcardEsc + bs = b'\\'.decode('ascii') for k, v in six.iteritems(pyxb.utils.unicode.SingleCharEsc): _AllEsc[bs + six.text_type(k)] = v for k, v in six.iteritems(pyxb.utils.unicode.MultiCharEsc): @@ -154,7 +159,7 @@ class DashClass: elif ch == six.u(']'): # End break - elif ch == six.unichr(0x5c): # backslash + elif ch == b'\\'.decode('ascii'): cps, position = _MatchCharClassEsc(text, position) single_char = cps.asSingleCharacter() if single_char is not None: @@ -163,7 +168,7 @@ class DashClass: tokens.append(cps) elif ch == six.u('-'): # We need to distinguish between "-" and "\-". So we use - # DASH for a plain "-", and u"-" for a "\-". + # DASH for a plain "-", and "-" for a "\-". tokens.append(DASH) position = position + 1 else: @@ -198,6 +203,7 @@ class DashClass: elif isinstance(start, six.text_type): result_cps.add(ord(start)) else: + assert isinstance(start, pyxb.utils.unicode.CodePointSet) result_cps.extend(start) cur_token = cur_token + 1 @@ -229,7 +235,7 @@ def _MatchCharClassExpr(text, position): position = position + 1 if position >= len(text): raise RegularExpressionError(position, 'Missing character class expression') - negated = (text[position] == '^') + negated = (text[position] == six.u('^')) if negated: position = position + 1 @@ -249,32 +255,218 @@ def _MatchCharClassExpr(text, position): raise RegularExpressionError(position, "Expected ']' to end character class") return result_cps, position + 1 -def MaybeMatchCharacterClass (text, position): - """Attempt to match a U{character class expression - }. - @param text: The complete text of the regular expression being - translated +# =========================================================================== +# Utilities for Python's RE module +# =========================================================================== + +_python_re_escape_char_dict = { + six.u('\000'): b'\\000'.decode('ascii'), + six.u('\r'): b'\\r'.decode('ascii'), + six.u('\n'): b'\\n'.decode('ascii'), + six.u('.'): b'\\.'.decode('ascii'), + six.u('^'): b'\\^'.decode('ascii'), + six.u('$'): b'\\$'.decode('ascii'), + six.u('*'): b'\\*'.decode('ascii'), + six.u('+'): b'\\+'.decode('ascii'), + six.u('?'): b'\\?'.decode('ascii'), + six.u('{'): b'\\{'.decode('ascii'), + six.u('}'): b'\\}'.decode('ascii'), + b'\\'.decode('ascii'): b'\\\\'.decode('ascii'), + six.u('['): b'\\['.decode('ascii'), + six.u(']'): b'\\]'.decode('ascii'), + six.u('|'): b'\\|'.decode('ascii'), + six.u('('): b'\\('.decode('ascii'), + six.u(')'): b'\\)'.decode('ascii'), + six.u('-'): b'\\-'.decode('ascii'), # Needed inside [] blocks + } + +def _python_re_escape_char(char): + '''Escape characters that need it. Pass a single character only. + + Note: Python's re.escape() function it a little overeager to + escape everything. This only escapes things that need it.''' + return _python_re_escape_char_dict.get(char, char) + +def _AddQualifier(pattern, min_occurs, max_occurs): + assert isinstance(pattern, six.text_type) + assert len(pattern) >= 1 + assert isinstance(min_occurs, int) + assert min_occurs >= 0 + assert max_occurs is None or isinstance(max_occurs, int) + assert max_occurs is None or max_occurs >= 0 - @param position: The offset of the start of the potential - expression. + if min_occurs == 1 and max_occurs == 1: + pass + elif max_occurs is None: + if min_occurs == 0: + pattern += six.u('*') + elif min_occurs == 1: + pattern += six.u('+') + else: + pattern = six.u('%s{%d,}') % (pattern, min_occurs) + elif max_occurs == 1 and min_occurs == 0: + pattern += six.u('?') + elif max_occurs == 0: + pattern = six.u('') + elif min_occurs == max_occurs: + pattern = six.u('%s{%d}') % (pattern, min_occurs) + else: + pattern = six.u('%s{%d,%d}') % (pattern, min_occurs, max_occurs) + return pattern + + +# =========================================================================== +# Functions to parse a XSD regexp +# =========================================================================== + +# Char ::= [^.\?*+()|#x5B#x5D] +# This appears to be a bug in the spec - the text says {} are invalid, +# but the grammar doesn't. Excluding them because otherwise "a{4}" is +# ambiguous - does it only match the literal "aaaa" or only match the +# literal "a{4}"? (We only actually need to exclude "{" to make the +# grammar unambiguous, but this is cleaner and matches the standard's +# text). +_invalid_literal_chars = frozenset(b'.\\?*+()|[]{}'.decode('ascii')) + +def _MatchAtom(text, position): + '''Parses an "atom". + + This is either: + - "Char", a plain single character + - A bracketed "regExp" + - "charClassEsc" an escape code for either a single character or a range + of characters + - "WildcardEsc", the "." wildcard + - "charClassExpr", a character class using the [] syntax + + Preconditions: Not at end of string. + If the parsing fails, throws a RegularExpressionError. + Returns a tuple with the Python regex that matches the atom, and the new + position. + Postconditions: None. + ''' + assert position < len(text) + start_position = position + ch = text[position] + if ch not in _invalid_literal_chars: + atom_pattern = _python_re_escape_char(ch) + position = position + 1 + elif ch == six.u('('): + atom_pattern, position = _MatchSubRegex(text, position + 1) + if position >= len(text) or text[position] != six.u(')'): + raise RegularExpressionError(start_position, "Unmatched bracket") + position = position + 1 + elif ch == b'\\'.decode('ascii'): + char_class, position = _MatchCharClassEsc(text, position) + single_char = char_class.asSingleCharacter() + if single_char is not None: + # E.g. '\\\\' isn't really a char range. + atom_pattern = _python_re_escape_char(single_char) + else: + atom_pattern = char_class.asPattern() + elif ch == six.u('.'): + atom_pattern = pyxb.utils.unicode.WildcardEsc.asPattern() + position = position + 1 + elif ch == six.u('['): + char_class, position = _MatchCharClassExpr(text, position) + atom_pattern = char_class.asPattern() + else: + raise RegularExpressionError(position, "Invalid character") + return atom_pattern, position + +_quantifier_curlybrace_re = re.compile( + b'\\{(?:' + b'(?P[0-9]+)' # {3} style + b'|' + b'(?:(?P[0-9]+),(?P[0-9]+)?)' # {3,4} or {3,} + b')\\}'.decode('ascii')) + +def _MatchQuantifier(text, position): + '''Tries to parse a "quantifier", if present. + If not, just returns the default quantifier of {1,1}. + + Preconditions: None. + If there's a "{" character indicating the start of a quantifier, but + parsing it fails, throws a RegularExpressionError. Will not throw if + there's no quantifier at all. + Returns a tuple with the min and max occurs, and the new position. Max + occurs can be None for unlimited. + Postconditions: None. + ''' - @return: C{None} if C{position} does not begin a character class - expression; otherwise a pair C{(cps, p)} where C{cps} is a - L{pyxb.utils.unicode.CodePointSet} containing the code points associated with - the property, and C{p} is the text offset immediately following - the closing brace.""" - if position >= len(text): - return None - c = text[position] - np = position + 1 - if '.' == c: - return (pyxb.utils.unicode.WildcardEsc, np) - if '[' == c: - return _MatchCharClassExpr(text, position) - if '\\' == c: - return _MatchCharClassEsc(text, position) - return None + min_occurs = 1 + max_occurs = 1 + if position < len(text): + ch = text[position] + if ch == six.u('?'): + min_occurs = 0 + position = position + 1 + elif ch == six.u('*'): + min_occurs = 0 + max_occurs = None + position = position + 1 + elif ch == six.u('+'): + max_occurs = None + position = position + 1 + elif ch == six.u('{'): + mo = _quantifier_curlybrace_re.match(text, position) + if not mo: + raise RegularExpressionError(position, "Cannot parse quantifier starting '{')") + exact_occurs = mo.group('exact_occurs') + if exact_occurs is not None: + min_occurs = max_occurs = int(exact_occurs, 10) + else: + min_occurs = int(mo.group('min_occurs'), 10) + max_occurs = mo.group('max_occurs') + if max_occurs is not None: + max_occurs = int(max_occurs, 10) + position = mo.end() + return min_occurs, max_occurs, position + +def _MatchBranch(text, position): + '''Parses a "branch". This is a series of "piece"s. It doesn't contain + the "|" character (unless it's bracketed or escaped). + + Each "piece" is an "atom" with an optional "qualifier". + + Preconditions: None + If the parsing fails, throws a RegularExpressionError. + Returns a tuple with the (possibly empty) Python regex that matches the + branch and the new position. + Postconditions: At end of string, or next character is '|' or ')'. + ''' + pieces = [] + while position < len(text) and text[position] != six.u('|') and text[position] != six.u(')'): + atom_pattern, position = _MatchAtom(text, position) + min_occurs, max_occurs, position = _MatchQuantifier(text, position) + pieces.append(_AddQualifier(atom_pattern, min_occurs, max_occurs)) + + pattern = six.u('').join(pieces) + return pattern, position + +def _MatchSubRegex(text, position): + '''Parses a "regExp". This is one or more "branch"es, separated by "|" + characters. + + Preconditions: None + If the parsing fails, throws a RegularExpressionError. + Returns a tuple with the Python regex that matches the XSD regex and the + new position. + Postconditions: At end of string, or next character is ')'. + ''' + branches = [] + new_branch, position = _MatchBranch(text, position) + branches.append(new_branch) + while position < len(text) and text[position] == six.u('|'): + new_branch, position = _MatchBranch(text, position + 1) + branches.append(new_branch) + pattern = six.u('(?:%s)') % (six.u('|').join(branches),) + return pattern, position + +# =========================================================================== +# Main XSD-to-Python regex conversion function +# =========================================================================== def XMLToPython (pattern): """Convert the given pattern to the format required for Python @@ -287,23 +479,7 @@ def XMLToPython (pattern): @return: A Unicode string specifying a Python regular expression that matches the same language as C{pattern}.""" assert isinstance(pattern, six.text_type) - new_pattern_elts = [] - new_pattern_elts.append('^(') - position = 0 - while position < len(pattern): - cg = MaybeMatchCharacterClass(pattern, position) - if cg is None: - ch = pattern[position] - if ch == six.u('^') or ch == six.u('$'): - # These characters have no special meaning in XSD. But they - # match start and end of string in Python, so they have to - # be escaped. - new_pattern_elts.append(six.unichr(0x5c) + ch) - else: - new_pattern_elts.append(ch) - position += 1 - else: - (cps, position) = cg - new_pattern_elts.append(cps.asPattern()) - new_pattern_elts.append(')$') - return ''.join(new_pattern_elts) + py_pattern, position = _MatchSubRegex(pattern, 0) + if position != len(pattern): + raise RegularExpressionError() + return six.u("^%s$") % (py_pattern,) diff --git a/setup.py b/setup.py index 80ab9322..e823bf2e 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Require Python 2.6 or higher or Python 3.1 or higher if (sys.version_info[:2] < (2, 6)) or ((sys.version_info[0] == 3) and sys.version_info[:2] < (3, 1)): - raise ValueError('''PyXB requires: + raise ValueError('''PyXB-X requires: Python2 version 2.6 or later; or Python3 version 3.1 or later (You have %s.)''' % (sys.version,)) @@ -19,7 +19,7 @@ import datetime import logging -from distutils.core import setup, Command +from setuptools import setup, Command # Stupid little command to automatically update the version number # where it needs to be updated. @@ -34,7 +34,7 @@ class update_version (Command): # Files in the distribution that need to be rewritten when the # version number changes - files = ( 'README.txt', 'pyxb/__init__.py', 'doc/conf.py' ) + files = ( 'pyxb/__init__.py', 'doc/conf.py' ) # The substitutions (key braced by @ signs) substitutions = { 'VERSION' : version, @@ -215,8 +215,8 @@ def run (self): packages.extend(b_packages) package_data.update(b_data) -setup(name='PyXB', - description = 'PyXB ("pixbee") is a pure Python package that generates Python source code for classes that correspond to data structures defined by XMLSchema.', +setup(name='PyXB-X', + description = 'PyXB-X ("pixbix") is a pure Python package that generates Python source code for classes that correspond to data structures defined by XMLSchema.', author='Peter A. Bigot', author_email='pabigot@users.sourceforge.net', url='http://pyxb.sourceforge.net', diff --git a/tests/complex/nsaugment/test.sh b/tests/complex/nsaugment/test.sh index 695159ca..d5f50721 100755 --- a/tests/complex/nsaugment/test.sh +++ b/tests/complex/nsaugment/test.sh @@ -1,3 +1,5 @@ +#! /bin/sh + PYXB_ARCHIVE_PATH=. export PYXB_ARCHIVE_PATH diff --git a/tests/complex/nsdep/test.sh b/tests/complex/nsdep/test.sh index 3fa80bed..6c6fa92b 100755 --- a/tests/complex/nsdep/test.sh +++ b/tests/complex/nsdep/test.sh @@ -1,3 +1,5 @@ +#! /bin/sh + PYXB_ARCHIVE_PATH=bindings rm -rf bindings mkdir -p bindings diff --git a/tests/complex/nsext/test.sh b/tests/complex/nsext/test.sh index 30508acf..21231b15 100755 --- a/tests/complex/nsext/test.sh +++ b/tests/complex/nsext/test.sh @@ -1,3 +1,5 @@ +#! /bin/sh + PYXB_ARCHIVE_PATH=. export PYXB_ARCHIVE_PATH diff --git a/tests/documents/union/test-union-001.xml b/tests/documents/union/test-union-001.xml old mode 100755 new mode 100644 diff --git a/tests/drivers/test-mg-sequence.py b/tests/drivers/test-mg-sequence.py index 418af6d6..11f4a9a2 100644 --- a/tests/drivers/test-mg-sequence.py +++ b/tests/drivers/test-mg-sequence.py @@ -22,7 +22,7 @@ def ToDOM (instance, tag=None): return instance.toDOM().documentElement import unittest -import collections +import collections.abc class TestMGSeq (unittest.TestCase): def setUp (self): @@ -50,7 +50,7 @@ def testBasics (self): self.assertTrue(isinstance(instance.first, sequence._ElementMap['first'].elementBinding().typeDefinition())) self.assertTrue(isinstance(instance.second_opt, sequence._ElementMap['second_opt'].elementBinding().typeDefinition())) self.assertTrue(isinstance(instance.third, sequence._ElementMap['third'].elementBinding().typeDefinition())) - self.assertTrue(isinstance(instance.fourth_0_2, collections.MutableSequence)) + self.assertTrue(isinstance(instance.fourth_0_2, collections.abc.MutableSequence)) self.assertEqual(1, len(instance.fourth_0_2)) self.assertTrue(isinstance(instance.fourth_0_2[0], sequence._ElementMap['fourth_0_2'].elementBinding().typeDefinition())) self.assertEqual(ToDOM(instance).toxml("utf-8"), xmld) @@ -63,7 +63,7 @@ def testMultiplesAtEnd (self): self.assertTrue(isinstance(instance.first, sequence._ElementMap['first'].elementBinding().typeDefinition())) self.assertTrue(instance.second_opt is None) self.assertTrue(isinstance(instance.third, sequence._ElementMap['third'].elementBinding().typeDefinition())) - self.assertTrue(isinstance(instance.fourth_0_2, collections.MutableSequence)) + self.assertTrue(isinstance(instance.fourth_0_2, collections.abc.MutableSequence)) self.assertEqual(2, len(instance.fourth_0_2)) self.assertTrue(isinstance(instance.fourth_0_2[0], sequence._ElementMap['fourth_0_2'].elementBinding().typeDefinition())) self.assertEqual(ToDOM(instance).toxml("utf-8"), xmld) @@ -73,7 +73,7 @@ def testMultiplesInMiddle (self): xmld = xmlt.encode('utf-8') dom = pyxb.utils.domutils.StringToDOM(xmlt) instance = altwrapper.createFromDOM(dom.documentElement) - self.assertTrue(isinstance(instance.first, collections.MutableSequence)) + self.assertTrue(isinstance(instance.first, collections.abc.MutableSequence)) self.assertEqual(1, len(instance.first)) self.assertEqual(2, len(instance.second_multi)) self.assertTrue(isinstance(instance.third, altsequence._ElementMap['third'].elementBinding().typeDefinition())) @@ -84,7 +84,7 @@ def testMultiplesAtStart (self): xmld = xmlt.encode('utf-8') dom = pyxb.utils.domutils.StringToDOM(xmlt) instance = altwrapper.createFromDOM(dom.documentElement) - self.assertTrue(isinstance(instance.first, collections.MutableSequence)) + self.assertTrue(isinstance(instance.first, collections.abc.MutableSequence)) self.assertEqual(2, len(instance.first)) self.assertEqual(0, len(instance.second_multi)) self.assertTrue(isinstance(instance.third, altsequence._ElementMap['third'].elementBinding().typeDefinition())) @@ -100,7 +100,7 @@ def testMissingInMiddle (self): self.assertTrue(isinstance(instance.first, sequence._ElementMap['first'].elementBinding().typeDefinition())) self.assertTrue(instance.second_opt is None) self.assertTrue(isinstance(instance.third, sequence._ElementMap['third'].elementBinding().typeDefinition())) - self.assertTrue(isinstance(instance.fourth_0_2, collections.MutableSequence)) + self.assertTrue(isinstance(instance.fourth_0_2, collections.abc.MutableSequence)) self.assertEqual(0, len(instance.fourth_0_2)) self.assertEqual(ToDOM(instance).toxml("utf-8"), xmld) diff --git a/tests/drivers/test-stored.sh b/tests/drivers/test-stored.sh index 88bbe6c2..c12ce05d 100755 --- a/tests/drivers/test-stored.sh +++ b/tests/drivers/test-stored.sh @@ -1,3 +1,5 @@ +#! /bin/sh + rm -rf bindings mkdir bindings diff --git a/tests/schemas/nested-groups.xsd b/tests/schemas/nested-groups.xsd old mode 100755 new mode 100644 diff --git a/tests/schemas/test-union.xsd b/tests/schemas/test-union.xsd old mode 100755 new mode 100644 diff --git a/tests/support.sh b/tests/support.sh index 34fc8868..f210f038 100644 --- a/tests/support.sh +++ b/tests/support.sh @@ -1,3 +1,5 @@ +#! /bin/sh + # POSIX shell infrastructure included by various test scripts to # remove redundancy and simplify the scripts. # diff --git a/tests/trac/issue-0048/test.sh b/tests/trac/issue-0048/test.sh index 3b00cb82..5c6e2d6a 100755 --- a/tests/trac/issue-0048/test.sh +++ b/tests/trac/issue-0048/test.sh @@ -1,3 +1,5 @@ +#! /bin/sh + pyxbgen \ --schema-location profile.xsd --module profile python check.py diff --git a/tests/trac/test-issue-0092.py b/tests/trac/test-issue-0092.py new file mode 100644 index 00000000..a8dac8b9 --- /dev/null +++ b/tests/trac/test-issue-0092.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import logging +import pyxb.binding.generate +import pyxb.utils.domutils +import xml.dom.minidom as dom + + +if __name__ == '__main__': + logging.basicConfig() +_log = logging.getLogger(__name__) + +xsd = ''' + + + + + + + + + + +''' + +code = pyxb.binding.generate.GeneratePython(schema_text=xsd) + +rv = compile(code, 'test', 'exec') +eval(rv) + +import unittest + +class TestIssue0092 (unittest.TestCase): + def testCreateEmptyTemplate (self): + xmlt = '1