Skip to content

Commit

Permalink
Merge branch 'feature/nosetests' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
rlskoeser committed Jan 28, 2013
2 parents a1b7147 + c02ed76 commit 941fb28
Show file tree
Hide file tree
Showing 19 changed files with 235 additions and 254 deletions.
9 changes: 6 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ python:
- "2.7"
- "2.6"
# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
# install a local squid proxy to allow testing schemas
install:
- "pip install . --use-mirrors"
- "pip install -r pip-opt-req.txt --use-mirrors"
- "pip install -e . --use-mirrors"
- "pip install 'eulxml[dev]' --use-mirrors"
- "sudo apt-get install squid"
- "sudo squid3"
# command to run tests, e.g. python setup.py test
script: python ./test/test_all.py
script: env HTTP_PROXY=localhost:3128 nosetests
9 changes: 8 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The following is a summary of changes and improvements to
any necessary information about installation or upgrade notes.


0.19.0
-------

* Corrected a minor bug where schema validation errors were not cleared between
multiple validations.


0.18.0 - Formset Ordering and DateTime
--------------------------------------

Expand All @@ -31,7 +38,7 @@ any necessary information about installation or upgrade notes.
* :class:`eulxml.xmlmap.XmlObject` now supports lazy-loading for XSD
Schemas. To take advantage of this feature,
:class:`~eulxml.xmlmap.XmlObject` subclasses should define an
``XSD_SCHEMA`` location but should not set an ``xmlschema``.
``XSD_SCHEMA`` location but should not set an ``xmlschema``.
* When :ref:`field <xmlmap-field>` mapped on a
:class:`eulxml.xmlmap.XmlObject` is deleted, any XPath predicates
that could have been automatically constructed when setting the
Expand Down
40 changes: 38 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
EULxml
======

.. image:: https://api.travis-ci.org/emory-libraries/eulxml.png
:alt: current build status for namedropper-py
:target: https://travis-ci.org/emory-libraries/eulxml


EULxml is a `Python <http://www.python.org/>`_ module that provides
utilities and classes for interacting with XML that allow the
definition of re-usable XML objects that can be accessed, updated and
Expand All @@ -27,10 +32,10 @@ Dependencies
**eulxml** depends on `PLY <http://www.dabeaz.com/ply/>`_ and `lxml
<http://lxml.de/>`_.

**eulxml.forms** requires and was designed to be used with
**eulxml.forms** requires and was designed to be used with
`Django <https://www.djangoproject.com/>`_, although Django is not
required for installation and use of the non-form components of
**eulxml**.
**eulxml**.


Contact Information
Expand All @@ -53,3 +58,34 @@ Development History
For instructions on how to see and interact with the full development
history of **eulxml**, see
`eulcore-history <https://github.com/emory-libraries/eulcore-history>`_.

Developer notes
---------------

To install dependencies for your local check out of the code, run ``pip install``
in the ``eulxml`` directory (the use of `virtualenv`_ is recommended)::

pip install -e .

.. _virtualenv: http://www.virtualenv.org/en/latest/

If you want to run unit tests or build sphinx documentation, you will also
need to install development dependencies::

pip install -e . "eulxml[dev]"

To run all unit tests::

nosetests # for normal development
nosetests --with-coverage --cover-package=eulxml --cover-xml --with-xunit # for continuous integration

To run unit tests for a specific module, use syntax like this::

nosetests test/test_xpath.py


To generate sphinx documentation::

cd doc
make html

67 changes: 41 additions & 26 deletions eulxml/xmlmap/core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# file eulxml/xmlmap/core.py
#
#
# Copyright 2010,2011 Emory University Libraries
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -45,10 +45,13 @@
# This lxml behavior has been logged as a bug:
# https://bugs.launchpad.net/lxml/+bug/673205


def parseUri(stream, uri=None):
"""Read an XML document from a URI, and return a :mod:`lxml.etree`
document."""
return etree.parse(stream, parser=_get_xmlparser(), base_url=uri)


def parseString(string, uri=None):
"""Read an XML document provided as a byte string, and return a
:mod:`lxml.etree` document. String cannot be a Unicode string.
Expand All @@ -57,10 +60,12 @@ def parseString(string, uri=None):

# internal cache for loaded schemas, so we only load each schema once
_loaded_schemas = {}


def loadSchema(uri, base_uri=None, override_proxy_requirement=False):
"""Load an XSD XML document (specified by filename or URL), and return a
:class:`lxml.etree.XMLSchema`.
Note that frequently loading a schema without using a web proxy may
introduce significant network resource usage as well as instability if
the schema becomes unavailable. Thus this function will fail if the
Expand All @@ -70,7 +75,7 @@ def loadSchema(uri, base_uri=None, override_proxy_requirement=False):
# uri to use for reporting errors - include base uri if any
if uri in _loaded_schemas:
return _loaded_schemas[uri]

