Skip to content
This repository has been archived by the owner on Oct 4, 2022. It is now read-only.
/ pyxb Public archive
forked from pabigot/pyxb

Commit

Permalink
binding/datatypes: detect empty xml construction of scalar values
Browse files Browse the repository at this point in the history
The Python default value was inappropriately used for simple data types
when constructed from an empty XML element (i.e. an empty string value).
Detect this situation and reject unless the value is explicitly nil.

Closes pabigot#71
  • Loading branch information
pabigot committed May 13, 2017
1 parent 1d26a04 commit 0dfd102
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 71 deletions.
21 changes: 17 additions & 4 deletions pyxb/binding/basis.py
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,14 @@ class _RepresentAsXsdLiteral_mixin (pyxb.cscRoot):
e.g. duration, decimal, and any of the date/time types."""
pass

class _NoNullaryNonNillableNew_mixin (pyxb.cscRoot):
"""Marker class indicating that a simple data type cannot construct
a value from XML through an empty string.
This class should appear immediately L{simpleTypeDefinition} (or whatever
inherits from L{simpleTypeDefinition} in cases where it applies."""
pass

class simpleTypeDefinition (_TypeBinding_mixin, utility._DeconflictSymbols_mixin, _DynamicCreate_mixin):
"""L{simpleTypeDefinition} is a base class that is part of the
hierarchy of any class that represents the Python datatype for a
Expand Down Expand Up @@ -886,11 +894,16 @@ def __new__ (cls, *args, **kw):
kw.pop('_element', None)
kw.pop('_fallback_namespace', None)
kw.pop('_apply_attributes', None)
kw.pop('_nil', None)
# ConvertArguments will remove _element and _apply_whitespace_facet
dom_node = kw.get('_dom_node')
is_nil = kw.pop('_nil', None)
# ConvertArguments will remove _dom_node, _element, and
# _apply_whitespace_facet, and it will set _from_xml.
args = cls._ConvertArguments(args, kw)
kw.pop('_from_xml', dom_node is not None)
from_xml = kw.pop('_from_xml', False)
if ((0 == len(args))
and from_xml
and not is_nil
and issubclass(cls, _NoNullaryNonNillableNew_mixin)):
raise pyxb.SimpleTypeValueError(cls, args);
kw.pop('_location', None)
assert issubclass(cls, _TypeBinding_mixin)
try:
Expand Down
148 changes: 83 additions & 65 deletions pyxb/binding/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def XsdValueLength (cls, value):
# It is illegal to subclass the bool type in Python, so we subclass
# int instead.
@six.python_2_unicode_compatible
class boolean (basis.simpleTypeDefinition, six.int_type):
class boolean (basis.simpleTypeDefinition, six.int_type, basis._NoNullaryNonNillableNew_mixin):
"""XMLSchema datatype U{boolean<http://www.w3.org/TR/xmlschema-2/#boolean>}."""
_XsdBaseType = anySimpleType
_ExpandedName = pyxb.namespace.XMLSchema.createExpandedName('boolean')
Expand Down Expand Up @@ -126,7 +126,7 @@ def __new__ (cls, *args, **kw):

_PrimitiveDatatypes.append(boolean)

class decimal (basis.simpleTypeDefinition, python_decimal.Decimal, basis._RepresentAsXsdLiteral_mixin):
class decimal (basis.simpleTypeDefinition, python_decimal.Decimal, basis._RepresentAsXsdLiteral_mixin, basis._NoNullaryNonNillableNew_mixin):
"""XMLSchema datatype U{decimal<http://www.w3.org/TR/xmlschema-2/#decimal>}.
This class uses Python's L{decimal.Decimal} class to support (by
Expand Down Expand Up @@ -184,7 +184,7 @@ def XsdLiteral (cls, value):

_PrimitiveDatatypes.append(decimal)

class _fp (basis.simpleTypeDefinition, six.float_type):
class _fp (basis.simpleTypeDefinition, six.float_type, basis._NoNullaryNonNillableNew_mixin):
_XsdBaseType = anySimpleType

@classmethod
Expand Down Expand Up @@ -246,70 +246,79 @@ def durationData (self):
def __new__ (cls, *args, **kw):
args = cls._ConvertArguments(args, kw)
have_kw_update = False
if not kw.get('_nil'):
if 0 == len(args):
raise SimpleTypeValueError(cls, args)
text = args[0]
negative_duration = False
if kw.get('_nil'):
data = dict(zip(cls.__PythonFields, len(cls.__PythonFields) * [0,]))
negative_duration = False
elif isinstance(text, six.string_types):
match = cls.__Lexical_re.match(text)
if match is None:
raise SimpleTypeValueError(cls, text)
match_map = match.groupdict()
if 'T' == match_map.get('Time'):
# Can't have T without additional time information
raise SimpleTypeValueError(cls, text)

negative_duration = ('-' == match_map.get('neg'))

fractional_seconds = 0.0
if match_map.get('fracsec') is not None:
fractional_seconds = six.float_type('0%s' % (match_map['fracsec'],))
usec = six.int_type(1000000 * fractional_seconds)
if negative_duration:
kw['microseconds'] = - usec
else:
kw['microseconds'] = usec
else:
# Discard any bogosity passed in by the caller
kw.pop('microsecond', None)

data = { }
for fn in cls.__XSDFields:
v = match_map.get(fn, 0)
if v is None:
v = 0
data[fn] = six.int_type(v)
if fn in cls.__PythonFields:
if negative_duration:
kw[fn] = - data[fn]
else:
kw[fn] = data[fn]
data['seconds'] += fractional_seconds
have_kw_update = True
elif isinstance(text, cls):
data = text.durationData().copy()
negative_duration = text.negativeDuration()
elif isinstance(text, datetime.timedelta):
data = { 'days' : text.days,
'seconds' : text.seconds + (text.microseconds / 1000000.0) }
negative_duration = (0 > data['days'])
if negative_duration:
if 0.0 == data['seconds']:
data['days'] = - data['days']
else:
data['days'] = 1 - data['days']
data['seconds'] = 24 * 60 * 60.0 - data['seconds']
data['minutes'] = 0
data['hours'] = 0
elif isinstance(text, six.integer_types) and (1 < len(args)):
elif 0 == len(args):
if kw.get('_from_xml'):
raise SimpleTypeValueError(cls, args)
data = dict(zip(cls.__PythonFields, len(cls.__PythonFields) * [0,]))
elif 1 < len(args):
if kw.get('_from_xml'):
raise SimpleTypeValueError(cls, args)
# Apply the arguments as in the underlying Python constructor
data = dict(zip(cls.__PythonFields[:len(args)], args))
negative_duration = False
else:
raise SimpleTypeValueError(cls, text)
text = args[0];
if isinstance(text, six.string_types):
match = cls.__Lexical_re.match(text)
if match is None:
raise SimpleTypeValueError(cls, text)
match_map = match.groupdict()
if 'T' == match_map.get('Time'):
# Can't have T without additional time information
raise SimpleTypeValueError(cls, text)

negative_duration = ('-' == match_map.get('neg'))

fractional_seconds = 0.0
if match_map.get('fracsec') is not None:
fractional_seconds = six.float_type('0%s' % (match_map['fracsec'],))
usec = six.int_type(1000000 * fractional_seconds)
if negative_duration:
kw['microseconds'] = - usec
else:
kw['microseconds'] = usec
else:
# Discard any bogosity passed in by the caller
kw.pop('microsecond', None)

data = { }
for fn in cls.__XSDFields:
v = match_map.get(fn, 0)
if v is None:
v = 0
data[fn] = six.int_type(v)
if fn in cls.__PythonFields:
if negative_duration:
kw[fn] = - data[fn]
else:
kw[fn] = data[fn]
data['seconds'] += fractional_seconds
have_kw_update = True
elif kw.get('_from_xml'):
raise SimpleTypeValueError(cls, args)
elif isinstance(text, cls):
data = text.durationData().copy()
negative_duration = text.negativeDuration()
elif isinstance(text, datetime.timedelta):
data = { 'days' : text.days,
'seconds' : text.seconds + (text.microseconds / 1000000.0) }
negative_duration = (0 > data['days'])
if negative_duration:
if 0.0 == data['seconds']:
data['days'] = - data['days']
else:
data['days'] = 1 - data['days']
data['seconds'] = 24 * 60 * 60.0 - data['seconds']
data['minutes'] = 0
data['hours'] = 0
elif isinstance(text, six.integer_types):
# Apply the arguments as in the underlying Python constructor
data = dict(zip(cls.__PythonFields[:len(args)], args))
negative_duration = False
else:
raise SimpleTypeValueError(cls, text)
if not have_kw_update:
rem_time = data.pop('seconds', 0)
if (0 != (rem_time % 1)):
Expand Down Expand Up @@ -516,6 +525,10 @@ def __new__ (cls, *args, **kw):
ctor_kw = { }
if kw.get('_nil'):
ctor_kw = { 'year': 1900, 'month': 1, 'day': 1 }
elif 0 == len(args):
if kw.get('_from_xml'):
raise SimpleTypeValueError(cls, args)
ctor_kw = { 'year': 1900, 'month': 1, 'day': 1 }
elif 1 == len(args):
value = args[0]
if isinstance(value, six.string_types):
Expand Down Expand Up @@ -590,7 +603,12 @@ class time (_PyXBDateTime_base, datetime.time):
def __new__ (cls, *args, **kw):
args = cls._ConvertArguments(args, kw)
ctor_kw = { }
if 1 <= len(args):
if kw.get('_nil'):
pass
elif 0 == len(args):
if kw.get('_from_xml'):
raise SimpleTypeValueError(cls, args)
else:
value = args[0]
if isinstance(value, six.string_types):
ctor_kw.update(cls._LexicalToKeywords(value))
Expand Down Expand Up @@ -1162,7 +1180,7 @@ class ENTITIES (basis.STD_list):
_ItemType = ENTITY
_ListDatatypes.append(ENTITIES)

class integer (basis.simpleTypeDefinition, six.long_type):
class integer (basis.simpleTypeDefinition, six.long_type, basis._NoNullaryNonNillableNew_mixin):
"""XMLSchema datatype U{integer<http://www.w3.org/TR/xmlschema-2/#integer>}."""
_XsdBaseType = decimal
_ExpandedName = pyxb.namespace.XMLSchema.createExpandedName('integer')
Expand All @@ -1188,7 +1206,7 @@ class long (integer):
_ExpandedName = pyxb.namespace.XMLSchema.createExpandedName('long')
_DerivedDatatypes.append(long)

class int (basis.simpleTypeDefinition, six.int_type):
class int (basis.simpleTypeDefinition, six.int_type, basis._NoNullaryNonNillableNew_mixin):
"""XMLSchema datatype U{int<http://www.w3.org/TR/xmlschema-2/#int>}."""
_XsdBaseType = long
_ExpandedName = pyxb.namespace.XMLSchema.createExpandedName('int')
Expand Down
6 changes: 4 additions & 2 deletions tests/datatypes/test-duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@ def testCreation (self):
self.assertEqual(3, v.days)
self.assertEqual(14842, v.seconds)
self.assertEqual('P3DT4H7M22.5S', v.xsdLiteral())
self.assertRaises(pyxb.SimpleTypeValueError, xsd.duration)
self.assertRaises(pyxb.SimpleTypeValueError, xsd.duration, 4)
self.assertEqual(datetime.timedelta(), xsd.duration())
self.assertRaises(pyxb.SimpleTypeValueError, xsd.duration, _from_xml=True)
self.assertEqual(datetime.timedelta(4), xsd.duration(4))
self.assertRaises(pyxb.SimpleTypeValueError, xsd.duration, 4, _from_xml=True)

if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions tests/datatypes/test-time.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def testBad (self):
self.assertRaises(pyxb.SimpleTypeValueError, xsd.time, '12:14:32.Z')
self.assertRaises(pyxb.SimpleTypeValueError, xsd.time, '12:14:32.123405:00')
self.assertRaises(pyxb.SimpleTypeValueError, xsd.time, '12:14:32.1234+05')
self.assertRaises(pyxb.SimpleTypeValueError, xsd.time, _from_xml=True)

def testFromText (self):
self.verifyTime(xsd.time('12:14:32'), with_usec=False, with_tzinfo=False)
Expand Down
5 changes: 5 additions & 0 deletions tests/drivers/test-facets.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,10 @@ def testQuantity (self):
self.assertRaises(Exception, quantity, -52)
self.assertRaises(Exception, quantity, 100)

def testEmptyQuantity(self):
xml = '<quantity xmlns="URN:test-facets"/>'
dom = pyxb.utils.domutils.StringToDOM(xml).documentElement;
self.assertRaises(SimpleTypeValueError, CreateFromDOM, dom)

if __name__ == '__main__':
unittest.main()
61 changes: 61 additions & 0 deletions tests/trac/test-issue-0071.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
import logging
if __name__ == '__main__':
logging.basicConfig()
_log = logging.getLogger(__name__)
import pyxb.binding.generate
import pyxb.utils.domutils
import pyxb.binding.datatypes as xs
import datetime;
from xml.dom import Node

import os.path
xsd='''<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="b" type="xs:boolean"/>
<xs:element name="nNI" type="xs:nonNegativeInteger"/>
<xs:element name="int" type="xs:int"/>
<xs:element name="time" type="xs:time"/>
<xs:element name="duration" type="xs:duration"/>
</xs:schema>
'''

code = pyxb.binding.generate.GeneratePython(schema_text=xsd)
#open('code.py', 'w').write(code)
#print code

rv = compile(code, 'test', 'exec')
eval(rv)

from pyxb.exceptions_ import *

import unittest

class TestIssue0071 (unittest.TestCase):
def testNonNegativeInteger (self):
self.assertEqual(0, xs.nonNegativeInteger());
self.assertEqual(0, CreateFromDocument(six.u('<nNI>0</nNI>')));
self.assertRaises(SimpleTypeValueError, CreateFromDocument, six.u('<nNI/>'));

def testBoolean (self):
self.assertEqual(0, xs.boolean());
self.assertEqual(0, CreateFromDocument(six.u('<b>0</b>')));
self.assertRaises(SimpleTypeValueError, CreateFromDocument, six.u('<b/>'));

def testInt (self):
self.assertEqual(0, xs.int());
self.assertEqual(0, CreateFromDocument(six.u('<int>0</int>')));
self.assertRaises(SimpleTypeValueError, CreateFromDocument, six.u('<int/>'));

def testTime (self):
self.assertEqual(datetime.time(), xs.time());
self.assertEqual(datetime.time(), CreateFromDocument(six.u('<time>00:00:00</time>')));
self.assertRaises(SimpleTypeValueError, CreateFromDocument, six.u('<time/>'));

def testDuration (self):
self.assertEqual(datetime.timedelta(), xs.duration());
self.assertEqual(datetime.timedelta(), CreateFromDocument(six.u('<duration>P0D</duration>')));
self.assertRaises(SimpleTypeValueError, CreateFromDocument, six.u('<duration/>'));

if __name__ == '__main__':
unittest.main()

0 comments on commit 0dfd102

Please sign in to comment.