From d38c5ed63ded12536795fb7e9752cd4ed89461ba Mon Sep 17 00:00:00 2001 From: Michael Lasevich Date: Wed, 21 Aug 2019 21:48:22 -0700 Subject: [PATCH] Add field setting hooks for SchemeNode --- README.md | 2 + package.cfg | 2 +- src/quick_scheme/base_node.py | 10 +++- src/quick_scheme/field.py | 16 ++--- src/quick_scheme/nodes/__init__.py | 2 +- src/quick_scheme/nodes/key_based_list.py | 2 +- src/quick_scheme/nodes/key_based_list_test.py | 2 +- .../nodes/{node.py => scheme_node.py} | 60 ++++++++++++++----- .../{node_test.py => scheme_node_test.py} | 58 +++++++++++++++++- src/quick_scheme/proxy_access_test.py | 2 +- src/quick_scheme/utils.py | 18 ++++++ 11 files changed, 144 insertions(+), 30 deletions(-) rename src/quick_scheme/nodes/{node.py => scheme_node.py} (76%) rename src/quick_scheme/nodes/{node_test.py => scheme_node_test.py} (70%) create mode 100644 src/quick_scheme/utils.py diff --git a/README.md b/README.md index 85af240..bd948a5 100755 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.cfg b/package.cfg index e4e2a1d..12a410d 100644 --- a/package.cfg +++ b/package.cfg @@ -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/ diff --git a/src/quick_scheme/base_node.py b/src/quick_scheme/base_node.py index 0367d1c..5491e65 100644 --- a/src/quick_scheme/base_node.py +++ b/src/quick_scheme/base_node.py @@ -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): @@ -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 diff --git a/src/quick_scheme/field.py b/src/quick_scheme/field.py index af9eace..5f675a4 100644 --- a/src/quick_scheme/field.py +++ b/src/quick_scheme/field.py @@ -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))) @@ -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 @@ -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 diff --git a/src/quick_scheme/nodes/__init__.py b/src/quick_scheme/nodes/__init__.py index 2efbd42..91d8aba 100644 --- a/src/quick_scheme/nodes/__init__.py +++ b/src/quick_scheme/nodes/__init__.py @@ -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 diff --git a/src/quick_scheme/nodes/key_based_list.py b/src/quick_scheme/nodes/key_based_list.py index 41ac09f..92e5ceb 100644 --- a/src/quick_scheme/nodes/key_based_list.py +++ b/src/quick_scheme/nodes/key_based_list.py @@ -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): diff --git a/src/quick_scheme/nodes/key_based_list_test.py b/src/quick_scheme/nodes/key_based_list_test.py index c8c28c3..a2ab369 100644 --- a/src/quick_scheme/nodes/key_based_list_test.py +++ b/src/quick_scheme/nodes/key_based_list_test.py @@ -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): diff --git a/src/quick_scheme/nodes/node.py b/src/quick_scheme/nodes/scheme_node.py similarity index 76% rename from src/quick_scheme/nodes/node.py rename to src/quick_scheme/nodes/scheme_node.py index 6dddc08..e662cf1 100644 --- a/src/quick_scheme/nodes/node.py +++ b/src/quick_scheme/nodes/scheme_node.py @@ -2,6 +2,7 @@ Base Node Types ''' +from email.policy import default import logging from quick_scheme.field import FieldValue @@ -9,6 +10,7 @@ from ..base_node import SchemeBaseNode from ..exceptions import QuickSchemeValidationException from ..qs_yaml import UnsortableOrderedDict +from ..utils import Args LOG = logging.getLogger(__name__) @@ -16,7 +18,6 @@ class SchemeNode(SchemeBaseNode): ''' Basic Node - a dictionary with defined Fields ''' - # List of fields FIELDS = [] # Allow undefined fields. if false, throw exception on undefined field @@ -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): diff --git a/src/quick_scheme/nodes/node_test.py b/src/quick_scheme/nodes/scheme_node_test.py similarity index 70% rename from src/quick_scheme/nodes/node_test.py rename to src/quick_scheme/nodes/scheme_node_test.py index 8563088..5787307 100644 --- a/src/quick_scheme/nodes/node_test.py +++ b/src/quick_scheme/nodes/scheme_node_test.py @@ -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): @@ -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''' @@ -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''' diff --git a/src/quick_scheme/proxy_access_test.py b/src/quick_scheme/proxy_access_test.py index 504f98d..4d49ac1 100644 --- a/src/quick_scheme/proxy_access_test.py +++ b/src/quick_scheme/proxy_access_test.py @@ -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): diff --git a/src/quick_scheme/utils.py b/src/quick_scheme/utils.py new file mode 100644 index 0000000..d822e0b --- /dev/null +++ b/src/quick_scheme/utils.py @@ -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