Skip to content

Commit

Permalink
Add field setting hooks for SchemeNode
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Lasevich committed Aug 22, 2019
1 parent ded7cc4 commit d38c5ed
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 30 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
| :---: |

## QuickScheme Release Notes
* 0.2.0
* Add before/instead/after field set hooks
* 0.1.1
* Add Validators
* 0.1.0
Expand Down
2 changes: 1 addition & 1 deletion package.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[Package]
version = 0.1.1pre
version = 0.2.0
name = QuickScheme
description = Quick Way To Define Data Schema and Mapping Data To Objects
url = https://mlasevich.github.io/QuickScheme/
Expand Down
10 changes: 9 additions & 1 deletion src/quick_scheme/base_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __init__(self, parent=None, identity=None, data=None, **kwargs):
self._data = None
self.quick_scheme = ProxyAccess(self)
self._set_identity(identity)
self._set_data(data)
self._from_data(data)
self._is_valid = self._validate(True)

def _root(self):
Expand Down Expand Up @@ -126,6 +126,14 @@ def __repr__(self, *_args, **_kwargs):
def _initialize(self, *args, **kwargs):
''' Initialization hook for object - called before the data is set'''

def _from_data(self, data):
''' Set this parameter from data '''
self._set_data(data)
self._inflate()

def _inflate(self):
''' Post Load Inflate Hook '''

def _set_data(self, data):
''' Set data for this object '''
self._data = data
Expand Down
16 changes: 9 additions & 7 deletions src/quick_scheme/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def get_data(self):
value = self.get()
if isinstance(value, SchemeBaseNode):
data = value.quick_scheme.get_data()
#ident = value.quick_scheme.get_identity()
# ident = value.quick_scheme.get_identity()
# print("** Data for %s - %s is %s (%s)" %
# (ident if ident is not None else '*', self.name(), data, type(data)))

Expand All @@ -87,10 +87,12 @@ def get(self):
def is_valid(self):
''' Check if this field value is valid '''
if self.node:
if self.field.required and not self.is_set:
LOG.debug("Field '%s' in '%s' is required but not set",
self.name(), self.node._path_str())
return False
if not self.is_set:
if self.field.required:
LOG.debug("Field '%s' in '%s' is required but not set",
self.name(), self.node._path_str())
return False
return True
return self.field.validate(self)
return True

Expand Down Expand Up @@ -127,14 +129,14 @@ def __getattr__(self, item):

@property
def is_qs_node(self):
'''
'''
Returns true, if type for this field is a QuickScheme Node (i.e. a child of SchemeBaseNode)
'''
return issubclass(self.ftype, SchemeBaseNode)

@property
def has_default(self):
'''
'''
Returns True if this field has a default value '''
default = self._data.get('default', None)
return default is not None
Expand Down
2 changes: 1 addition & 1 deletion src/quick_scheme/nodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
from .key_based_list import KeyBasedListNode, KeyBasedList
from .list_of_nodes import ListOfNodes
from .list_of_references import ListOfReferences
from .node import SchemeNode
from quick_scheme.nodes.scheme_node import SchemeNode
2 changes: 1 addition & 1 deletion src/quick_scheme/nodes/key_based_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from ..base_node import SchemeBaseNode
from quick_scheme.field import FieldValue
from .node import SchemeNode
from quick_scheme.nodes.scheme_node import SchemeNode


class KeyBasedListNode(SchemeNode):
Expand Down
2 changes: 1 addition & 1 deletion src/quick_scheme/nodes/key_based_list_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from quick_scheme.field import Field
from .key_based_list import KeyBasedList, KeyBasedListInst, KeyBasedListNode
from .node import SchemeNode
from quick_scheme.nodes.scheme_node import SchemeNode


