Skip to content

Implement correct xsd:whitespace behaviour #1125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/zeep/xsd/types/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ def _wrapper(self, value):
return _wrapper


def treat_whitespace(behaviour):
def _treat_whitespace(func):
def _wrapper(self, value):
assert behaviour in ["replace", "collapse", "preserve"]
if behaviour == "replace":
return func(self, re.sub(r'[\n\r\t]', ' ', value))
elif behaviour == "collapse":
return func(self, re.sub(r'[\n\r\t ]', ' ', value).strip())
return func(self, value)
return _wrapper
return _treat_whitespace


##
# Primitive types
class String(BuiltinType):
Expand All @@ -58,6 +71,7 @@ class Boolean(BuiltinType):
def xmlvalue(self, value):
return "true" if value and value not in ("false", "0") else "false"

@treat_whitespace('collapse')
def pythonvalue(self, value):
"""Return True if the 'true' or '1'. 'false' and '0' are legal false
values, but we consider everything not true as false.
Expand All @@ -74,6 +88,7 @@ class Decimal(BuiltinType):
def xmlvalue(self, value):
return str(value)

@treat_whitespace('collapse')
def pythonvalue(self, value):
return _Decimal(value)

Expand All @@ -85,6 +100,7 @@ class Float(BuiltinType):
def xmlvalue(self, value):
return str(value).upper()

@treat_whitespace('collapse')
def pythonvalue(self, value):
return float(value)

Expand All @@ -97,6 +113,7 @@ class Double(BuiltinType):
def xmlvalue(self, value):
return str(value)

@treat_whitespace('collapse')
def pythonvalue(self, value):
return float(value)

Expand All @@ -109,6 +126,7 @@ class Duration(BuiltinType):
def xmlvalue(self, value):
return isodate.duration_isoformat(value)

@treat_whitespace('collapse')
def pythonvalue(self, value):
if value.startswith("PT-"):
value = value.replace("PT-", "PT")
Expand Down Expand Up @@ -144,6 +162,7 @@ def xmlvalue(self, value):
return isodate.isostrf.strftime(value, "%Y-%m-%dT%H:%M:%S.%f%Z")
return isodate.isostrf.strftime(value, "%Y-%m-%dT%H:%M:%S%Z")

@treat_whitespace('collapse')
def pythonvalue(self, value):

# Determine based on the length of the value if it only contains a date
Expand All @@ -166,6 +185,7 @@ def xmlvalue(self, value):
return isodate.isostrf.strftime(value, "%H:%M:%S.%f%Z")
return isodate.isostrf.strftime(value, "%H:%M:%S%Z")

@treat_whitespace('collapse')
def pythonvalue(self, value):
return isodate.parse_time(value)

Expand All @@ -180,6 +200,7 @@ def xmlvalue(self, value):
return value
return isodate.isostrf.strftime(value, "%Y-%m-%d")

@treat_whitespace('collapse')
def pythonvalue(self, value):
return isodate.parse_date(value)

Expand All @@ -203,6 +224,7 @@ def xmlvalue(self, value):
year, month, tzinfo = value
return "%04d-%02d%s" % (year, month, _unparse_timezone(tzinfo))

@treat_whitespace('collapse')
def pythonvalue(self, value):
match = self._pattern.match(value)
if not match:
Expand Down Expand Up @@ -231,6 +253,7 @@ def xmlvalue(self, value):
year, tzinfo = value
return "%04d%s" % (year, _unparse_timezone(tzinfo))

@treat_whitespace('collapse')
def pythonvalue(self, value):
match = self._pattern.match(value)
if not match:
Expand Down Expand Up @@ -258,6 +281,7 @@ def xmlvalue(self, value):
month, day, tzinfo = value
return "--%02d-%02d%s" % (month, day, _unparse_timezone(tzinfo))

@treat_whitespace('collapse')
def pythonvalue(self, value):
match = self._pattern.match(value)
if not match:
Expand Down Expand Up @@ -288,6 +312,7 @@ def xmlvalue(self, value):
day, tzinfo = value
return "---%02d%s" % (day, _unparse_timezone(tzinfo))

@treat_whitespace('collapse')
def pythonvalue(self, value):
match = self._pattern.match(value)
if not match:
Expand All @@ -312,6 +337,7 @@ def xmlvalue(self, value):
month, tzinfo = value
return "--%d%s" % (month, _unparse_timezone(tzinfo))

@treat_whitespace('collapse')
def pythonvalue(self, value):
match = self._pattern.match(value)
if not match:
Expand Down Expand Up @@ -352,6 +378,7 @@ class AnyURI(BuiltinType):
def xmlvalue(self, value):
return value

@treat_whitespace('collapse')
def pythonvalue(self, value):
return value

Expand All @@ -364,6 +391,7 @@ class QName(BuiltinType):
def xmlvalue(self, value):
return value

@treat_whitespace('collapse')
def pythonvalue(self, value):
return value

Expand All @@ -380,10 +408,18 @@ class Notation(BuiltinType):
class NormalizedString(String):
_default_qname = xsd_ns("normalizedString")

@treat_whitespace('replace')
def pythonvalue(self, value):
return value


class Token(NormalizedString):
_default_qname = xsd_ns("token")

@treat_whitespace('collapse')
def pythonvalue(self, value):
return value


class Language(Token):
_default_qname = xsd_ns("language")
Expand Down
59 changes: 57 additions & 2 deletions tests/test_xsd_builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,42 @@ def test_pythonvalue(self):
result = instance.pythonvalue("foobar")
assert result == "foobar"

result = instance.pythonvalue(" foo\tbar\r\n ")
assert result == " foo\tbar\r\n "


class TestNormalizedString:
def test_xmlvalue(self):
instance = builtins.NormalizedString()
result = instance.xmlvalue("foobar")
assert result == "foobar"

def test_pythonvalue(self):
instance = builtins.NormalizedString()
result = instance.pythonvalue("foobar")
assert result == "foobar"

result = instance.pythonvalue("fo\tob\rar\n")
assert result == "fo ob ar "


class TestToken:
def test_xmlvalue(self):
instance = builtins.Token()
result = instance.xmlvalue("foobar")
assert result == "foobar"

def test_pythonvalue(self):
instance = builtins.Token()
result = instance.pythonvalue("foobar")
assert result == "foobar"

result = instance.pythonvalue("fo\tob\rar")
assert result == "fo ob ar"

result = instance.pythonvalue(" foobar ")
assert result == "foobar"


class TestBoolean:
def test_xmlvalue(self):
Expand All @@ -36,6 +72,7 @@ def test_pythonvalue(self):
assert instance.pythonvalue("true") is True
assert instance.pythonvalue("0") is False
assert instance.pythonvalue("false") is False
assert instance.pythonvalue("\t \r\nfalse ") is False


class TestDecimal:
Expand All @@ -53,6 +90,7 @@ def test_pythonvalue(self):
assert instance.pythonvalue("10.001") == D("10.001")
assert instance.pythonvalue("+10.001") == D("10.001")
assert instance.pythonvalue("-10.001") == D("-10.001")
assert instance.pythonvalue(" \r\n 10 \t") == D("10")


class TestFloat:
Expand All @@ -74,6 +112,7 @@ def test_pythonvalue(self):
assert instance.pythonvalue("-0") == float(0)
assert instance.pythonvalue("0") == float(0)
assert instance.pythonvalue("INF") == float("inf")
assert instance.pythonvalue("\t \r12.78e-2\n ") == float("0.1278")


class TestDouble:
Expand All @@ -89,6 +128,7 @@ def test_pythonvalue(self):
assert instance.pythonvalue("12") == float(12)
assert instance.pythonvalue("-0") == float(0)
assert instance.pythonvalue("0") == float(0)
assert instance.pythonvalue(" \r\n0 \t") == float(0)


class TestDuration:
Expand All @@ -103,6 +143,10 @@ def test_pythonvalue(self):
value = "P0Y1347M0D"
assert instance.pythonvalue(value) == expected

expected = isodate.parse_duration("P0Y1347M0D")
value = "\r \nP0Y1347M0D\t "
assert instance.pythonvalue(value) == expected


class TestDateTime:
def test_xmlvalue(self):
Expand Down Expand Up @@ -137,6 +181,9 @@ def test_pythonvalue(self):
value = datetime.datetime(2016, 3, 4, 0, 0, 0)
assert instance.pythonvalue("2016-03-04") == value

value = datetime.datetime(2016, 3, 4, 0, 0, 0)
assert instance.pythonvalue(" \r\n\t2016-03-04 ") == value

def test_pythonvalue_invalid(self):
instance = builtins.DateTime()
with pytest.raises(ValueError):
Expand All @@ -161,6 +208,9 @@ def test_pythonvalue(self):
value = isodate.parse_time("21:14:42.120+0200")
assert instance.pythonvalue("21:14:42.120+0200") == value

value = datetime.time(21, 14, 42)
assert instance.pythonvalue("\t\r\n 21:14:42 ") == value

def test_pythonvalue_invalid(self):
instance = builtins.Time()
with pytest.raises(ValueError):
Expand All @@ -181,6 +231,7 @@ def test_pythonvalue(self):
assert instance.pythonvalue("2001-10-26+02:00") == datetime.date(2001, 10, 26)
assert instance.pythonvalue("2001-10-26Z") == datetime.date(2001, 10, 26)
assert instance.pythonvalue("2001-10-26+00:00") == datetime.date(2001, 10, 26)
assert instance.pythonvalue("\r\n\t 2016-03-04 ") == datetime.date(2016, 3, 4)

def test_pythonvalue_invalid(self):
instance = builtins.Date()
Expand Down Expand Up @@ -218,8 +269,8 @@ def test_pythonvalue(self):
class TestgYear:
def test_xmlvalue(self):
instance = builtins.gYear()
instance.xmlvalue((2001, None)) == "2001"
instance.xmlvalue((2001, pytz.utc)) == "2001Z"
assert instance.xmlvalue((2001, None)) == "2001"
assert instance.xmlvalue((2001, pytz.utc)) == "2001Z"

def test_pythonvalue(self):
instance = builtins.gYear()
Expand All @@ -229,6 +280,7 @@ def test_pythonvalue(self):
assert instance.pythonvalue("2001+00:00") == (2001, pytz.utc)
assert instance.pythonvalue("-2001") == (-2001, None)
assert instance.pythonvalue("-20000") == (-20000, None)
assert instance.pythonvalue(" \t2001+02:00\r\n ") == (2001, pytz.FixedOffset(120))

with pytest.raises(builtins.ParseError):
assert instance.pythonvalue("99")
Expand All @@ -247,6 +299,7 @@ def test_pythonvalue(self):
assert instance.pythonvalue("--11-01-04:00") == (11, 1, pytz.FixedOffset(-240))
assert instance.pythonvalue("--11-15") == (11, 15, None)
assert instance.pythonvalue("--02-29") == (2, 29, None)
assert instance.pythonvalue("\t\r\n --05-01 ") == (5, 1, None)

with pytest.raises(builtins.ParseError):
assert instance.pythonvalue("99")
Expand All @@ -265,6 +318,7 @@ def test_pythonvalue(self):
assert instance.pythonvalue("--11-04:00") == (11, pytz.FixedOffset(-240))
assert instance.pythonvalue("--11") == (11, None)
assert instance.pythonvalue("--02") == (2, None)
assert instance.pythonvalue("\n\t --11Z \r") == (11, pytz.utc)

with pytest.raises(builtins.ParseError):
assert instance.pythonvalue("99")
Expand All @@ -291,6 +345,7 @@ def test_pythonvalue(self):
assert instance.pythonvalue("---01-04:00") == (1, pytz.FixedOffset(-240))
assert instance.pythonvalue("---15") == (15, None)
assert instance.pythonvalue("---31") == (31, None)
assert instance.pythonvalue("\r\n \t---31 ") == (31, None)
with pytest.raises(builtins.ParseError):
assert instance.pythonvalue("99")

Expand Down