Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d9da141
[info] Update version number
mpsonntag Mar 30, 2020
710ccbd
[format] Add property cardinality attribute
mpsonntag Mar 30, 2020
9822545
[property] Add cardinality to init
mpsonntag Mar 30, 2020
d11c7a9
[property] Add cardinality accessor methods
mpsonntag Mar 30, 2020
61f7142
[property] Properly handle cardinality setter
mpsonntag Mar 30, 2020
0d69b54
[property] Add set cardinality convenience method
mpsonntag Mar 30, 2020
06e53bd
[test/dumper] Fix stdout testing issue
mpsonntag Mar 30, 2020
efaa298
[tools/xmlparser] Add parse_cardinality method
mpsonntag Mar 30, 2020
59835f9
[tools/dictparser] Add parse_cardinality method
mpsonntag Mar 30, 2020
001a725
[validation] Add prop value cardinality validation
mpsonntag Mar 31, 2020
d63dec6
[info] Library version number set to 1.5.0
mpsonntag Apr 1, 2020
d2fda93
[property] Rename method to set_values_cardinality
mpsonntag Apr 1, 2020
e5a5ac7
[validation] Shorten prop val cardinality message
mpsonntag Apr 1, 2020
8a49b32
[property] Add validation on val_cardinality set
mpsonntag Apr 1, 2020
c7a7db7
[property] Add validation on values set
mpsonntag Apr 1, 2020
9b23378
[test/property] Add basic values cardinality test
mpsonntag Apr 4, 2020
bff325c
[test/property_int] Add values cardinality test
mpsonntag Apr 4, 2020
bb30904
[test/validation_integration] Add test file
mpsonntag Apr 4, 2020
b764372
[test/validation_int] Add buffer read method
mpsonntag Apr 4, 2020
89c8538
[test/validation_int] Test prop values cardinality
mpsonntag Apr 4, 2020
6a9f3f7
[test/validation] Add prop values cardinality test
mpsonntag Apr 4, 2020
ad4cc18
[test/property] set_values_cardinality method test
mpsonntag Apr 4, 2020
e04387d
[property] Clarify values cardinality exc msgs
mpsonntag Apr 4, 2020
79e5acf
[test/validation_init] Fix Win stdout redirect
mpsonntag Apr 5, 2020
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
3 changes: 2 additions & 1 deletion odml/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ class Property(Format):
'uncertainty': 0,
'reference': 0,
'type': 0,
'value_origin': 0
'value_origin': 0,
'val_cardinality': 0
}
_map = {
'dependencyvalue': 'dependency_value',
Expand Down
2 changes: 1 addition & 1 deletion odml/info.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"VERSION": "1.4.5",
"VERSION": "1.5.0",
"FORMAT_VERSION": "1.1",
"AUTHOR": "Hagen Fritsch, Jan Grewe, Christian Kellner, Achilleas Koutsou, Michael Sonntag, Lyuba Zehl",
"COPYRIGHT": "(c) 2011-2020, German Neuroinformatics Node",
Expand Down
98 changes: 97 additions & 1 deletion odml/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ class BaseProperty(base.BaseObject):
:param oid: object id, UUID string as specified in RFC 4122. If no id is provided,
an id will be generated and assigned. An id has to be unique
within an odML Document.
:param val_cardinality: Value cardinality defines how many values are allowed for this Property.
By default unlimited values can be set.
A required number of values can be set by assigning a tuple of the
format "(min, max)".
:param value: Legacy code to the 'values' attribute. If 'values' is provided,
any data provided via 'value' will be ignored.
"""
Expand All @@ -91,7 +95,7 @@ class BaseProperty(base.BaseObject):
def __init__(self, name=None, values=None, parent=None, unit=None,
uncertainty=None, reference=None, definition=None,
dependency=None, dependency_value=None, dtype=None,
value_origin=None, oid=None, value=None):
value_origin=None, oid=None, val_cardinality=None, value=None):

try:
if oid is not None:
Expand All @@ -115,6 +119,7 @@ def __init__(self, name=None, values=None, parent=None, unit=None,
self._definition = definition
self._dependency = dependency
self._dependency_value = dependency_value
self._val_cardinality = None

self._dtype = None
if dtypes.valid_type(dtype):
Expand All @@ -129,6 +134,10 @@ def __init__(self, name=None, values=None, parent=None, unit=None,

self.parent = parent

# Cardinality should always be set after values have been added
# since it is always tested against values when it is set.
self.val_cardinality = val_cardinality

for err in validation.Validation(self).errors:
if err.is_error:
msg = "\n\t- %s %s: %s" % (err.obj, err.rank, err.msg)
Expand Down Expand Up @@ -401,6 +410,11 @@ def values(self, new_value):
raise ValueError(msg)
self._values = [dtypes.get(v, self.dtype) for v in new_value]

# Validate and inform user if the current values cardinality is violated
valid = validation.Validation(self)
for err in valid.errors:
print("%s: %s" % (err.rank.capitalize(), err.msg))

@property
def value_origin(self):
"""
Expand Down Expand Up @@ -507,6 +521,88 @@ def dependency_value(self, new_value):
new_value = None
self._dependency_value = new_value

@property
def val_cardinality(self):
"""
The value cardinality of a Property. It defines how many values
are minimally required and how many values should be maximally
stored. Use 'values_set_cardinality' to set.
"""
return self._val_cardinality

@val_cardinality.setter
def val_cardinality(self, new_value):
"""
Sets the values cardinality of a Property.

The following cardinality cases are supported:
(n, n) - default, no restriction
(d, n) - minimally d entries, no maximum
(n, d) - maximally d entries, no minimum
(d, d) - minimally d entries, maximally d entries

Only positive integers are supported. 'None' is used to denote
no restrictions on a maximum or minimum.

:param new_value: Can be either 'None', a positive integer, which will set
the maximum or an integer 2-tuple of the format '(min, max)'.
"""
invalid_input = False
exc_msg = "Can only assign positive single int or int-tuples of the format '(min, max)'"

# Empty values reset the cardinality to None.
if not new_value or new_value == (None, None):
self._val_cardinality = None

# Providing a single integer sets the maximum value in a tuple.
elif isinstance(new_value, int) and new_value > 0:
self._val_cardinality = (None, new_value)

# Only integer 2-tuples of the format '(min, max)' are supported to set the cardinality
elif isinstance(new_value, tuple) and len(new_value) == 2:
v_min = new_value[0]
v_max = new_value[1]

min_int = isinstance(v_min, int) and v_min >= 0
max_int = isinstance(v_max, int) and v_max >= 0

if max_int and min_int and v_max > v_min:
self._val_cardinality = (v_min, v_max)

elif max_int and not v_min:
self._val_cardinality = (None, v_max)

elif min_int and not v_max:
self._val_cardinality = (v_min, None)

else:
invalid_input = True

# Use helpful exception message in the following case:
if max_int and min_int and v_max < v_min:
exc_msg = "Minimum larger than maximum (min=%s, max=%s)" % (v_min, v_max)
else:
invalid_input = True

if not invalid_input:
# Validate and inform user if the current values cardinality is violated
valid = validation.Validation(self)
for err in valid.errors:
print("%s: %s" % (err.rank.capitalize(), err.msg))
else:
raise ValueError(exc_msg)

def set_values_cardinality(self, min_val=None, max_val=None):
"""
Sets the values cardinality of a Property.

:param min_val: Required minimal number of values elements. None denotes
no restrictions on values elements minimum. Default is None.
:param max_val: Allowed maximal number of values elements. None denotes
no restrictions on values elements maximum. Default is None.
"""
self.val_cardinality = (min_val, max_val)

def remove(self, value):
"""
Remove a value from this property. Only the first encountered
Expand Down
47 changes: 46 additions & 1 deletion odml/tools/dict_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,47 @@
from .parser_utils import InvalidVersionException, ParserException, odml_tuple_export


def parse_cardinality(vals):
"""
Parses an odml specific cardinality from an input value.

If the input content is valid, returns an appropriate tuple.
Returns None if the input is empty or the content cannot be
properly parsed.

:param vals: list or tuple
:return: None or 2-tuple
"""
if not vals:
return None

if isinstance(vals, (list, tuple)) and len(vals) == 2:
min_val = vals[0]
max_val = vals[1]

if min_val is None or str(min_val).strip() == "None":
min_val = None

if max_val is None or str(max_val).strip() == "None":
max_val = None

min_int = isinstance(min_val, int) and min_val >= 0
max_int = isinstance(max_val, int) and max_val >= 0

if min_int and max_int and max_val > min_val:
return min_val, max_val

if min_int and not max_val:
return min_val, None

if max_int and not min_val:
return None, max_val

# We were not able to properly parse the current cardinality, so add
# an appropriate Error/Warning once the reader 'ignore_errors' option has been implemented.
return None


class DictWriter:
"""
A writer to parse an odml.Document to a Python dictionary object equivalent.
Expand Down Expand Up @@ -255,8 +296,12 @@ def parse_properties(self, props_list):
for i in _property:
attr = self.is_valid_attribute(i, odmlfmt.Property)
if attr:
content = _property[attr]
if attr.endswith("_cardinality"):
content = parse_cardinality(content)

# Make sure to always use the correct odml format attribute name
prop_attrs[odmlfmt.Property.map(attr)] = _property[attr]
prop_attrs[odmlfmt.Property.map(attr)] = content

prop = odmlfmt.Property.create(**prop_attrs)
odml_props.append(prop)
Expand Down
40 changes: 40 additions & 0 deletions odml/tools/xmlparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,43 @@
"""


def parse_cardinality(val):
"""
Parses an odml specific cardinality from a string.

If the string content is valid, returns an appropriate tuple.
Returns None if the string is empty or the content cannot be
properly parsed.

:param val: string
:return: None or 2-tuple
"""
if not val:
return None

# Remove parenthesis and split on comma
parsed_vals = val.strip()[1:-1].split(",")
if len(parsed_vals) == 2:
min_val = parsed_vals[0].strip()
max_val = parsed_vals[1].strip()

min_int = min_val.isdigit() and int(min_val) >= 0
max_int = max_val.isdigit() and int(max_val) >= 0

if min_int and max_int and int(max_val) > int(min_val):
return int(min_val), int(max_val)

if min_int and max_val == "None":
return int(min_val), None

if max_int and min_val == "None":
return None, int(max_val)

# Todo we were not able to properly parse the current cardinality
# add an appropriate Error/Warning
return None


def to_csv(val):
"""
Modifies odML values for serialization to strings and files.
Expand Down Expand Up @@ -410,6 +447,9 @@ def parse_tag(self, root, fmt, insert_children=True):
if tag == "values" and curr_text:
content = from_csv(node.text)
arguments[tag] = content
# Special handling of cardinality
elif tag.endswith("_cardinality") and curr_text:
arguments[tag] = parse_cardinality(node.text)
else:
arguments[tag] = curr_text
else:
Expand Down
29 changes: 29 additions & 0 deletions odml/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,32 @@ def property_values_string_check(prop):


Validation.register_handler('property', property_values_string_check)


def property_values_cardinality(prop):
"""
Checks Property values against any set value cardinality.

:param prop: odml.Property
:return: Yields a ValidationError warning, if a set cardinality is not met.
"""
if prop.val_cardinality and isinstance(prop.val_cardinality, tuple):

val_min = prop.val_cardinality[0]
val_max = prop.val_cardinality[1]

val_len = len(prop.values) if prop.values else 0

invalid_cause = ""
if val_min and val_len < val_min:
invalid_cause = "minimum %s" % val_min
elif val_max and (prop.values and len(prop.values) > val_max):
invalid_cause = "maximum %s" % val_max

if invalid_cause:
msg = "Property values cardinality violated"
msg += " (%s values, %s found)" % (invalid_cause, val_len)
yield ValidationError(prop, msg, LABEL_WARNING)


Validation.register_handler("property", property_values_cardinality)
31 changes: 17 additions & 14 deletions test/test_dumper.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import unittest
import sys
import odml

try:
from StringIO import StringIO
except ImportError:
from io import StringIO

import odml

from odml.tools.dumper import dump_doc


class TestTypes(unittest.TestCase):

def setUp(self):
# Capture the output printed by the functions to STDOUT, and use it for
# testing purposes.
self.captured_stdout = StringIO()
sys.stdout = self.captured_stdout

s_type = "type"

self.doc = odml.Document(author='Rave', version='1.0')
Expand All @@ -38,10 +35,19 @@ def setUp(self):
self.doc.append(s1)

def test_dump_doc(self):
# Capture the output printed by the functions to STDOUT, and use it for
# testing purposes. It needs to be reset after the capture.
captured_stdout = StringIO()
sys.stdout = captured_stdout

# This test dumps the whole document and checks it word by word.
# If possible, maybe some better way of testing this ?
odml.tools.dumper.dump_doc(self.doc)
output = [x.strip() for x in self.captured_stdout.getvalue().split('\n') if x]
# If possible, maybe some better way of testing this?
dump_doc(self.doc)
output = [x.strip() for x in captured_stdout.getvalue().split('\n') if x]

# Reset stdout
sys.stdout = sys.__stdout__

expected_output = []
expected_output.append("*Cell (type='type')")
expected_output.append(":Type (values=Rechargeable, dtype='string')")
Expand All @@ -50,10 +56,7 @@ def test_dump_doc(self):
expected_output.append("*Electrode (type='type')")
expected_output.append(":Material (values=Nickel, dtype='string')")
expected_output.append(":Models (values=[AA,AAA], dtype='string')")

self.assertEqual(len(output), len(expected_output))
for i in range(len(output)):
self.assertEqual(output[i], expected_output[i])

# Discard the document output from stdout stream
self.captured_stdout.seek(0)
self.captured_stdout.truncate(0)
Loading