def clean_dict(data):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@
Base Node Types
'''
from email.policy import default
import logging

from quick_scheme.field import FieldValue

from ..base_node import SchemeBaseNode
from ..exceptions import QuickSchemeValidationException
from ..qs_yaml import UnsortableOrderedDict
from ..utils import Args


LOG = logging.getLogger(__name__)


class SchemeNode(SchemeBaseNode):
''' Basic Node - a dictionary with defined Fields '''

# List of fields
FIELDS = []
# Allow undefined fields. if false, throw exception on undefined field
Expand Down Expand Up @@ -110,27 +111,56 @@ def _fields(self):
''' Get fields '''
return self._int_get('fields', {})

def _run_if_present(self, name, default_value=None, args=None):
''' Run method if present '''
if args is None:
args = Args()
if hasattr(self, name):
LOG.warning("Found method %s", name)
return args.run(getattr(self, name), default)
return default_value

def _set_field_data(self, field, value, extra_data):
''' Internal part of set field data '''
indexed_value_holder = self._fields.get(field, None)
if indexed_value_holder is not None:
_, value_holder = indexed_value_holder
value_holder.set(value, parent=self, identity=field)
field_exists = True
else:
field_exists = False
if self.ALLOW_UNDEFINED:
LOG.warning("M3: adding extra field %s", field)
extra_data[field] = value
else:
raise QuickSchemeValidationException("Invalid field '%s' for '%s'" % (field,
self._name))
return field_exists

def _init_field(self, field, value, extra_data):
''' Set data for one field. This is the outer call that calls hooks '''
args = Args(value, field=field, extra_data=extra_data)
value = self._run_if_present('_before_set_%s' % field, value, args)
field_exists = False
if not self._run_if_present('_do_set_%s' % field, False, args):
field_exists = self._set_field_data(field, value, extra_data)
return self._run_if_present('_after_set_%s' % field, field_exists, args)

def _set_data(self, data):
''' Set data for this object '''
extra_data = self._get_map_class_instance()

fields = self._fields
keys_set = []
if isinstance(data, dict):
for key, value in data.items():
indexed_value_holder = fields.get(key, None)
if indexed_value_holder is not None:
_, value_holder = indexed_value_holder
value_holder.set(value, parent=self, identity=key)
else:
if self.ALLOW_UNDEFINED:
extra_data[key] = value
else:
raise QuickSchemeValidationException(
"Invalid field '%s' for '%s'" % (key, self._name))
for field, value in data.items():
keys_set.append(object)
self._init_field(field, value, extra_data)
else:
# print("Invalid data type: %s for %s (%s)" %
# (type(data), self.quick_scheme.path_str(), data))
self._brief_set(data)

LOG.error("%s fields not set, %s extra fields when setting %s from %s",
len(keys_set), len(extra_data), self._path_str(), data)

self._data = extra_data

def _get_by_id(self, id):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import logging
import unittest

from ..exceptions import QuickSchemeValidationException
from quick_scheme.field import Field
from .node import SchemeNode
from quick_scheme.nodes.scheme_node import SchemeNode

from ..exceptions import QuickSchemeValidationException


class MyEmptyNode(SchemeNode):
Expand Down Expand Up @@ -39,6 +40,22 @@ class MyOpenNode(MyClosedNode):
Field('integer_with_default_2', ftype=int, default=2, always=False),
]

def _before_set_b4(self, value, **_):
''' Runs Before b4 is set '''
value = "__" + value + "__"
return value

def _after_set_after(self, value, field, extra_data):
''' Runs After 'after' is set '''
extra_data['after_%s' % field] = value
return True

def _do_set_override(self, value, field, extra_data):
''' Runs instead setting 'override' '''
print("Overriding!")
extra_data['instead'] = {field: value}
return True


class TestSchemeNode(unittest.TestCase):
'''Field tests'''
Expand Down Expand Up @@ -130,6 +147,43 @@ def test_open_node_data(self):
'integer_no_default': 5,
'integer_with_default_1': 1})

def test_open_node_data_hooks_b4(self):
''' Test before_set hook'''
node = MyOpenNode(
None, data={'id': 'value', 'integer_no_default': 5, 'b4': 'my_before'})
self.assertEqual(len(node.quick_scheme.fields),
len(MyClosedNode.FIELDS))
self.assertDictEqual(node.quick_scheme.get_data(), {'id': 'value',
'name': 'def',
'integer_no_default': 5,
'integer_with_default_1': 1,
'b4': '__my_before__'})

def test_open_node_data_hooks_after(self):
''' Test after_set hook'''
node = MyOpenNode(
None, data={'id': 'value', 'integer_no_default': 5, 'after': 'my_after'})
self.assertEqual(len(node.quick_scheme.fields),
len(MyClosedNode.FIELDS))
self.assertDictEqual(node.quick_scheme.get_data(), {'id': 'value',
'name': 'def',
'integer_no_default': 5,
'integer_with_default_1': 1,
'after': 'my_after',
'after_after': 'my_after'})

def test_open_node_data_hooks_instead(self):
''' Test 'on-set' hook '''
node = MyOpenNode(
None, data={'id': 'value', 'integer_no_default': 5, 'override': 'my_instead'})
self.assertEqual(len(node.quick_scheme.fields),
len(MyClosedNode.FIELDS))
self.assertDictEqual(node.quick_scheme.get_data(), {'id': 'value',
'name': 'def',
'integer_no_default': 5,
'integer_with_default_1': 1,
'instead': {'override': 'my_instead'}})

def test_open_node_extra_data(self):
''' Test getting action_desc'''

Expand Down
2 changes: 1 addition & 1 deletion src/quick_scheme/proxy_access_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import unittest

from .base_node import ProxyAccess
from .nodes.node import SchemeNode
from quick_scheme.nodes.scheme_node import SchemeNode


class MyEmptyNode(SchemeNode):
Expand Down
18 changes: 18 additions & 0 deletions src/quick_scheme/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
''' Utilities '''
import logging

LOG = logging.getLogger(__name__)


class Args(object):
''' Argument Holder '''

def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs

def run(self, function, default=None):
''' Run a runable function'''
if callable(function):
return function(*self.args, **self.kwargs)
return default

0 comments on commit d38c5ed

Please sign in to comment.