Skip to content

Commit

Permalink
Merge branch 'feat/helpful-errors' into 'master'
Browse files Browse the repository at this point in the history
feat: helpful errors

See merge request utilities/ntc!13
  • Loading branch information
Artem Vasenin committed Oct 12, 2020
2 parents b7c17d5 + 8910b13 commit 30a1e7c
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 22 deletions.
2 changes: 1 addition & 1 deletion ntc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
from .leaf import *
from .node import *

__version__ = "0.5.1"
__version__ = "0.6.1"
27 changes: 22 additions & 5 deletions ntc/leaf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class CfgLeaf:
def __init__(self, first: Any = None, second: Any = None, *, required=False, subclass=False):
def __init__(self, first: Any = None, second: Any = None, *, required=False, subclass=False, full_key: str = None):
if first is None and second is None:
raise SchemaError("Must provide either type or default value for config leaf")
if second is None:
Expand All @@ -28,6 +28,7 @@ def __init__(self, first: Any = None, second: Any = None, *, required=False, sub
self._subclass = subclass

self._value = value
self._full_key = full_key

def __str__(self):
return f"CfgLeaf({self.value})"
Expand All @@ -51,16 +52,32 @@ def value(self) -> Any:
@value.setter
def value(self, val) -> None:
if self._required and val is None:
raise MissingRequired("Can't set required value to None")
raise MissingRequired(f"Can't set required value to None for key {self.full_key}")
if val is not None:
if self._subclass and not isinstance(val, type):
raise TypeMismatch(f"Subclass of type {self._type} expected, but {val} found!")
raise TypeMismatch(
f"Subclass of type {self._type} expected, but {val!r} found for key {self.full_key}!"
)
if self._subclass and not issubclass(val, self._type):
raise TypeMismatch(f"Subclass of type {self._type} expected, but {val} found!")
raise TypeMismatch(
f"Subclass of type {self._type} expected, but {val!r} found for key {self.full_key}!"
)
if not self._subclass and not isinstance(val, self._type):
raise TypeMismatch(f"Instance of type {self._type} expected, but {val} found!")
raise TypeMismatch(
f"Instance of type {self._type} expected, but {val!r} found for key {self.full_key}!"
)
self._value = val

@property
def full_key(self):
return self._full_key

@full_key.setter
def full_key(self, value: str):
if self._full_key and value != self._full_key:
raise ValueError(f"full_key cannot be reassigned for node at {self._full_key}")
self._full_key = value

def clone(self) -> CfgLeaf:
return CfgLeaf(deepcopy(self._value), self._type, required=self._required, subclass=self._subclass)

Expand Down
64 changes: 48 additions & 16 deletions ntc/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@ class CfgNode(UserDict):
"_transforms",
"_validators",
"_hooks",
"_full_key",
"full_key",
)
RESERVED_KEYS = (*_BUILT_IN_ATTRS, "data")

def __init__(
self, first: Any = None, *, schema_frozen: bool = False, frozen: bool = False, new_allowed: bool = False,
self,
first: Any = None,
*,
schema_frozen: bool = False,
frozen: bool = False,
new_allowed: bool = False,
full_key: str = None,
):
super().__init__()
if isinstance(first, (dict, CfgNode)):
Expand All @@ -37,6 +45,8 @@ def __init__(
if leaf_spec is not None and not isinstance(leaf_spec, CfgLeaf):
leaf_spec = CfgLeaf(None, leaf_spec)

self._full_key = full_key

self._schema_frozen = schema_frozen
self._frozen = frozen
self._new_allowed = new_allowed
Expand Down Expand Up @@ -109,13 +119,12 @@ def load(cfg_path: Union[Path, str]) -> CfgNode:
@staticmethod
def validate_required(cfg: CfgNode) -> None:
if cfg.leaf_spec is not None and cfg.leaf_spec.required and len(cfg) == 0:
raise MissingRequired(f"Missing required members for {cfg.leaf_spec}")
raise MissingRequired(f"Missing required members for {cfg.leaf_spec} at key {cfg.full_key}")
for key, attr in cfg.attrs:
if isinstance(attr, CfgNode):
CfgNode.validate_required(attr)
continue
if attr.required and attr.value is None:
raise MissingRequired(f"Key {key} is required, but was not provided.")
elif attr.required and attr.value is None:
raise MissingRequired(f"Key {attr.full_key} is required, but was not provided.")

def save(self, path: Path) -> None:
if self._was_unfrozen:
Expand Down Expand Up @@ -224,6 +233,16 @@ def frozen(self) -> bool:
def leaf_spec(self) -> CfgLeaf:
return self._leaf_spec

@property
def full_key(self):
return self._full_key or "cfg"

@full_key.setter
def full_key(self, value: str):
if self._full_key and value != self._full_key:
raise ValueError(f"full_key cannot be reassigned for node assigned to {self._full_key}")
self._full_key = value

def set_module(self, module) -> None:
self._module = module

Expand All @@ -232,15 +251,18 @@ def _set_attrs(self, attrs: List[Tuple[str, Union[CfgNode, CfgLeaf]]]) -> None:
setattr(self, key, attr.clone())

def _set_new(self, key: str, value: Any) -> None:
child_full_key = self._build_child_key(key)
leaf_spec = self.leaf_spec
if isinstance(value, CfgNode):
if leaf_spec:
raise SchemaError(f"Key {key} cannot contain nested nodes as leaf spec is defined for it.")
raise SchemaError(f"Key {child_full_key} cannot contain nested nodes as leaf spec is defined for it.")
if self._schema_frozen and not self._new_allowed:
raise SchemaFrozenError(f"Trying to add node {key}, but schema is frozen.")
raise SchemaFrozenError(f"Trying to add node {child_full_key}, but schema is frozen.")
value.full_key = child_full_key
value_to_set = value
elif isinstance(value, CfgLeaf):
value_to_set = value
value_to_set.full_key = child_full_key
elif isinstance(value, type):
if leaf_spec:
required = leaf_spec.required
Expand All @@ -251,43 +273,50 @@ def _set_new(self, key: str, value: Any) -> None:
value, # Need to pass value here instead of copying from spec, in case new value is more restrictive
subclass=True,
required=required,
full_key=child_full_key,
)
elif leaf_spec:
value_to_set = leaf_spec.clone()
value_to_set.value = value
value_to_set.full_key = child_full_key
else:
value_to_set = CfgLeaf(value, type(value), required=True)
value_to_set = CfgLeaf(value, type(value), required=True, full_key=child_full_key)

