diff --git a/goblin/__init__.py b/goblin/__init__.py index 354f37e7ffa59c4c4ae5d3bdcd27ff95f13ff315..5be1e8bcb0dceea5e331e4f9bd4bce292186432c 100644 --- a/goblin/__init__.py +++ b/goblin/__init__.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Affero General Public License # along with Goblin. If not, see <http://www.gnu.org/licenses/>. -from goblin.element import Vertex, Edge, VertexProperty from goblin.app import create_app, Goblin +from goblin.cardinality import Cardinality +from goblin.element import Vertex, Edge, VertexProperty from goblin.properties import Property, String diff --git a/goblin/abc.py b/goblin/abc.py index 5abb4c9c355e3c69ce96293902bd8574fd2db370..91b8a1240ae53515643fce60540e403acdcf4359 100644 --- a/goblin/abc.py +++ b/goblin/abc.py @@ -16,6 +16,12 @@ # along with Goblin. If not, see <http://www.gnu.org/licenses/>. import abc +import logging + +from goblin import cardinality, manager, exception + + +logger = logging.getLogger(__name__) class DataType(abc.ABC): @@ -44,35 +50,37 @@ class DataType(abc.ABC): @abc.abstractmethod def to_ogm(self, val): """Convert property value to a Python compatible format""" - try: - self.validate(val) - except exception.ValidationError: - logger.warning( - "DB val {} Fails OGM validation for {}".format(val, self)) return val - def validate_cardinality(self, val, cardinality): - if cardinality: - if issubclass(cardinality, list): - if isinstance(val, list): - val = val - elif isinstance(val, set): - val = list(set) - else: - val = [val] - # Check here if vertex prop - val = [self.validate(v) for v in val] - elif issubclass(cardinality, set): - if isinstance(val, set): - val = val - elif isinstance(val, list): - val = set(val) - else: - val = set([val]) - # Check here if vertex prop - val = {self.validate(v) for v in val } + def validate_vertex_prop(self, val, card, vertex_prop, data_type): + if card == cardinality.Cardinality.list: + if isinstance(val, list): + val = val + elif isinstance(val, (set, tuple)): + val = list(val) + else: + val = [val] + val = manager.ListVertexPropertyManager( + data_type, + vertex_prop, + card, + [vertex_prop(data_type, val=self.validate(v), card=card) + for v in val]) + elif card == cardinality.Cardinality.set: + if isinstance(val, set): + val = val + elif isinstance(val, (list, tuple)): + val = set(val) + else: + val = set([val]) + val = manager.SetVertexPropertyManager( + data_type, + vertex_prop, + card, + {vertex_prop(data_type, val=self.validate(v), card=card) + for v in val}) else: - val = self.validate(val) + val = vertex_prop(data_type, val=self.validate(val)) return val @@ -81,7 +89,3 @@ class BaseProperty: @property def data_type(self): raise NotImplementedError - - @property - def cardinality(self): - raise NotImplementedError diff --git a/goblin/cardinality.py b/goblin/cardinality.py new file mode 100644 index 0000000000000000000000000000000000000000..ce477a61af1259b0f2812f3346732cf71c3a7e65 --- /dev/null +++ b/goblin/cardinality.py @@ -0,0 +1,27 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +"""Enum to handle property cardinality""" + +import enum + + +class Cardinality(enum.Enum): + """Property cardinality""" + single = 1 + list = 2 + set = 3 diff --git a/goblin/element.py b/goblin/element.py index 989663a8ce015d05c3dde829167b0ce362211f29..bc8be9a441bc43bb80acbcd047c3eaefa78f96a4 100644 --- a/goblin/element.py +++ b/goblin/element.py @@ -19,10 +19,7 @@ import logging import inflection -from goblin import abc -from goblin import exception -from goblin import mapper -from goblin import properties +from goblin import abc, cardinality, exception, mapper, properties logger = logging.getLogger(__name__) @@ -44,9 +41,9 @@ class ElementMeta(type): new_namespace = {} for k, v in namespace.items(): if isinstance(v, abc.BaseProperty): - if element_type == 'edge' and v.cardinality is not None: + if element_type == 'edge' and hasattr(v, 'cardinality'): raise exception.MappingError( - 'Edge property with set/list cardinality') + 'Edge property cannot have set/list cardinality') props[k] = v v = v.__descriptor__(k, v) new_namespace[k] = v @@ -115,7 +112,7 @@ class GenericEdge(Edge): Class used to build edges when user defined edges class is not available. Generally not instantiated by end user. """ - + pass class VertexPropertyDescriptor: """ @@ -128,44 +125,41 @@ class VertexPropertyDescriptor: self._vertex_property = vertex_property.__class__ self._data_type = vertex_property.data_type self._default = vertex_property.default + self._cardinality = vertex_property._cardinality def __get__(self, obj, objtype): if obj is None: - return self._vertex_property + return self._vertex_property # do this like property default = self._default if default: - default = self._data_type.validate(default) - default = self._vertex_property(self._default) + default = self._data_type.validate_vertex_prop( + default, self._cardinality, self._vertex_property, + self._data_type) return getattr(obj, self._name, default) def __set__(self, obj, val): - if isinstance(val, (list, tuple , set)): - vertex_property = [] - for v in val: - v = self._data_type.validate(v) - vertex_property.append( - self._vertex_property(self._data_type, value=v)) - - else: - val = self._data_type.validate(val) - vertex_property = self._vertex_property(self._data_type, value=val) - setattr(obj, self._name, vertex_property) + if val: + val = self._data_type.validate_vertex_prop( + val, self._cardinality, self._vertex_property, self._data_type) + setattr(obj, self._name, val) -class VertexProperty(Element, abc.BaseProperty): +class VertexProperty(Vertex, abc.BaseProperty): """Base class for user defined vertex properties. Not yet supported.""" __descriptor__ = VertexPropertyDescriptor - def __init__(self, data_type, *, value=None, default=None, db_name=None, - cardinality=None): + def __init__(self, data_type, *, val=None, default=None, db_name=None, + card=None): if isinstance(data_type, type): data_type = data_type() self._data_type = data_type - self._value = value + self._val = val self._default = default self._db_name = db_name - self._cardinality = cardinality + if card is None: + card = cardinality.Cardinality.single + self._cardinality = card @property def default(self): @@ -177,7 +171,7 @@ class VertexProperty(Element, abc.BaseProperty): @property def value(self): - return self._value + return self._val @property def db_name(self): diff --git a/goblin/manager.py b/goblin/manager.py new file mode 100644 index 0000000000000000000000000000000000000000..9de16b6d64bbcc9e5a6cbd79a260d55dacdc694d --- /dev/null +++ b/goblin/manager.py @@ -0,0 +1,43 @@ +"""Managers for multi cardinality vertex properties""" + + +class VertexPropertyManager: + + def __call__(self, val): + results = [] + for v in self: + if v.value == val: + results.append(v) + if len(results) == 1: + results = results[0] + elif not results: + results = None + return results + + +class ListVertexPropertyManager(list, VertexPropertyManager): + + def __init__(self, data_type, vertex_prop, card, obj): + self._data_type = data_type + self._vertex_prop = vertex_prop + self._card = card + list.__init__(self, obj) + + def append(self, val): + val = self._data_type.validate(val) + val = self._vertex_prop(self._data_type, val=val, card=self._card) + super().append(val) + + +class SetVertexPropertyManager(set, VertexPropertyManager): + + def __init__(self, data_type, vertex_prop, card, obj): + self._data_type = data_type + self._vertex_prop = vertex_prop + self._card = card + set.__init__(self, obj) + + def add(self, val): + val = self._data_type.validate(val) + val = self._vertex_prop(self._data_type, val=val, card=self._card) + super().add(val) diff --git a/goblin/mapper.py b/goblin/mapper.py index 0a086de3511457454f46394314dbba804a94d95c..793d7851b5068db302b30599bdbbdd00c1cdd878 100644 --- a/goblin/mapper.py +++ b/goblin/mapper.py @@ -25,13 +25,37 @@ from goblin import exception logger = logging.getLogger(__name__) +# def map_props_to_db(element, mapping): +# """Convert OGM property names/values to DB property names/values""" +# property_tuples = [] +# props = mapping.ogm_properties +# for ogm_name, (db_name, data_type) in props.items(): +# val = getattr(element, ogm_name, None) +# property_tuples.append((db_name, data_type.to_db(val))) +# return property_tuples + + +#######IMPLEMENT def map_props_to_db(element, mapping): """Convert OGM property names/values to DB property names/values""" property_tuples = [] - props = mapping.properties + props = mapping.ogm_properties for ogm_name, (db_name, data_type) in props.items(): val = getattr(element, ogm_name, None) - property_tuples.append((db_name, data_type.to_db(val))) + # if val is list etc... these have card pass to next function maybe can give + # default option for Card, and combin this function with map props, and then add_properties + if val and isinstance(val, (list, set)): + card = None + for v in val: + # get metaprops as dic + metaprops = {} + property_tuples.append( + (card, db_name, data_type.to_db(v.value), metaprops)) + card = v.cardinality + else: + if hasattr(val, '__mapping__'): + val = val.value + property_tuples.append((None, db_name, data_type.to_db(val), None)) return property_tuples @@ -39,8 +63,12 @@ def map_vertex_to_ogm(result, element, *, mapping=None): """Map a vertex returned by DB to OGM vertex""" for db_name, value in result['properties'].items(): # This will be more complex for vertex properties... - value = value[0]['value'] - name, data_type = mapping.properties.get(db_name, (db_name, None)) + if len(value) > 1: + # parse and assign vertex props + metas + value = [v['value'] for v in value] + else: + value = value[0]['value'] + name, data_type = mapping.db_properties.get(db_name, (db_name, None)) if data_type: value = data_type.to_ogm(value) setattr(element, name, value) @@ -52,7 +80,7 @@ def map_vertex_to_ogm(result, element, *, mapping=None): def map_edge_to_ogm(result, element, *, mapping=None): """Map an edge returned by DB to OGM edge""" for db_name, value in result.get('properties', {}).items(): - name, data_type = mapping.properties.get(db_name, (db_name, None)) + name, data_type = mapping.db_properties.get(db_name, (db_name, None)) if data_type: value = data_type.to_ogm(value) setattr(element, name, value) @@ -104,19 +132,16 @@ class Mapping: self._label = namespace['__label__'] self._element_type = element_type self._mapper_func = functools.partial(mapper_func, mapping=self) - self._properties = {} - if self._element_type == 'vertex': - self._vertex_properties = {} - else: - self._vertex_properties = None + self._db_properties = {} + self._ogm_properties = {} self._map_properties(properties) - @property - def vertex_properties(self): - if self._vertex_properties is None: - raise exception.MappingError( - 'Edge mappings do not have vertex_properties') - return self._vertex_properties + # @property + # def vertex_properties(self): + # if self._vertex_properties is None: + # raise exception.MappingError( + # 'Edge mappings do not have vertex_properties') + # return self._vertex_properties @property def label(self): @@ -129,13 +154,18 @@ class Mapping: return self._mapper_func @property - def properties(self): + def db_properties(self): + """A dictionary of property mappings""" + return self._db_properties + + @property + def ogm_properties(self): """A dictionary of property mappings""" - return self._properties + return self._ogm_properties def __getattr__(self, value): try: - mapping, _ = self._properties[value] + mapping, _ = self._ogm_properties[value] return mapping except: raise exception.MappingError( @@ -144,20 +174,22 @@ class Mapping: def _map_properties(self, properties): for name, prop in properties.items(): - if hasattr(prop, '__mapping__'): - if not self._element_type == 'vertex': - raise exception.MappingError( - 'Only vertices can have vertex properties') - self._vertex_properties[name] = prop data_type = prop.data_type if prop.db_name: db_name = prop.db_name else: db_name = '{}__{}'.format(self._label, name) - self._properties[db_name] = (name, data_type) - self._properties[name] = (db_name, data_type) + if hasattr(prop, '__mapping__'): + if not self._element_type == 'vertex': + raise exception.MappingError( + 'Only vertices can have vertex properties') + # self._vertex_properties[db_name] = (name, data_type) + # self._vertex_properties[name] = (db_name, data_type) + # else: + self._db_properties[db_name] = (name, data_type) + self._ogm_properties[name] = (db_name, data_type) def __repr__(self): return '<{}(type={}, label={}, properties={})>'.format( self.__class__.__name__, self._element_type, self._label, - self._properties) + self._ogm_properties) diff --git a/goblin/properties.py b/goblin/properties.py index 344446d5989c22e0267885c4f5026a2c3b945665..91b7572094544447044597524bf98f2b61fa879f 100644 --- a/goblin/properties.py +++ b/goblin/properties.py @@ -19,7 +19,7 @@ import logging -from goblin import abc, exception +from goblin import abc, cardinality, exception logger = logging.getLogger(__name__) @@ -36,7 +36,6 @@ class PropertyDescriptor: self._name = '_' + name self._data_type = prop.data_type self._default = prop.default - self._cardinality = prop.cardinality def __get__(self, obj, objtype): if obj is None: @@ -44,7 +43,7 @@ class PropertyDescriptor: return getattr(obj, self._name, self._default) def __set__(self, obj, val): - val = self._data_type.validate_cardinality(val, self._cardinality) + val = self._data_type.validate(val) setattr(obj, self._name, val) def __delete__(self, obj): @@ -66,13 +65,9 @@ class Property(abc.BaseProperty): __descriptor__ = PropertyDescriptor - def __init__(self, data_type, *, cardinality=None, db_name=None, - default=None): + def __init__(self, data_type, *, db_name=None, default=None): if isinstance(data_type, type): data_type = data_type() - if cardinality is not None: - cardinality = cardinality() - self._cardinality = cardinality self._data_type = data_type self._db_name = db_name self._default = default @@ -89,10 +84,6 @@ class Property(abc.BaseProperty): def default(self): return self._default - @property - def cardinality(self): - return self._cardinality - # Data types class String(abc.DataType): @@ -120,7 +111,7 @@ class Integer(abc.DataType): if val is not None: try: return int(val) - except ValueError as e: + except (ValueError, TypeError) as e: raise exception.ValidationError( 'Not a valid integer: {}'.format(val)) from e diff --git a/goblin/session.py b/goblin/session.py index 4a3c832a6c7a9f920c1368949e9aa516f4d7d8cd..6ed6f673a5a93d76b24942cb3f2445ad7194a3c1 100644 --- a/goblin/session.py +++ b/goblin/session.py @@ -276,6 +276,7 @@ class Session(connection.AbstractConnection): :returns: :py:class:`Vertex<goblin.element.Vertex>` object """ props = mapper.map_props_to_db(vertex, vertex.__mapping__) + # vert_props = mapper.map_vert_props_to_db traversal = self.g.V(vertex.id) return await self._update_vertex_properties(vertex, traversal, props) @@ -346,14 +347,15 @@ class Session(connection.AbstractConnection): return await stream.fetch_data() async def _update_vertex_properties(self, element, traversal, props): - traversal, removals = self.traversal_factory.add_vertex_properties( + traversal, removals = self.traversal_factory.add_properties( traversal, props) + # traversal, removals = self.traversal_factory.add_vertex_properties(...) for k in removals: await self.g.V(element.id).properties(k).drop().one_or_none() return traversal async def _update_edge_properties(self, element, traversal, props): - traversal, removals = self.traversal_factory.add_edge_properties( + traversal, removals = self.traversal_factory.add_properties( traversal, props) for k in removals: await self.g.E(element.id).properties(k).drop().one_or_none() diff --git a/goblin/traversal.py b/goblin/traversal.py index 895295047552fea8168f202b1f49f9ba7781e952..0a2b3acd63dacc5c26636eea70d8ee0c0d696761 100644 --- a/goblin/traversal.py +++ b/goblin/traversal.py @@ -21,7 +21,7 @@ import asyncio import functools import logging -from goblin import element, mapper +from goblin import cardinality, element, mapper from goblin.driver import connection, graph from gremlin_python import process @@ -42,7 +42,7 @@ def bindprop(element_class, ogm_name, val, *, binding=None): :returns: tuple object ('db_property_name', ('binding(if passed)', val)) """ db_name = getattr(element_class, ogm_name, ogm_name) - _, data_type = element_class.__mapping__.properties[ogm_name] + _, data_type = element_class.__mapping__.ogm_properties[ogm_name] val = data_type.to_db(val) if binding: val = (binding, val) @@ -142,8 +142,10 @@ class TraversalFactory: def add_vertex(self, elem): """Convenience function for generating crud traversals.""" props = mapper.map_props_to_db(elem, elem.__mapping__) + # vert_props = mapper.map_props_to_db traversal = self.traversal().addV(elem.__mapping__.label) - traversal, _ = self.add_vertex_properties(traversal, props) + traversal, _ = self.add_properties(traversal, props) + # traversal, _ = self.add_vertex_properties(...) return traversal def add_edge(self, elem): @@ -153,49 +155,58 @@ class TraversalFactory: traversal = traversal.addE(elem.__mapping__._label) traversal = traversal.to( self.traversal().V(elem.target.id)) - traversal, _ = self.add_edge_properties(traversal, props) + traversal, _ = self.add_properties(traversal, props) return traversal - def add_vertex_properties(self, traversal, props): + # def add_vertex_properties(self, traversal, props): + # # refactor + # binding = 0 + # potential_removals = [] + # for k, v in props: + # if v: + # if isinstance(v, element.VertexProperty): + # v = v.value + # if isinstance(v, (list, set)): + # new_val = [] + # for val in v: + # if isinstance(val, element.VertexProperty): + # val = val.value + # new_val.append(val) + # if isinstance(v, set): + # cardinality = process.Cardinality.set + # else: + # cardinality = process.Cardinality.list + # traversal = traversal.property( + # cardinality, + # ('k' + str(binding), k), + # ('v' + str(binding), new_val)) + # else: + # traversal = traversal.property( + # ('k' + str(binding), k), + # ('v' + str(binding), v)) + # binding += 1 + # else: + # potential_removals.append(k) + # return traversal, potential_removals + + def add_properties(self, traversal, props): binding = 0 potential_removals = [] - for k, v in props: - if v: - if isinstance(v, element.VertexProperty): - v = v.value - if isinstance(v, (list, set)): - new_val = [] - for val in v: - if isinstance(val, element.VertexProperty): - val = val.value - new_val.append(val) - if isinstance(v, set): - new_val = set(new_val) - cardinality = Cardinality.set + for card, db_name, val, metaprops in props: + if val: + key = ('k' + str(binding), db_name) + val = ('v' + str(binding), val) + if card: + if card == cardinality.Cardinality.list: + card = process.Cardinality.list + elif card == cardinality.Cardinality.set: + card = process.Cardinality.set else: - cardinality = Cardinality.list - traversal = traversal.property( - cardinality, - ('k' + str(binding), k), - ('*v' + str(binding), new_val)) + card = process.Cardinality.single + traversal = traversal.property(card, key, val) else: - traversal = traversal.property( - ('k' + str(binding), k), - ('v' + str(binding), v)) + traversal = traversal.property(key, val) binding += 1 else: - potential_removals.append(k) - return traversal, potential_removals - - def add_edge_properties(self, traversal, props): - binding = 0 - potential_removals = [] - for k, v in props: - if v: - traversal = traversal.property( - ('k' + str(binding), k), - ('v' + str(binding), v)) - binding += 1 - else: - potential_removals.append(k) + potential_removals.append(db_name) return traversal, potential_removals diff --git a/tests/conftest.py b/tests/conftest.py index fc516c5dee3d6bacf05627dde126ab4bf3b38bdd..0040676cd7751f3f803265a2d73c1b450704d33c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,24 +16,29 @@ # along with Goblin. If not, see <http://www.gnu.org/licenses/>. import pytest -from goblin import create_app, driver, element, properties +from goblin import create_app, driver, element, properties, Cardinality from gremlin_python import process -class PlaceName(element.VertexProperty): - pass +# class PlaceName(element.VertexProperty): +# pass class Person(element.Vertex): __label__ = 'person' - name = properties.Property(properties.String, cardinality=list) + name = properties.Property(properties.String) age = properties.Property(properties.Integer, db_name='custom__person__age') + birthplace = element.VertexProperty(properties.String) + nicknames = element.VertexProperty( + properties.String, card=Cardinality.list) class Place(element.Vertex): - name = PlaceName(properties.String, cardinality=set) + name = properties.Property(properties.String) zipcode = properties.Property(properties.Integer) + important_numbers = element.VertexProperty( + properties.Integer, card=Cardinality.set) class Knows(element.Edge): diff --git a/tests/test_mapper.py b/tests/test_mapper.py index 592f90060def47334daacebbfe77e906db1b6ba9..ffb86627f500ea2a34b4c75b9f94f44fe1f5a8b0 100644 --- a/tests/test_mapper.py +++ b/tests/test_mapper.py @@ -21,16 +21,26 @@ from goblin import exception, properties def test_property_mapping(person, lives_in): - db_name, data_type = person.__mapping__._properties['name'] + db_name, data_type = person.__mapping__._ogm_properties['name'] assert db_name == 'person__name' assert isinstance(data_type, properties.String) - db_name, data_type = person.__mapping__._properties['age'] + db_name, data_type = person.__mapping__._ogm_properties['age'] assert db_name == 'custom__person__age' assert isinstance(data_type, properties.Integer) - db_name, data_type = lives_in.__mapping__._properties['notes'] + db_name, data_type = lives_in.__mapping__._ogm_properties['notes'] assert db_name == 'lives_in__notes' assert isinstance(data_type, properties.String) + ogm_name, data_type = person.__mapping__._db_properties['person__name'] + assert ogm_name == 'name' + assert isinstance(data_type, properties.String) + ogm_name, data_type = person.__mapping__._db_properties['custom__person__age'] + assert ogm_name == 'age' + assert isinstance(data_type, properties.Integer) + ogm_name, data_type = lives_in.__mapping__._db_properties['lives_in__notes'] + assert ogm_name == 'notes' + assert isinstance(data_type, properties.String) + def test_label_creation(place, lives_in): assert place.__mapping__._label == 'place' diff --git a/tests/test_properties.py b/tests/test_properties.py index bcba03b917c1eba7c5631a81cdc5e56b4c659322..80997e97447d44c8d6ddbbb061c265cb9e3896cc 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -17,6 +17,8 @@ import pytest +from goblin import element, exception, manager, properties + def test_set_change_property(person, lives_in): # vertex @@ -52,6 +54,86 @@ def test_setattr_validation(person): setattr(person, 'age', 'hello') +# Vertex properties +def test_set_change_vertex_property(person): + assert not person.birthplace + person.birthplace = 'Iowa City' + assert isinstance(person.birthplace, element.VertexProperty) + assert person.birthplace.value == 'Iowa City' + person.birthplace = 'U of I Hospital' + assert person.birthplace.value == 'U of I Hospital' + + +def test_validate_vertex_prop(person): + assert not person.birthplace + person.birthplace = 1 + assert person.birthplace.value == '1' + + +def test_set_change_list_card_vertex_property(person): + assert not person.nicknames + person.nicknames = 'sly' + assert isinstance(person.nicknames, list) + assert isinstance(person.nicknames, manager.ListVertexPropertyManager) + assert isinstance(person.nicknames[0], element.VertexProperty) + assert person.nicknames[0].value == 'sly' + assert person.nicknames('sly') == person.nicknames[0] + person.nicknames = set(['sly', 'guy']) + assert isinstance(person.nicknames, list) + assert person.nicknames('sly').value == 'sly' + assert person.nicknames('guy').value == 'guy' + person.nicknames = ('sly', 'big', 'guy') + assert isinstance(person.nicknames, list) + assert [v.value for v in person.nicknames] == ['sly', 'big', 'guy'] + person.nicknames = ['sly', 'big', 'guy', 'guy'] + assert isinstance(person.nicknames, list) + assert len(person.nicknames('guy')) == 2 + assert [v.value for v in person.nicknames] == ['sly', 'big', 'guy', 'guy'] + person.nicknames.append(1) + assert person.nicknames('1').value == '1' + + +def test_list_card_vertex_property_validation(person): + person.nicknames = [1, 1.5, 2] + assert [v.value for v in person.nicknames] == ['1', '1.5', '2'] + + +def test_set_change_set_card_vertex_property(place): + assert not place.important_numbers + place.important_numbers = 1 + assert isinstance(place.important_numbers, set) + assert isinstance( + place.important_numbers, manager.SetVertexPropertyManager) + number_one, = place.important_numbers + assert isinstance(number_one, element.VertexProperty) + assert number_one.value == 1 + assert place.important_numbers(1) == number_one + place.important_numbers = [1, 2] + assert isinstance(place.important_numbers, set) + assert {v.value for v in place.important_numbers} == set([1, 2]) + place.important_numbers.add(3) + assert {v.value for v in place.important_numbers} == set([1, 2, 3]) + place.important_numbers = (1, 2, 3, 4) + assert isinstance(place.important_numbers, set) + assert {v.value for v in place.important_numbers} == set([1, 2, 3, 4]) + place.important_numbers = set([1, 2, 3]) + assert isinstance(place.important_numbers, set) + assert {v.value for v in place.important_numbers} == set([1, 2, 3]) + with pytest.raises(exception.ValidationError): + place.important_numbers.add('dude') + + +def test_set_card_validation_vertex_property(place): + with pytest.raises(exception.ValidationError): + place.important_numbers = set(['hello', 2, 3]) + + +def test_cant_set_vertex_prop_on_edge(): + with pytest.raises(exception.MappingError): + class MyEdge(element.Edge): + vert_prop = element.VertexProperty(properties.String) + + class TestString: def test_validation(self, string): diff --git a/tests/test_session.py b/tests/test_session.py index 1e89acc355f7c38d6009bb71a421eaf0e620df71..d60d3cd6ec530946ba84942e8ebf321a665c32b5 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU Affero General Public License # along with Goblin. If not, see <http://www.gnu.org/licenses/>. +"""Functional sessions tests""" + import pytest from goblin import element @@ -44,7 +46,7 @@ class TestCreationApi: jon.name = 'jonathan' jon.age = 38 leif = person_class() - leif.name = 'leif' + leif.name = 'leifur' leif.age = 28 session.add(jon, leif) assert not hasattr(jon, 'id') @@ -53,9 +55,11 @@ class TestCreationApi: assert hasattr(jon, 'id') assert session.current[jon.id] is jon assert jon.name == 'jonathan' + assert jon.age == 38 assert hasattr(leif, 'id') assert session.current[leif.id] is leif - assert leif.name == 'leif' + assert leif.name == 'leifur' + assert leif.age == 28 @pytest.mark.asyncio async def test_create_edge(self, session, person_class, place_class, @@ -182,6 +186,7 @@ class TestCreationApi: person.name = 'dave' person.age = 35 result = await session.save(person) + assert result.name == 'dave' assert result.age == 35 person.name = 'david' person.age = None @@ -267,10 +272,10 @@ class TestTraversalApi: async def test_vertex_deserialization(self, session, person_class): async with session: resp = await session.g.addV('person').property( - person_class.name, 'leif').property('birthplace', 'detroit').one_or_none() + person_class.name, 'leif').property('place_of_birth', 'detroit').one_or_none() assert isinstance(resp, person_class) assert resp.name == 'leif' - assert resp.birthplace == 'detroit' + assert resp.place_of_birth == 'detroit' @pytest.mark.asyncio async def test_edge_desialization(self, session, knows_class): diff --git a/tests/test_vertex_properties_functional.py b/tests/test_vertex_properties_functional.py new file mode 100644 index 0000000000000000000000000000000000000000..96802f05c498f0f5c0abd380b511b91af8443f7f --- /dev/null +++ b/tests/test_vertex_properties_functional.py @@ -0,0 +1,56 @@ +import pytest + + +@pytest.mark.asyncio +async def test_add_update_property(session, person): + async with session: + person.birthplace = 'Iowa City' + result = await session.save(person) + assert result.birthplace.value == 'Iowa City' + person.birthplace = 'unknown' + result = await session.save(person) + assert result.birthplace.value == 'unknown' + + +@pytest.mark.asyncio +async def test_add_update_list_card_property(session, person): + async with session: + person.nicknames = ['db', 'dirtydb'] + result = await session.save(person) + assert [v.value for v in result.nicknames] == ['db', 'dirtydb'] + person.nicknames.append('davebshow') + result = await session.save(person) + assert [v.value for v in result.nicknames] == [ + 'db', 'dirtydb', 'davebshow'] + person.nicknames = [] + result = await session.save(person) + assert not result.nicknames + person.nicknames = ['none'] + result = await session.save(person) + assert result.nicknames('none').value == 'none' + person.nicknames = None + result = await session.save(person) + assert not result.nicknames + + +@pytest.mark.asyncio +async def test_add_update_list_card_property(session, place): + async with session: + place.important_numbers = set([1, 2]) + result = await session.save(place) + assert {v.value for v in result.important_numbers} == {1, 2} + place.important_numbers = set([3, 4]) + result = await session.save(place) + assert {v.value for v in result.important_numbers} == {3, 4} + place.important_numbers.add(5) + result = await session.save(place) + assert {v.value for v in result.important_numbers} == {3, 4, 5} + place.important_numbers = set() + result = await session.save(place) + assert not result.important_numbers + place.important_numbers = set([1, 2]) + result = await session.save(place) + assert place.important_numbers(1).value == 1 + place.important_numbers = None + result = await session.save(place) + assert not result.important_numbers