error_uri = uri
if base_uri is not None:
error_uri += ' (base URI %s)' % base_uri
Expand All @@ -80,7 +85,7 @@ def loadSchema(uri, base_uri=None, override_proxy_requirement=False):
if 'HTTP_PROXY' not in os.environ and _http_uri(uri):
message = ('Loading schema %s without a web proxy may introduce ' +
'significant network resource usage as well as ' +
'instability if that server becomes inaccessible. ' +
'instability if that server becomes inaccessible. ' +
'The HTTP_PROXY environment variable is required ' +
'for loading schemas. Schema validation will be disabled.') \
% (error_uri,)
Expand All @@ -107,9 +112,12 @@ def loadSchema(uri, base_uri=None, override_proxy_requirement=False):
except etree.XMLSchemaParseError as parse_err:
# re-raise as a schema parse error, but ensure includes details about schema being loaded
raise etree.XMLSchemaParseError('Failed to parse schema %s -- %s' % (error_uri, parse_err))


def _http_uri(uri):
return uri.startswith('http:') or uri.startswith('https:')


class _FieldDescriptor(object):
def __init__(self, field):
self.field = field
Expand All @@ -119,7 +127,7 @@ def __get__(self, obj, objtype):
return self
return self.field.get_for_node(obj.node, obj.context)

def __set__(self, obj, value):
def __set__(self, obj, value):
return self.field.set_for_node(obj.node, obj.context, value)

def __delete__(self, obj):
Expand Down Expand Up @@ -179,7 +187,7 @@ def __new__(cls, name, bases, defined_attrs):
# otherwise, use nearest parent xsd
else:
schema_obj = load_xmlobject_from_file(base_xsd, XsdSchema)

attr_val = attr_val.get_field(schema_obj)
field = attr_val
fields[attr_name] = field
Expand Down Expand Up @@ -229,6 +237,7 @@ def create_field(xmlobject):
create_field.__name__ = field_name
return create_field


class XmlObject(object):

"""
Expand Down Expand Up @@ -283,7 +292,7 @@ class XmlObject(object):
'''Override for schema validation; if a schema must be defined for
the use of :class:`xmlmap.fields.SchemaField` for a sub-xmlobject
that should not be validated, set to False.'''

@property
def xmlschema(self):
"""A parsed XSD schema instance of
Expand All @@ -293,14 +302,14 @@ def xmlschema(self):
schema at class definition time, instead of at class instance
initialization time, you may want to define your schema in
your subclass like this::
XSD_SCHEMA = "http://www.openarchives.org/OAI/2.0/oai_dc.xsd"
xmlschema = xmlmap.loadSchema(XSD_SCHEMA)
"""
if self.XSD_SCHEMA:
return loadSchema(self.XSD_SCHEMA)

# NOTE: DTD and RNG validation could be handled similarly to XSD validation logic

def __init__(self, node=None, context=None, **kwargs):
Expand All @@ -318,7 +327,8 @@ def __init__(self, node=None, context=None, **kwargs):
nsmap = {}

# xpath has no notion of a default namespace - omit any namespace with no prefix
self.context = {'namespaces': dict([(prefix, ns) for prefix, ns in nsmap.iteritems() if prefix ]) }
self.context = {'namespaces': dict([(prefix, ns) for prefix, ns
in nsmap.iteritems() if prefix])}

if context is not None:
self.context.update(context)
Expand All @@ -340,7 +350,6 @@ def _build_root_element(self):
E = ElementMaker(**opts)
root = E(self.ROOT_NAME)
return root


def xsl_transform(self, filename=None, xsl=None, return_type=None, **params):
"""Run an xslt transform on the contents of the XmlObject.
Expand Down Expand Up @@ -448,7 +457,7 @@ def validation_errors(self):
if the xml is schema valid or no schema is defined. If a
schema is defined but :attr:`schema_validate` is False, schema
validation will be skipped.
Currently only supports schema validation.
:rtype: list
Expand All @@ -466,6 +475,11 @@ def schema_valid(self):
:raises: Exception if no XSD schema is defined for this XmlObject instance
"""
if self.xmlschema is not None:
# clear out errors so they are not duplicated by repeated
# validations on the same schema object
self.xmlschema._clear_error_log()
# NOTE: _clear_error_log is technically private, but I can't find
# any public method to clear the validation log.
return self.xmlschema.validate(self.node)
else:
raise Exception('No XSD schema is defined, cannot validate document')
Expand All @@ -474,7 +488,7 @@ def schema_validation_errors(self):
"""
Retrieve any validation errors that occured during schema validation
done via :meth:`is_valid`.
:returns: a list of :class:`lxml.etree._LogEntry` instances
:raises: Exception if no XSD schema is defined for this XmlObject instance
"""
Expand All @@ -489,7 +503,8 @@ def is_empty(self):
attributes, and no text. Returns False if any are present.
"""
return len(self.node) == 0 and len(self.node.attrib) == 0 \
and not self.node.text and not self.node.tail # regular text or text after a node
and not self.node.text and not self.node.tail # regular text or text after a node