if isinstance(value_to_set, CfgLeaf):
if leaf_spec:
if leaf_spec.required and not value_to_set.required:
raise SchemaError(f"Leaf at {key} must have required == True")
raise SchemaError(f"Leaf at {child_full_key} must have required == True")
if leaf_spec.subclass != value_to_set.subclass:
raise SchemaError(f"Leaf at {key} must have subclass == True")
raise SchemaError(f"Leaf at {child_full_key} must have subclass == True")
if not issubclass(value_to_set.type, leaf_spec.type):
raise SchemaError(f"Required type for leaf at {key} must be subclass of {leaf_spec.type}")
raise SchemaError(
f"Required type for leaf at {child_full_key} must be subclass of {leaf_spec.type}"
)
if not leaf_spec.required and value_to_set.value is None:
pass
elif leaf_spec.subclass and not isinstance(value_to_set.value, type):
raise SchemaError(f"Leaf value at {key} must be a type")
raise SchemaError(f"Leaf value at {child_full_key} must be a type")
elif not leaf_spec.subclass and not isinstance(value_to_set.value, leaf_spec.type):
raise SchemaError(f"Value at {key} must be instance of {leaf_spec.type}")
raise SchemaError(f"Value at {child_full_key} must be instance of {leaf_spec.type}")
elif self._schema_frozen and not self._new_allowed:
raise SchemaFrozenError(f"Trying to add leaf {key} to node with frozen schema, but without leaf spec.")
raise SchemaFrozenError(
f"Trying to add leaf {child_full_key} to node with frozen schema, but without leaf spec."
)
super().__setitem__(key, value_to_set)

def _set_existing(self, key: str, value: Any) -> None:
cur_attr = super().__getitem__(key)
type(value)
if isinstance(cur_attr, CfgNode):
raise NodeReassignment(f"Nested CfgNode {key} cannot be reassigned.")
raise NodeReassignment(f"Nested CfgNode {self._build_child_key(key)} cannot be reassigned.")
elif isinstance(cur_attr, CfgLeaf):
cur_attr.value = value
else:
raise AssertionError("This should not happen!")

def _init_with_base(self, base: Union[dict, CfgNode]) -> None:
if isinstance(base, CfgNode):
self.full_key = base.full_key
self._transforms = base._transforms + self._transforms
self._validators = base._validators + self._validators
self._hooks = base._hooks + self._hooks
Expand All @@ -296,11 +325,14 @@ def _init_with_base(self, base: Union[dict, CfgNode]) -> None:
elif isinstance(base, dict):
for key, value in base.items():
if isinstance(value, dict):
value = CfgNode(value)
value = CfgNode(value, full_key=self._build_child_key(key))
self[key] = value
else:
raise ValueError("This should not happen!")

def _build_child_key(self, key: str) -> str:
return f"{self.full_key}.{key}"


CN = CfgNode

Expand Down

0 comments on commit 30a1e7c

Please sign in to comment.