class Urllib2Resolver(etree.Resolver):
def resolve(self, url, public_id, context):
Expand All @@ -502,6 +517,7 @@ def resolve(self, url, public_id, context):
return self.resolve_file(f, context, base_url=url)
_defaultResolver = Urllib2Resolver()


def _get_xmlparser(xmlclass=XmlObject, validate=False, resolver=_defaultResolver):
"""Initialize an instance of :class:`lxml.etree.XMLParser` with appropriate
settings for validation. If validation is requested and the specified
Expand All @@ -520,18 +536,19 @@ def _get_xmlparser(xmlclass=XmlObject, validate=False, resolver=_defaultResolver
opts = {'schema': xmlschema}
else:
# if configured XmlObject does not have a schema defined, assume DTD validation
opts = {'dtd_validation': True}
opts = {'dtd_validation': True}
else:
# if validation is not requested, no parser options are needed
opts = {}

parser = etree.XMLParser(**opts)

if resolver is not None:
parser.resolvers.add(resolver)

return parser


def load_xmlobject_from_string(string, xmlclass=XmlObject, validate=False,
resolver=None):
"""Initialize an XmlObject from a string.
Expand All @@ -550,7 +567,7 @@ def load_xmlobject_from_string(string, xmlclass=XmlObject, validate=False,
:param validate: boolean, enable validation; defaults to false
:rtype: instance of :class:`~eulxml.xmlmap.XmlObject` requested
"""
parser = _get_xmlparser(xmlclass=xmlclass, validate=validate, resolver=resolver)
parser = _get_xmlparser(xmlclass=xmlclass, validate=validate, resolver=resolver)
element = etree.fromstring(string, parser)
return xmlclass(element)

Expand All @@ -566,7 +583,7 @@ def load_xmlobject_from_file(filename, xmlclass=XmlObject, validate=False,
:param filename: name of the file that should be loaded as an xmlobject.
:meth:`etree.lxml.parse` will accept a file name/path, a file object, a
file-like object, or an HTTP or FTP url, however file path and URL are
recommended, as they are generally faster for lxml to handle.
recommended, as they are generally faster for lxml to handle.
"""
parser = _get_xmlparser(xmlclass=xmlclass, validate=validate, resolver=resolver)

Expand All @@ -580,6 +597,7 @@ def load_xmlobject_from_file(filename, xmlclass=XmlObject, validate=False,
# XSD schema xmlobjects - used in XmlObjectType to process SchemaFields
# FIXME: where should these actually go? depends on both XmlObject and fields


class XsdType(XmlObject):
ROOT_NAME = 'simpleType'
name = StringField('@name')
Expand All @@ -594,12 +612,12 @@ def base_type(self):
else:
basetype = self.base
return basetype


class XsdSchema(XmlObject):
ROOT_NAME = 'schema'
ROOT_NS = 'http://www.w3.org/2001/XMLSchema'
ROOT_NAMESPACES = {'xs': ROOT_NS }
ROOT_NAMESPACES = {'xs': ROOT_NS}

def get_type(self, name=None, xpath=None):
if xpath is None:
Expand All @@ -613,7 +631,4 @@ def get_type(self, name=None, xpath=None):
elif len(result) > 1:
raise Exception("Too many schema type definitions found for xpath '%s' (found %d)" \
% (xpath, len(result)))
return XsdType(result[0], context=self.context) # pass in namespaces



return XsdType(result[0], context=self.context) # pass in namespaces
4 changes: 0 additions & 4 deletions pip-dev-req.txt

This file was deleted.

2 changes: 0 additions & 2 deletions pip-opt-req.txt

This file was deleted.

9 changes: 9 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ def run(self, *args, **kwargs):
extras_require={
'django': ['Django'],
'rdf': ['rdflib>=3.0'],
'dev': [
'sphinx',
'coverage',
'Django',
'rdflib>=3.0',
'mock',
'nose',
'unittest2', # for python 2.6
]
},

description='XPath-based XML data binding, with Django form support',
Expand Down
Loading

0 comments on commit 941fb28

Please sign in to comment.