From 78479b0f529429c2ede391a5802d70b47efdae14 Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Sat, 1 Jun 2019 01:48:20 -0400 Subject: [PATCH 1/7] Add a couple of comments to _types --- pydiggy/_types.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pydiggy/_types.py b/pydiggy/_types.py index 35ca921..a74a8d6 100644 --- a/pydiggy/_types.py +++ b/pydiggy/_types.py @@ -26,6 +26,10 @@ class Tokenizer(DirectiveArgument): class Directive: + """ + A directive adds extra instructions to a schema or query. Annotated + with the '@' symbol and optional arguments in parens. + """ def __str__(self): args = [] if "__annotations__" in self.__class__.__dict__: @@ -78,7 +82,7 @@ def __init__(self, name=None, many=False, with_facets=False): upsert = type("upsert", (Directive,), {}) lang = type("lang", (Directive,), {}) -DGRAPH_TYPES = { # Unsupported dgraph type: password, geo +DGRAPH_TYPES = { # TODO: add dgraph type 'password' "uid": "uid", "geo": "geo", "str": "string", From 33eb3e33e3b559ec4b84b959c7e1acc8c2d75b38 Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Sat, 1 Jun 2019 02:10:14 -0400 Subject: [PATCH 2/7] Escape quotes in strings --- pydiggy/node.py | 2 ++ pydiggy/operations.py | 12 +++++++----- pydiggy/utils.py | 6 ++++++ tests/test_mutation.py | 18 ++++++++++++++++++ tests/test_node.py | 18 ++++++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/pydiggy/node.py b/pydiggy/node.py index 5458621..1de20de 100644 --- a/pydiggy/node.py +++ b/pydiggy/node.py @@ -693,6 +693,7 @@ def save( client = get_client(host=host, port=9080) def _make_obj(node, pred, obj): + #TODO: Remove this in favor of the _make_obj in operations annotation = annotations.get(pred, "") if ( hasattr(annotation, "__origin__") @@ -702,6 +703,7 @@ def _make_obj(node, pred, obj): try: if annotation == str: + obj = re.sub('"','\\"', obj.rstrip()) obj = f'"{obj}"' elif annotation == bool: obj = f'"{str(obj).lower()}"' diff --git a/pydiggy/operations.py b/pydiggy/operations.py index bc33e10..bd721ee 100644 --- a/pydiggy/operations.py +++ b/pydiggy/operations.py @@ -1,4 +1,5 @@ import json as _json +import re from datetime import datetime from enum import Enum from typing import List, Tuple, Union, get_type_hints, Dict, Any @@ -11,11 +12,11 @@ def _make_obj(node, pred, obj): - localns = {x.__name__: x for x in Node._nodes} - localns.update({"List": List, "Union": Union, "Tuple": Tuple}) - annotations = get_type_hints(node, globalns=globals(), localns=localns) - annotation = annotations.get(pred, "") - if hasattr(annotation, "__origin__") and annotation.__origin__ == list: + annotation = node._annotations.get(pred, "") + if ( + hasattr(annotation, "__origin__") + and annotation.__origin__ == list + ): annotation = annotation.__args__[0] if issubclass(obj.__class__, Enum): @@ -51,6 +52,7 @@ def _make_obj(node, pred, obj): elif isinstance(obj, datetime): obj = f'"{obj.isoformat()}"' else: + obj = re.sub('"','\\"', obj.rstrip()) obj = f'"{obj}"' except ValueError: raise ValueError( diff --git a/pydiggy/utils.py b/pydiggy/utils.py index 38f5f32..f6e2b71 100644 --- a/pydiggy/utils.py +++ b/pydiggy/utils.py @@ -1,3 +1,5 @@ +import re + def _parse_subject(uid): if isinstance(uid, int): return f"<{hex(uid)}>", uid @@ -6,7 +8,11 @@ def _parse_subject(uid): def _rdf_value(value): + """ + Translates a value into a string annotated with an RDF type + """ if isinstance(value, str): + value = re.sub('"','\\"', value.rstrip()) value = f'"{value}"' elif isinstance(value, bool): value = f'"{str(value).lower()}"' diff --git a/tests/test_mutation.py b/tests/test_mutation.py index 78f7183..a6bcb93 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -54,3 +54,21 @@ def test_mutations(RegionClass): pprint.pprint(mutation) assert control == mutation + + +def test__mutation__with__quotes(RegionClass): + Region = RegionClass + + Region._reset() + + florida = Region(name="Florida \'The \"Sunshine\" State\'") + + florida.stage() + + mutation = generate_mutation() + + control = """_:unsaved.0 "true" . +_:unsaved.0 <_type> "Region" . +_:unsaved.0 "Florida 'The \\"Sunshine\\" State'" .""" + + assert mutation == control \ No newline at end of file diff --git a/tests/test_node.py b/tests/test_node.py index 0f244c5..af6bcb2 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -48,3 +48,21 @@ def test__node__to__json(RegionClass): ] assert regions == control + + +def test__node__with__quotes(RegionClass): + Region = RegionClass + + Region._reset() + + florida = Region(name="Florida \'The \"Sunshine\" State\'") + + regions = Node.json().get("Region") + + control = [ + {'_type': 'Region', + 'name': 'Florida \'The "Sunshine" State\'', + 'uid': 'unsaved.0'} + ] + + assert regions == control \ No newline at end of file From eea49f63b434b3c4607f0ecdfc61f935d1ee8ace Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Sat, 8 Jun 2019 00:03:23 -0400 Subject: [PATCH 3/7] Move Node classmethods to node_type_registry. --- pydiggy/__init__.py | 6 +- pydiggy/cli.py | 7 +- pydiggy/node.py | 369 +++------------------------------- pydiggy/node_type_registry.py | 364 +++++++++++++++++++++++++++++++++ pydiggy/operations.py | 9 +- tests/test_mutation.py | 9 +- tests/test_node.py | 10 +- tests/test_operations.py | 4 +- tests/test_pydiggy.py | 4 +- 9 files changed, 421 insertions(+), 361 deletions(-) create mode 100644 pydiggy/node_type_registry.py diff --git a/pydiggy/__init__.py b/pydiggy/__init__.py index 0b7ba1a..22666c4 100644 --- a/pydiggy/__init__.py +++ b/pydiggy/__init__.py @@ -6,7 +6,8 @@ __email__ = "admhpkns@gmail.com" __version__ = "0.1.0" -from pydiggy.node import Facets, Node, get_node, is_facets +from pydiggy.node import Facets, Node, is_facets +from pydiggy.node_type_registry import get_node_type, NodeTypeRegistry from pydiggy.operations import generate_mutation, hydrate, query, run_mutation from pydiggy._types import count, exact, geo, index, lang, reverse, uid, upsert @@ -17,12 +18,13 @@ "Facets", "generate_mutation", "geo", - "get_node", + "get_node_type", "hydrate", "is_facets", "index", "lang", "Node", + "NodeTypeRegistry", "query", "reverse", "run_mutation", diff --git a/pydiggy/cli.py b/pydiggy/cli.py index 57819f8..4268d07 100644 --- a/pydiggy/cli.py +++ b/pydiggy/cli.py @@ -8,6 +8,7 @@ from pydiggy.connection import get_client from pydiggy.node import Node +from pydiggy.node_type_registry import NodeTypeRegistry @click.group() @@ -40,12 +41,12 @@ def generate(module, run, host, port): click.echo(f"Generating schema for: {module}") importlib.import_module(module) - num_nodes = len(Node._nodes) + num_nodes = len(NodeTypeRegistry._node_types) click.echo(f"\nNodes found: ({num_nodes})") - for node in Node._nodes: + for node in NodeTypeRegistry._node_types: click.echo(f" - {node._get_name()}") - schema, unknown = Node._generate_schema() + schema, unknown = NodeTypeRegistry._generate_schema() if not run: click.echo("\nYour schema:\n~~~~~~~~\n") diff --git a/pydiggy/node.py b/pydiggy/node.py index 1de20de..96129f7 100644 --- a/pydiggy/node.py +++ b/pydiggy/node.py @@ -1,8 +1,8 @@ from __future__ import annotations import copy -import inspect import json +import inspect from collections import namedtuple from copy import deepcopy from dataclasses import dataclass, field @@ -89,19 +89,6 @@ def _force_instance( return directive(*args) -def get_node(name: str) -> Node: - """ - Retrieve a registered node class. - - Example: Region = get_node("Region") - - This is a safe method to make sure that any models used have been - declared as a Node. - """ - registered = {x.__name__: x for x in Node._nodes} - return registered.get(name, None) - - class NodeMeta(type): def __new__(cls, name, bases, attrs, **kwargs): directives = [ @@ -136,24 +123,42 @@ def __new__(cls, name, bases, attrs, **kwargs): class Node(metaclass=NodeMeta): - uid: int + _instances = dict() + _staged = dict() + + + @classmethod + def _get_staged(cls): + return cls._staged + + @classmethod + def _clear_staged(cls): + cls._staged = {} + # NodeTypeRegistry._i needs to be updated when clearing staged nodes - _i = _count() - _nodes = [] - _staged = {} + @classmethod + def _reset(cls) -> None: + cls._instances = dict() + + @classmethod + def _get_name(cls) -> str: + return cls.__name__ def __init_subclass__(cls, is_abstract: bool = False) -> None: if not is_abstract: - cls._register_node(cls) + from pydiggy.node_type_registry import NodeTypeRegistry + NodeTypeRegistry._register_node_type(cls) def __init__(self, uid=None, **kwargs): + + from pydiggy.node_type_registry import NodeTypeRegistry if uid is None: # TODO: # - There probably should be another property that is set here # so that it is possible to identify with a boolean if the instance # is brand new (and has never been committed to the DB) or if it # is being freshly generated - uid = next(self._generate_uid()) + uid = next(NodeTypeRegistry._generate_uid()) self.uid = uid self._dirty = set() @@ -162,7 +167,7 @@ def __init__(self, uid=None, **kwargs): # - perhaps this code to generate self._annotations belongs somewhere # else. Regardless, there is a lot of cleanup in this module that # could probably use this property instead of running get_type_hints - localns = {x.__name__: x for x in Node._nodes} + localns = {x.__name__: x for x in NodeTypeRegistry._node_types} localns.update({"List": List, "Union": Union, "Tuple": Tuple}) self._annotations = get_type_hints( self.__class__, globalns=globals(), localns=localns @@ -196,7 +201,8 @@ def __hash__(self): return hash(self.uid) def __getattr__(self, attribute): - if attribute in self.__annotations__: + if (not attribute == "__annotations__")\ + and (attribute in self.__annotations__): raise MissingAttribute(self, attribute) super().__getattribute__(attribute) @@ -211,7 +217,10 @@ def __setattr__(self, name: str, value: Any): orig = self.__dict__.get(name, None) self.__dict__[name] = value - if hasattr(self, "_init") and self._init and not name.startswith("_"): + # TODO: This causes issues with Node types that want private variables + if hasattr(self, "_init")\ + and self._init\ + and not name.startswith("_"): self._dirty.add(name) if name in self._directives and any( isinstance(d, reverse) for d in self._directives[name] @@ -258,301 +267,6 @@ def _assign(obj, key, value, do_many, remove=False): orig, reverse_name, self, directive.many, remove=True ) - @classmethod - def _reset(cls) -> None: - cls._i = _count() - cls._instances = dict() - - @classmethod - def _register_node(cls, node: Node) -> None: - cls._nodes.append(node) - - @classmethod - def _get_name(cls) -> str: - return cls.__name__ - - @classmethod - def _generate_schema(cls) -> str: - nodes = cls._nodes - edges = {} - schema = [] - type_schema = [] - edge_schema = [] - unknown_schema = [] - - type_schema.append(f"_type: string .") - - # TODO: - # - Prime candidate for some refactoring to reduce complexity - for node in nodes: - name = node._get_name() - type_schema.append(f"{name}: bool @index(bool) .") - - annotations = get_type_hints(node) - for prop_name, prop_type in annotations.items(): - # TODO: - # - Probably need to be a little more careful for the origins - # - Currently, it is assuming Union[something, None], - # but if you had two legit items inside Union (or anything else) - # then the second would be ignored. Which, is probably correct - # unless Dgraph schema would not allow multiple types for a single - # predicate. If it is possible, then the solution may simply - # be to loop over a list of deepcopy(annotations.items()), - # and append all the __args__ to that list to extend the iteration - # - is_list_type should probably become its own function - is_list_type = ( - True - if isinstance(prop_type, _GenericAlias) - and prop_type.__origin__ in (list, tuple) - else False - ) - - if ( - isinstance(prop_type, _GenericAlias) - and prop_type.__origin__ in ACCEPTABLE_GENERIC_ALIASES - ): - prop_type = prop_type.__args__[0] - - prop_type = PropType( - prop_type, - is_list_type, - node._directives.get(prop_name, []), - ) - - if prop_name in edges: - if prop_type != edges.get( - prop_name - ) and not cls._is_node_type(prop_type[0]): - - # Check if there is a type conflict - if ( - edges.get(prop_name).directives - != prop_type.directives - and all( - ( - inspect.isclass(x) - and issubclass(x, Directive) - ) - or issubclass(x.__class__, Directive) - for x in edges.get(prop_name).directives - ) - and all( - ( - inspect.isclass(x) - and issubclass(x, Directive) - ) - or issubclass(x.__class__, Directive) - for x in prop_type.directives - ) - ): - pass - else: - raise ConflictingType( - prop_name, prop_type, edges.get(prop_name) - ) - - # Set the type for translating the value - if prop_type[0] in ACCEPTABLE_TRANSLATIONS: - edges[prop_name] = prop_type - elif cls._is_node_type(prop_type[0]): - edges[prop_name] = PropType( - "uid", - is_list_type, - node._directives.get(prop_name, []), - ) - else: - if prop_name != "uid": - origin = getattr(prop_type[0], "__origin__", None) - # if origin and origin - unknown_schema.append( - f"{prop_name}: {prop_type[0]} || {origin}" - ) - - # TODO: - # - When the v1.1 comes out, will need to address this: - # - # for edge_name, (edge_type, is_list_type) in edges.items(): - # type_name = cls._get_type_name(edge_type) - # # Currently, Dgraph does not support [uid] schema. - # # See https://github.com/dgraph-io/dgraph/issues/2511 - # if is_list_type and type_name != 'uid': - for edge_name, edge in edges.items(): - type_name = cls._get_type_name(edge.prop_type) - if edge.is_list_type and type_name != "uid": - type_name = f"[{type_name}]" - - directives = edge.directives - directives = " ".join([str(d) for d in directives] + [""]) - - edge_schema.append(f"{edge_name}: {type_name} {directives}.") - - type_schema.sort() - edge_schema.sort() - - schema = type_schema + edge_schema - - return "\n".join(schema), "\n".join(unknown_schema) - - @classmethod - def _get_type_name(cls, schema_type): - if isinstance(schema_type, str): - return schema_type - - name = schema_type.__name__ - if cls._is_node_type(schema_type): - return name - else: - if name not in DGRAPH_TYPES: - raise Exception(f"Could not find type: {name}") - return DGRAPH_TYPES.get(name) - - @classmethod - def _get_staged(cls): - return cls._staged - - @classmethod - def _clear_staged(cls): - cls._staged = {} - cls._i = _count() - - @classmethod - def _hydrate( - cls, raw: Dict[str, Any], types: Dict[str, Node] = None - ) -> Node: - # TODO: - # - Accept types that are passed. Loop thru them and register if needed - # and raising an exception if they are not valid. - # - This method is another candidate for some refactoring to reduce - # complexity. - # - Should create a Facets type so that the type annotation of this function - # is _hydrate(cls, raw: str, types: Dict[str, Node] = None) -> Union[Node, Facets] - registered = {x.__name__: x for x in Node._nodes} - localns = {x.__name__: x for x in Node._nodes} - localns.update({"List": List, "Union": Union, "Tuple": Tuple}) - - if "_type" in raw and raw.get("_type") in registered: - if "uid" not in raw: - raise InvalidData("Missing uid.") - - k = registered[raw.get("_type")] - - keys = deepcopy(list(raw.keys())) - facet_data = [ - (key.split("|")[1], raw.pop(key)) for key in keys if "|" in key - ] - - kwargs = {"uid": int(raw.pop("uid"), 16)} - delay = [] - computed = {} - - pred_items = [ - (pred, value) - for pred, value in raw.items() - if not pred.startswith("_") - ] - - annotations = get_type_hints( - k, globalns=globals(), localns=localns - ) - for pred, value in pred_items: - """ - The pred falls into one of three categories: - 1. predicates that have already been defined - 2. predicates that are a reverse of a relationship - 3. predicates that are used for some computed value - """ - if pred in annotations: - if isinstance(value, list): - prop_type = annotations[pred] - is_list_type = ( - True - if isinstance(prop_type, _GenericAlias) - and prop_type.__origin__ in (list, tuple) - else False - ) - if is_list_type: - value = [cls._hydrate(x) for x in value] - else: - if len(value) > 1: - # This should NOT happen. Because uid - # in dgraph is not forced 1:1 with a uid predicate - # it should return as a [] with only - # a single item in it. If the developer wants - # multiple: then the Node definition should - # be List[MyNode] - # Will probably need to be revisited when - # Dgraph v. 1.1 is released - raise Exception("Unknown data") - node = get_node(value[0].get("_type")) - value = node._hydrate(value[0]) - elif isinstance(value, dict): - value = cls._hydrate(value) - - if value is not None: - kwargs.update({pred: value}) - elif pred.startswith("~"): - p = pred[1:] - if isinstance(value, list): - for x in value: - keys = deepcopy(list(x.keys())) - value_facet_data = [ - (k.split("|")[1], x.pop(k)) - for k in keys - if "|" in k - ] - item = get_node(x.get("_type"))._hydrate(x) - - if value_facet_data: - item = Facets(item, **dict(value_facet_data)) - delay.append((item, p, value_facet_data)) - elif isinstance(value, dict): - delay.append( - ( - get_node(value.get("_type"))._hydrate(value), - p, - None, - ) - ) - else: - if pred.endswith("_uid"): - value = int(value, 16) - computed.update({pred: value}) - - instance = k(**kwargs) - for d, p, v in delay: - if is_facets(d): - f = Facets(instance, **dict(v)) - setattr(d.obj, p, f) - else: - setattr(d, p, instance) - - if computed: - instance.computed = Computed(**computed) - - if facet_data: - facets = Facets(instance, **dict(facet_data)) - return facets - else: - return instance - return None - - @classmethod - def json(cls) -> Dict[str, List[Node]]: - """ - Return mapping of Node names to a list of node instances. - - Can be used as a way to dump what node instances are in memory. - """ - # TODO: - # - Probably should be renamed - # - Instrad of being a List[Node], it should probably be a Set - return { - x.__name__: list( - map(partial(cls._explode, max_depth=0), x._instances.values()) - ) - for x in cls._nodes - if len(x._instances) > 0 - } @classmethod def _explode( @@ -642,25 +356,6 @@ def _explode( obj[key] = value._asdict() return obj - @classmethod - def create(cls, **kwargs) -> Node: - """ - Constructor for creating a node. - """ - instance = cls() - for k, v in kwargs.items(): - setattr(instance, k, v) - return instance - - @staticmethod - def _is_node_type(cls) -> bool: - """Check if a class is a """ - return inspect.isclass(cls) and issubclass(cls, Node) - - def _generate_uid(self) -> str: - i = next(self._i) - yield f"unsaved.{i}" - def to_json(self, include: List[str] = None) -> Dict[str, Any]: # TODO: # - Should this be renamed? It is a little misleading. Perhaps to_dict() diff --git a/pydiggy/node_type_registry.py b/pydiggy/node_type_registry.py new file mode 100644 index 0000000..5ae954c --- /dev/null +++ b/pydiggy/node_type_registry.py @@ -0,0 +1,364 @@ +import inspect + +from collections import ChainMap +from copy import deepcopy +from functools import partial +from itertools import count as _count +from typing import ( + Any, + Dict, + List, + Optional, + Tuple, + Union, + _GenericAlias, + get_type_hints, +) + +from pydiggy.node import Node, Facets, is_facets, PropType +from pydiggy._types import ( + ACCEPTABLE_GENERIC_ALIASES, + ACCEPTABLE_TRANSLATIONS, + DGRAPH_TYPES, + SELF_INSERTING_DIRECTIVE_ARGS, + Directive, + geo, + reverse, + uid, + reverse, + count, + upsert, + lang, +) + +def get_node_type(name: str) -> Node: + """ + Retrieve a registered node class. + + Example: Region = get_node("Region") + + This is a safe method to make sure that any models used have been + declared as a Node. + """ + registered = {x.__name__: x for x in NodeTypeRegistry._node_types} + return registered.get(name, None) + +class NodeTypeRegistry: + _i = _count() + _node_types = [] + + @classmethod + def _generate_uid(cls) -> str: + i = next(cls._i) + yield f"unsaved.{i}" + + @classmethod + def _reset(cls) -> None: + cls._i = _count() + for node_type in cls._node_types: + node_type._reset() + + @classmethod + def _register_node_type(cls, node_type: type) -> None: + cls._node_types.append(node_type) + + @classmethod + def _generate_schema(cls) -> str: + nodes = cls._node_types + edges = {} + schema = [] + type_schema = [] + edge_schema = [] + unknown_schema = [] + + type_schema.append(f"_type: string .") + + # TODO: + # - Prime candidate for some refactoring to reduce complexity + for node in nodes: + name = node._get_name() + type_schema.append(f"{name}: bool @index(bool) .") + + annotations = get_type_hints(node) + for prop_name, prop_type in annotations.items(): + # TODO: + # - Probably need to be a little more careful for the origins + # - Currently, it is assuming Union[something, None], + # but if you had two legit items inside Union (or anything else) + # then the second would be ignored. Which, is probably correct + # unless Dgraph schema would not allow multiple types for a single + # predicate. If it is possible, then the solution may simply + # be to loop over a list of deepcopy(annotations.items()), + # and append all the __args__ to that list to extend the iteration + # - is_list_type should probably become its own function + is_list_type = ( + True + if isinstance(prop_type, _GenericAlias) + and prop_type.__origin__ in (list, tuple) + else False + ) + + if ( + isinstance(prop_type, _GenericAlias) + and prop_type.__origin__ in ACCEPTABLE_GENERIC_ALIASES + ): + prop_type = prop_type.__args__[0] + + prop_type = PropType( + prop_type, + is_list_type, + node._directives.get(prop_name, []), + ) + + if prop_name in edges: + if prop_type != edges.get( + prop_name + ) and not cls._is_node_type(prop_type[0]): + + # Check if there is a type conflict + if ( + edges.get(prop_name).directives + != prop_type.directives + and all( + ( + inspect.isclass(x) + and issubclass(x, Directive) + ) + or issubclass(x.__class__, Directive) + for x in edges.get(prop_name).directives + ) + and all( + ( + inspect.isclass(x) + and issubclass(x, Directive) + ) + or issubclass(x.__class__, Directive) + for x in prop_type.directives + ) + ): + pass + else: + raise ConflictingType( + prop_name, prop_type, edges.get(prop_name) + ) + + # Set the type for translating the value + if prop_type[0] in ACCEPTABLE_TRANSLATIONS: + edges[prop_name] = prop_type + elif cls._is_node_type(prop_type[0]): + edges[prop_name] = PropType( + "uid", + is_list_type, + node._directives.get(prop_name, []), + ) + else: + if prop_name != "uid": + origin = getattr(prop_type[0], "__origin__", None) + # if origin and origin + unknown_schema.append( + f"{prop_name}: {prop_type[0]} || {origin}" + ) + + # TODO: + # - When the v1.1 comes out, will need to address this: + # + # for edge_name, (edge_type, is_list_type) in edges.items(): + # type_name = cls._get_type_name(edge_type) + # # Currently, Dgraph does not support [uid] schema. + # # See https://github.com/dgraph-io/dgraph/issues/2511 + # if is_list_type and type_name != 'uid': + for edge_name, edge in edges.items(): + type_name = cls._get_type_name(edge.prop_type) + if edge.is_list_type and type_name != "uid": + type_name = f"[{type_name}]" + + directives = edge.directives + directives = " ".join([str(d) for d in directives] + [""]) + + edge_schema.append(f"{edge_name}: {type_name} {directives}.") + + type_schema.sort() + edge_schema.sort() + + schema = type_schema + edge_schema + + return "\n".join(schema), "\n".join(unknown_schema) + + @classmethod + def _get_type_name(cls, schema_type): + if isinstance(schema_type, str): + return schema_type + + name = schema_type.__name__ + if cls._is_node_type(schema_type): + return name + else: + if name not in DGRAPH_TYPES: + raise Exception(f"Could not find type: {name}") + return DGRAPH_TYPES.get(name) + + @classmethod + def _get_staged(cls): + all_staged = [node_type._get_staged() for node_type in cls._node_types] + return dict(ChainMap(*all_staged)) + + @classmethod + def _clear_staged(cls): + for node_type in cls._node_types: + node_type._clear_staged() + cls._i = _count() + + @classmethod + def _hydrate( + cls, raw: Dict[str, Any], types: Dict[str, Node] = None + ) -> Node: # -> Dict[str: List[Node]] + # TODO: + # - Accept types that are passed. Loop thru them and register if needed + # and raising an exception if they are not valid. + # - This method is another candidate for some refactoring to reduce + # complexity. + # - Should create a Facets type so that the type annotation of this function + # is _hydrate(cls, raw: str, types: Dict[str, Node] = None) -> Union[Node, Facets] + registered = {x.__name__: x for x in NodeTypeRegistry._node_types} + localns = {x.__name__: x for x in NodeTypeRegistry._node_types} + localns.update({"List": List, "Union": Union, "Tuple": Tuple}) + + if "_type" in raw and raw.get("_type") in registered: + if "uid" not in raw: + raise InvalidData("Missing uid.") + + k = registered[raw.get("_type")] + + keys = deepcopy(list(raw.keys())) + facet_data = [ + (key.split("|")[1], raw.pop(key)) for key in keys if "|" in key + ] + + kwargs = {"uid": int(raw.pop("uid"), 16)} + delay = [] + computed = {} + + pred_items = [ + (pred, value) + for pred, value in raw.items() + if not pred.startswith("_") + ] + + annotations = get_type_hints( + k, globalns=globals(), localns=localns + ) + for pred, value in pred_items: + """ + The pred falls into one of three categories: + 1. predicates that have already been defined + 2. predicates that are a reverse of a relationship + 3. predicates that are used for some computed value + """ + if pred in annotations: + if isinstance(value, list): + prop_type = annotations[pred] + is_list_type = ( + True + if isinstance(prop_type, _GenericAlias) + and prop_type.__origin__ in (list, tuple) + else False + ) + if is_list_type: + value = [cls._hydrate(x) for x in value] + else: + if len(value) > 1: + # This should NOT happen. Because uid + # in dgraph is not forced 1:1 with a uid predicate + # it should return as a [] with only + # a single item in it. If the developer wants + # multiple: then the Node definition should + # be List[MyNode] + # Will probably need to be revisited when + # Dgraph v. 1.1 is released + raise Exception("Unknown data") + node_type = get_node_type(value[0].get("_type")) + value = node_type._hydrate(value[0]) + elif isinstance(value, dict): + value = cls._hydrate(value) + + if value is not None: + kwargs.update({pred: value}) + elif pred.startswith("~"): + p = pred[1:] + if isinstance(value, list): + for x in value: + keys = deepcopy(list(x.keys())) + value_facet_data = [ + (k.split("|")[1], x.pop(k)) + for k in keys + if "|" in k + ] + item = NodeTypeRegistry._hydrate(x) + + if value_facet_data: + item = Facets(item, **dict(value_facet_data)) + delay.append((item, p, value_facet_data)) + elif isinstance(value, dict): + delay.append( + ( + get_node(value.get("_type"))._hydrate(value), + p, + None, + ) + ) + else: + if pred.endswith("_uid"): + value = int(value, 16) + computed.update({pred: value}) + + instance = k(**kwargs) + for d, p, v in delay: + if is_facets(d): + f = Facets(instance, **dict(v)) + setattr(d.obj, p, f) + else: + setattr(d, p, instance) + + if computed: + instance.computed = Computed(**computed) + + if facet_data: + facets = Facets(instance, **dict(facet_data)) + return facets + else: + return instance + return None + + @classmethod + def json(cls) -> Dict[str, List[Node]]: + """ + Return mapping of Node names to a list of node instances. + + Can be used as a way to dump what node instances are in memory. + """ + # TODO: + # - Probably should be renamed + # - Instead of being a List[Node], it should probably be a Set + return { + x.__name__: list( + map(partial(x._explode, max_depth=0), x._instances.values()) + ) + for x in cls._node_types + if len(x._instances) > 0 + } + + + @classmethod + def create(cls, **kwargs) -> Node: + """ + Constructor for creating a node. + """ + instance = cls() + for k, v in kwargs.items(): + setattr(instance, k, v) + return instance + + @staticmethod + def _is_node_type(cls) -> bool: + """Check if a class is a """ + return inspect.isclass(cls) and issubclass(cls, Node) \ No newline at end of file diff --git a/pydiggy/operations.py b/pydiggy/operations.py index bd721ee..a21fd83 100644 --- a/pydiggy/operations.py +++ b/pydiggy/operations.py @@ -7,6 +7,7 @@ from pydiggy.connection import get_client, PyDiggyClient from pydiggy.exceptions import NotStaged from pydiggy.node import Node +from pydiggy.node_type_registry import NodeTypeRegistry from pydiggy._types import * # noqa from pydiggy.utils import _parse_subject, _raw_value @@ -25,9 +26,9 @@ def _make_obj(node, pred, obj): # TODO: # - integreate utils._rdf_value try: - if Node._is_node_type(obj.__class__): + if NodeTypeRegistry._is_node_type(obj.__class__): uid, passed = _parse_subject(obj.uid) - staged = Node._get_staged() + staged = NodeTypeRegistry._get_staged() if ( uid not in staged @@ -131,14 +132,14 @@ def hydrate(data: str, types: List[Node] = None) -> Dict[str, List[Node]]: output = {} # data = data.get(data_set) - registered = {x.__name__: x for x in Node._nodes} + registered = {x.__name__: x for x in NodeTypeRegistry._node_types} for func_name, raw_data in data.items(): hydrated = [] for raw in raw_data: if "_type" in raw and raw.get("_type") in registered: cls = registered.get(raw.get("_type")) - hydrated.append(cls._hydrate(raw, types=types)) + hydrated.append(NodeTypeRegistry._hydrate(raw, types=types)) output[func_name] = hydrated diff --git a/tests/test_mutation.py b/tests/test_mutation.py index a6bcb93..e46ddf7 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -1,12 +1,9 @@ -from pydiggy import Facets, generate_mutation - -# import pytest - +from pydiggy import Facets, generate_mutation, NodeTypeRegistry def test_mutations(RegionClass): Region = RegionClass - Region._reset() + NodeTypeRegistry._reset() por = Region(uid=0x11, name="Portugal") spa = Region(uid=0x12, name="Spain") @@ -59,7 +56,7 @@ def test_mutations(RegionClass): def test__mutation__with__quotes(RegionClass): Region = RegionClass - Region._reset() + NodeTypeRegistry._reset() florida = Region(name="Florida \'The \"Sunshine\" State\'") diff --git a/tests/test_node.py b/tests/test_node.py index af6bcb2..107e373 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -4,13 +4,13 @@ # import pytest from pprint import pprint as print -from pydiggy import Facets, Node +from pydiggy import Facets, Node, NodeTypeRegistry def test__node__to__json(RegionClass): Region = RegionClass - Region._reset() + NodeTypeRegistry._reset() por = Region(uid=0x11, name="Portugal") spa = Region(uid=0x12, name="Spain") @@ -22,7 +22,7 @@ def test__node__to__json(RegionClass): gas.borders = [Facets(spa, foo="bar", hello="world"), mar] mar.borders = [spa, gas] - regions = Node.json().get("Region") + regions = NodeTypeRegistry.json().get("Region") control = [ {"_type": "Region", "borders": "[]", "name": "Portugal", "uid": 17}, @@ -53,11 +53,11 @@ def test__node__to__json(RegionClass): def test__node__with__quotes(RegionClass): Region = RegionClass - Region._reset() + NodeTypeRegistry._reset() florida = Region(name="Florida \'The \"Sunshine\" State\'") - regions = Node.json().get("Region") + regions = NodeTypeRegistry.json().get("Region") control = [ {'_type': 'Region', diff --git a/tests/test_operations.py b/tests/test_operations.py index e47bbf3..e3a6708 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -1,4 +1,4 @@ -from pydiggy import operations +from pydiggy import operations, NodeTypeRegistry def test__parse_subject(): @@ -13,7 +13,7 @@ def test__parse_subject(): def test__make_obj(TypeTestClass): - TypeTestClass._reset() + NodeTypeRegistry._reset() node = TypeTestClass() node.stage() diff --git a/tests/test_pydiggy.py b/tests/test_pydiggy.py index 0675f83..9f17125 100644 --- a/tests/test_pydiggy.py +++ b/tests/test_pydiggy.py @@ -6,7 +6,7 @@ import pytest from click.testing import CliRunner -from pydiggy import Node, cli +from pydiggy import Node, cli, NodeTypeRegistry @pytest.fixture @@ -24,7 +24,7 @@ def test_command_line_interface_has_commands(runner, commands): def test_dry_run_generate_schema(runner): - Node._nodes = [] + NodeTypeRegistry._node_types = [] result = runner.invoke(cli.main, ["generate", "tests.fakeapp", "--no-run"]) assert result.exit_code == 0 assert "Nodes found: (1)" in result.output From 0a432f83b9711b8030799a1bcb29fc756c28e0ef Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Thu, 13 Jun 2019 10:20:08 -0400 Subject: [PATCH 4/7] Put back the async constructor. --- pydiggy/node.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pydiggy/node.py b/pydiggy/node.py index 96129f7..67d3814 100644 --- a/pydiggy/node.py +++ b/pydiggy/node.py @@ -267,6 +267,15 @@ def _assign(obj, key, value, do_many, remove=False): orig, reverse_name, self, directive.many, remove=True ) + @classmethod + def create(cls, **kwargs) -> Node: + """ + Constructor for creating a node. + """ + instance = cls() + for k, v in kwargs.items(): + setattr(instance, k, v) + return instance @classmethod def _explode( From 33ffc374942fa90eea0fdfeab25538c2c1e46eb8 Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Thu, 13 Jun 2019 10:35:06 -0400 Subject: [PATCH 5/7] Move NodeTypeRegistry to node.py. --- pydiggy/__init__.py | 3 +- pydiggy/cli.py | 3 +- pydiggy/node.py | 329 +++++++++++++++++++++++++++++- pydiggy/node_type_registry.py | 364 ---------------------------------- pydiggy/operations.py | 3 +- 5 files changed, 329 insertions(+), 373 deletions(-) delete mode 100644 pydiggy/node_type_registry.py diff --git a/pydiggy/__init__.py b/pydiggy/__init__.py index 22666c4..ec64d53 100644 --- a/pydiggy/__init__.py +++ b/pydiggy/__init__.py @@ -6,8 +6,7 @@ __email__ = "admhpkns@gmail.com" __version__ = "0.1.0" -from pydiggy.node import Facets, Node, is_facets -from pydiggy.node_type_registry import get_node_type, NodeTypeRegistry +from pydiggy.node import Facets, Node, is_facets, get_node_type, NodeTypeRegistry from pydiggy.operations import generate_mutation, hydrate, query, run_mutation from pydiggy._types import count, exact, geo, index, lang, reverse, uid, upsert diff --git a/pydiggy/cli.py b/pydiggy/cli.py index 4268d07..745344c 100644 --- a/pydiggy/cli.py +++ b/pydiggy/cli.py @@ -7,8 +7,7 @@ from pydgraph import Operation from pydiggy.connection import get_client -from pydiggy.node import Node -from pydiggy.node_type_registry import NodeTypeRegistry +from pydiggy.node import Node, NodeTypeRegistry @click.group() diff --git a/pydiggy/node.py b/pydiggy/node.py index 67d3814..2dc9099 100644 --- a/pydiggy/node.py +++ b/pydiggy/node.py @@ -3,7 +3,7 @@ import copy import json import inspect -from collections import namedtuple +from collections import namedtuple, ChainMap from copy import deepcopy from dataclasses import dataclass, field from datetime import datetime @@ -69,6 +69,19 @@ def is_computed(node: Node) -> bool: return False +def get_node_type(name: str) -> Node: + """ + Retrieve a registered node class. + + Example: Region = get_node("Region") + + This is a safe method to make sure that any models used have been + registered in the NodeTypeRegistry + """ + registered = {x.__name__: x for x in NodeTypeRegistry._node_types} + return registered.get(name, None) + + def _force_instance( directive: Union[Directive, reverse, count, upsert, lang], prop_type: str = None, @@ -146,12 +159,10 @@ def _get_name(cls) -> str: def __init_subclass__(cls, is_abstract: bool = False) -> None: if not is_abstract: - from pydiggy.node_type_registry import NodeTypeRegistry NodeTypeRegistry._register_node_type(cls) def __init__(self, uid=None, **kwargs): - from pydiggy.node_type_registry import NodeTypeRegistry if uid is None: # TODO: # - There probably should be another property that is set here @@ -512,3 +523,315 @@ def _make_obj(node, pred, obj): transaction.commit() finally: transaction.discard() + + + + +class NodeTypeRegistry: + _i = _count() + _node_types = [] + + @classmethod + def _generate_uid(cls) -> str: + i = next(cls._i) + yield f"unsaved.{i}" + + @classmethod + def _reset(cls) -> None: + cls._i = _count() + for node_type in cls._node_types: + node_type._reset() + + @classmethod + def _register_node_type(cls, node_type: type) -> None: + cls._node_types.append(node_type) + + @classmethod + def _generate_schema(cls) -> str: + nodes = cls._node_types + edges = {} + schema = [] + type_schema = [] + edge_schema = [] + unknown_schema = [] + + type_schema.append(f"_type: string .") + + # TODO: + # - Prime candidate for some refactoring to reduce complexity + for node in nodes: + name = node._get_name() + type_schema.append(f"{name}: bool @index(bool) .") + + annotations = get_type_hints(node) + for prop_name, prop_type in annotations.items(): + # TODO: + # - Probably need to be a little more careful for the origins + # - Currently, it is assuming Union[something, None], + # but if you had two legit items inside Union (or anything else) + # then the second would be ignored. Which, is probably correct + # unless Dgraph schema would not allow multiple types for a single + # predicate. If it is possible, then the solution may simply + # be to loop over a list of deepcopy(annotations.items()), + # and append all the __args__ to that list to extend the iteration + # - is_list_type should probably become its own function + is_list_type = ( + True + if isinstance(prop_type, _GenericAlias) + and prop_type.__origin__ in (list, tuple) + else False + ) + + if ( + isinstance(prop_type, _GenericAlias) + and prop_type.__origin__ in ACCEPTABLE_GENERIC_ALIASES + ): + prop_type = prop_type.__args__[0] + + prop_type = PropType( + prop_type, + is_list_type, + node._directives.get(prop_name, []), + ) + + if prop_name in edges: + if prop_type != edges.get( + prop_name + ) and not cls._is_node_type(prop_type[0]): + + # Check if there is a type conflict + if ( + edges.get(prop_name).directives + != prop_type.directives + and all( + ( + inspect.isclass(x) + and issubclass(x, Directive) + ) + or issubclass(x.__class__, Directive) + for x in edges.get(prop_name).directives + ) + and all( + ( + inspect.isclass(x) + and issubclass(x, Directive) + ) + or issubclass(x.__class__, Directive) + for x in prop_type.directives + ) + ): + pass + else: + raise ConflictingType( + prop_name, prop_type, edges.get(prop_name) + ) + + # Set the type for translating the value + if prop_type[0] in ACCEPTABLE_TRANSLATIONS: + edges[prop_name] = prop_type + elif cls._is_node_type(prop_type[0]): + edges[prop_name] = PropType( + "uid", + is_list_type, + node._directives.get(prop_name, []), + ) + else: + if prop_name != "uid": + origin = getattr(prop_type[0], "__origin__", None) + # if origin and origin + unknown_schema.append( + f"{prop_name}: {prop_type[0]} || {origin}" + ) + + # TODO: + # - When the v1.1 comes out, will need to address this: + # + # for edge_name, (edge_type, is_list_type) in edges.items(): + # type_name = cls._get_type_name(edge_type) + # # Currently, Dgraph does not support [uid] schema. + # # See https://github.com/dgraph-io/dgraph/issues/2511 + # if is_list_type and type_name != 'uid': + for edge_name, edge in edges.items(): + type_name = cls._get_type_name(edge.prop_type) + if edge.is_list_type and type_name != "uid": + type_name = f"[{type_name}]" + + directives = edge.directives + directives = " ".join([str(d) for d in directives] + [""]) + + edge_schema.append(f"{edge_name}: {type_name} {directives}.") + + type_schema.sort() + edge_schema.sort() + + schema = type_schema + edge_schema + + return "\n".join(schema), "\n".join(unknown_schema) + + @classmethod + def _get_type_name(cls, schema_type): + if isinstance(schema_type, str): + return schema_type + + name = schema_type.__name__ + if cls._is_node_type(schema_type): + return name + else: + if name not in DGRAPH_TYPES: + raise Exception(f"Could not find type: {name}") + return DGRAPH_TYPES.get(name) + + @classmethod + def _get_staged(cls): + all_staged = [node_type._get_staged() for node_type in cls._node_types] + return dict(ChainMap(*all_staged)) + + @classmethod + def _clear_staged(cls): + for node_type in cls._node_types: + node_type._clear_staged() + cls._i = _count() + + @classmethod + def _hydrate( + cls, raw: Dict[str, Any], types: Dict[str, Node] = None + ) -> Node: # -> Dict[str: List[Node]] + # TODO: + # - Accept types that are passed. Loop thru them and register if needed + # and raising an exception if they are not valid. + # - This method is another candidate for some refactoring to reduce + # complexity. + # - Should create a Facets type so that the type annotation of this function + # is _hydrate(cls, raw: str, types: Dict[str, Node] = None) -> Union[Node, Facets] + registered = {x.__name__: x for x in NodeTypeRegistry._node_types} + localns = {x.__name__: x for x in NodeTypeRegistry._node_types} + localns.update({"List": List, "Union": Union, "Tuple": Tuple}) + + if "_type" in raw and raw.get("_type") in registered: + if "uid" not in raw: + raise InvalidData("Missing uid.") + + k = registered[raw.get("_type")] + + keys = deepcopy(list(raw.keys())) + facet_data = [ + (key.split("|")[1], raw.pop(key)) for key in keys if "|" in key + ] + + kwargs = {"uid": int(raw.pop("uid"), 16)} + delay = [] + computed = {} + + pred_items = [ + (pred, value) + for pred, value in raw.items() + if not pred.startswith("_") + ] + + annotations = get_type_hints( + k, globalns=globals(), localns=localns + ) + for pred, value in pred_items: + """ + The pred falls into one of three categories: + 1. predicates that have already been defined + 2. predicates that are a reverse of a relationship + 3. predicates that are used for some computed value + """ + if pred in annotations: + if isinstance(value, list): + prop_type = annotations[pred] + is_list_type = ( + True + if isinstance(prop_type, _GenericAlias) + and prop_type.__origin__ in (list, tuple) + else False + ) + if is_list_type: + value = [cls._hydrate(x) for x in value] + else: + if len(value) > 1: + # This should NOT happen. Because uid + # in dgraph is not forced 1:1 with a uid predicate + # it should return as a [] with only + # a single item in it. If the developer wants + # multiple: then the Node definition should + # be List[MyNode] + # Will probably need to be revisited when + # Dgraph v. 1.1 is released + raise Exception("Unknown data") + node_type = get_node_type(value[0].get("_type")) + value = node_type._hydrate(value[0]) + elif isinstance(value, dict): + value = cls._hydrate(value) + + if value is not None: + kwargs.update({pred: value}) + elif pred.startswith("~"): + p = pred[1:] + if isinstance(value, list): + for x in value: + keys = deepcopy(list(x.keys())) + value_facet_data = [ + (k.split("|")[1], x.pop(k)) + for k in keys + if "|" in k + ] + item = NodeTypeRegistry._hydrate(x) + + if value_facet_data: + item = Facets(item, **dict(value_facet_data)) + delay.append((item, p, value_facet_data)) + elif isinstance(value, dict): + delay.append( + ( + get_node(value.get("_type"))._hydrate(value), + p, + None, + ) + ) + else: + if pred.endswith("_uid"): + value = int(value, 16) + computed.update({pred: value}) + + instance = k(**kwargs) + for d, p, v in delay: + if is_facets(d): + f = Facets(instance, **dict(v)) + setattr(d.obj, p, f) + else: + setattr(d, p, instance) + + if computed: + instance.computed = Computed(**computed) + + if facet_data: + facets = Facets(instance, **dict(facet_data)) + return facets + else: + return instance + return None + + @classmethod + def json(cls) -> Dict[str, List[Node]]: + """ + Return mapping of Node names to a list of node instances. + + Can be used as a way to dump what node instances are in memory. + """ + # TODO: + # - Probably should be renamed + # - Instead of being a List[Node], it should probably be a Set + return { + x.__name__: list( + map(partial(x._explode, max_depth=0), x._instances.values()) + ) + for x in cls._node_types + if len(x._instances) > 0 + } + + @staticmethod + def _is_node_type(cls) -> bool: + """Check if a class is a """ + return inspect.isclass(cls) and issubclass(cls, Node) diff --git a/pydiggy/node_type_registry.py b/pydiggy/node_type_registry.py deleted file mode 100644 index 5ae954c..0000000 --- a/pydiggy/node_type_registry.py +++ /dev/null @@ -1,364 +0,0 @@ -import inspect - -from collections import ChainMap -from copy import deepcopy -from functools import partial -from itertools import count as _count -from typing import ( - Any, - Dict, - List, - Optional, - Tuple, - Union, - _GenericAlias, - get_type_hints, -) - -from pydiggy.node import Node, Facets, is_facets, PropType -from pydiggy._types import ( - ACCEPTABLE_GENERIC_ALIASES, - ACCEPTABLE_TRANSLATIONS, - DGRAPH_TYPES, - SELF_INSERTING_DIRECTIVE_ARGS, - Directive, - geo, - reverse, - uid, - reverse, - count, - upsert, - lang, -) - -def get_node_type(name: str) -> Node: - """ - Retrieve a registered node class. - - Example: Region = get_node("Region") - - This is a safe method to make sure that any models used have been - declared as a Node. - """ - registered = {x.__name__: x for x in NodeTypeRegistry._node_types} - return registered.get(name, None) - -class NodeTypeRegistry: - _i = _count() - _node_types = [] - - @classmethod - def _generate_uid(cls) -> str: - i = next(cls._i) - yield f"unsaved.{i}" - - @classmethod - def _reset(cls) -> None: - cls._i = _count() - for node_type in cls._node_types: - node_type._reset() - - @classmethod - def _register_node_type(cls, node_type: type) -> None: - cls._node_types.append(node_type) - - @classmethod - def _generate_schema(cls) -> str: - nodes = cls._node_types - edges = {} - schema = [] - type_schema = [] - edge_schema = [] - unknown_schema = [] - - type_schema.append(f"_type: string .") - - # TODO: - # - Prime candidate for some refactoring to reduce complexity - for node in nodes: - name = node._get_name() - type_schema.append(f"{name}: bool @index(bool) .") - - annotations = get_type_hints(node) - for prop_name, prop_type in annotations.items(): - # TODO: - # - Probably need to be a little more careful for the origins - # - Currently, it is assuming Union[something, None], - # but if you had two legit items inside Union (or anything else) - # then the second would be ignored. Which, is probably correct - # unless Dgraph schema would not allow multiple types for a single - # predicate. If it is possible, then the solution may simply - # be to loop over a list of deepcopy(annotations.items()), - # and append all the __args__ to that list to extend the iteration - # - is_list_type should probably become its own function - is_list_type = ( - True - if isinstance(prop_type, _GenericAlias) - and prop_type.__origin__ in (list, tuple) - else False - ) - - if ( - isinstance(prop_type, _GenericAlias) - and prop_type.__origin__ in ACCEPTABLE_GENERIC_ALIASES - ): - prop_type = prop_type.__args__[0] - - prop_type = PropType( - prop_type, - is_list_type, - node._directives.get(prop_name, []), - ) - - if prop_name in edges: - if prop_type != edges.get( - prop_name - ) and not cls._is_node_type(prop_type[0]): - - # Check if there is a type conflict - if ( - edges.get(prop_name).directives - != prop_type.directives - and all( - ( - inspect.isclass(x) - and issubclass(x, Directive) - ) - or issubclass(x.__class__, Directive) - for x in edges.get(prop_name).directives - ) - and all( - ( - inspect.isclass(x) - and issubclass(x, Directive) - ) - or issubclass(x.__class__, Directive) - for x in prop_type.directives - ) - ): - pass - else: - raise ConflictingType( - prop_name, prop_type, edges.get(prop_name) - ) - - # Set the type for translating the value - if prop_type[0] in ACCEPTABLE_TRANSLATIONS: - edges[prop_name] = prop_type - elif cls._is_node_type(prop_type[0]): - edges[prop_name] = PropType( - "uid", - is_list_type, - node._directives.get(prop_name, []), - ) - else: - if prop_name != "uid": - origin = getattr(prop_type[0], "__origin__", None) - # if origin and origin - unknown_schema.append( - f"{prop_name}: {prop_type[0]} || {origin}" - ) - - # TODO: - # - When the v1.1 comes out, will need to address this: - # - # for edge_name, (edge_type, is_list_type) in edges.items(): - # type_name = cls._get_type_name(edge_type) - # # Currently, Dgraph does not support [uid] schema. - # # See https://github.com/dgraph-io/dgraph/issues/2511 - # if is_list_type and type_name != 'uid': - for edge_name, edge in edges.items(): - type_name = cls._get_type_name(edge.prop_type) - if edge.is_list_type and type_name != "uid": - type_name = f"[{type_name}]" - - directives = edge.directives - directives = " ".join([str(d) for d in directives] + [""]) - - edge_schema.append(f"{edge_name}: {type_name} {directives}.") - - type_schema.sort() - edge_schema.sort() - - schema = type_schema + edge_schema - - return "\n".join(schema), "\n".join(unknown_schema) - - @classmethod - def _get_type_name(cls, schema_type): - if isinstance(schema_type, str): - return schema_type - - name = schema_type.__name__ - if cls._is_node_type(schema_type): - return name - else: - if name not in DGRAPH_TYPES: - raise Exception(f"Could not find type: {name}") - return DGRAPH_TYPES.get(name) - - @classmethod - def _get_staged(cls): - all_staged = [node_type._get_staged() for node_type in cls._node_types] - return dict(ChainMap(*all_staged)) - - @classmethod - def _clear_staged(cls): - for node_type in cls._node_types: - node_type._clear_staged() - cls._i = _count() - - @classmethod - def _hydrate( - cls, raw: Dict[str, Any], types: Dict[str, Node] = None - ) -> Node: # -> Dict[str: List[Node]] - # TODO: - # - Accept types that are passed. Loop thru them and register if needed - # and raising an exception if they are not valid. - # - This method is another candidate for some refactoring to reduce - # complexity. - # - Should create a Facets type so that the type annotation of this function - # is _hydrate(cls, raw: str, types: Dict[str, Node] = None) -> Union[Node, Facets] - registered = {x.__name__: x for x in NodeTypeRegistry._node_types} - localns = {x.__name__: x for x in NodeTypeRegistry._node_types} - localns.update({"List": List, "Union": Union, "Tuple": Tuple}) - - if "_type" in raw and raw.get("_type") in registered: - if "uid" not in raw: - raise InvalidData("Missing uid.") - - k = registered[raw.get("_type")] - - keys = deepcopy(list(raw.keys())) - facet_data = [ - (key.split("|")[1], raw.pop(key)) for key in keys if "|" in key - ] - - kwargs = {"uid": int(raw.pop("uid"), 16)} - delay = [] - computed = {} - - pred_items = [ - (pred, value) - for pred, value in raw.items() - if not pred.startswith("_") - ] - - annotations = get_type_hints( - k, globalns=globals(), localns=localns - ) - for pred, value in pred_items: - """ - The pred falls into one of three categories: - 1. predicates that have already been defined - 2. predicates that are a reverse of a relationship - 3. predicates that are used for some computed value - """ - if pred in annotations: - if isinstance(value, list): - prop_type = annotations[pred] - is_list_type = ( - True - if isinstance(prop_type, _GenericAlias) - and prop_type.__origin__ in (list, tuple) - else False - ) - if is_list_type: - value = [cls._hydrate(x) for x in value] - else: - if len(value) > 1: - # This should NOT happen. Because uid - # in dgraph is not forced 1:1 with a uid predicate - # it should return as a [] with only - # a single item in it. If the developer wants - # multiple: then the Node definition should - # be List[MyNode] - # Will probably need to be revisited when - # Dgraph v. 1.1 is released - raise Exception("Unknown data") - node_type = get_node_type(value[0].get("_type")) - value = node_type._hydrate(value[0]) - elif isinstance(value, dict): - value = cls._hydrate(value) - - if value is not None: - kwargs.update({pred: value}) - elif pred.startswith("~"): - p = pred[1:] - if isinstance(value, list): - for x in value: - keys = deepcopy(list(x.keys())) - value_facet_data = [ - (k.split("|")[1], x.pop(k)) - for k in keys - if "|" in k - ] - item = NodeTypeRegistry._hydrate(x) - - if value_facet_data: - item = Facets(item, **dict(value_facet_data)) - delay.append((item, p, value_facet_data)) - elif isinstance(value, dict): - delay.append( - ( - get_node(value.get("_type"))._hydrate(value), - p, - None, - ) - ) - else: - if pred.endswith("_uid"): - value = int(value, 16) - computed.update({pred: value}) - - instance = k(**kwargs) - for d, p, v in delay: - if is_facets(d): - f = Facets(instance, **dict(v)) - setattr(d.obj, p, f) - else: - setattr(d, p, instance) - - if computed: - instance.computed = Computed(**computed) - - if facet_data: - facets = Facets(instance, **dict(facet_data)) - return facets - else: - return instance - return None - - @classmethod - def json(cls) -> Dict[str, List[Node]]: - """ - Return mapping of Node names to a list of node instances. - - Can be used as a way to dump what node instances are in memory. - """ - # TODO: - # - Probably should be renamed - # - Instead of being a List[Node], it should probably be a Set - return { - x.__name__: list( - map(partial(x._explode, max_depth=0), x._instances.values()) - ) - for x in cls._node_types - if len(x._instances) > 0 - } - - - @classmethod - def create(cls, **kwargs) -> Node: - """ - Constructor for creating a node. - """ - instance = cls() - for k, v in kwargs.items(): - setattr(instance, k, v) - return instance - - @staticmethod - def _is_node_type(cls) -> bool: - """Check if a class is a """ - return inspect.isclass(cls) and issubclass(cls, Node) \ No newline at end of file diff --git a/pydiggy/operations.py b/pydiggy/operations.py index a21fd83..45cd47a 100644 --- a/pydiggy/operations.py +++ b/pydiggy/operations.py @@ -6,8 +6,7 @@ from pydiggy.connection import get_client, PyDiggyClient from pydiggy.exceptions import NotStaged -from pydiggy.node import Node -from pydiggy.node_type_registry import NodeTypeRegistry +from pydiggy.node import Node, NodeTypeRegistry from pydiggy._types import * # noqa from pydiggy.utils import _parse_subject, _raw_value From 10e9cd1add7b6c32316a9f6584948d4ad965a67e Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Thu, 13 Jun 2019 10:40:27 -0400 Subject: [PATCH 6/7] Format code with black. --- examples/subclass.py | 29 +++-------- pydiggy/_types.py | 1 + pydiggy/node.py | 105 +++++++++++---------------------------- pydiggy/operations.py | 7 +-- pydiggy/utils.py | 3 +- tests/test_mutation.py | 5 +- tests/test_node.py | 12 +++-- tests/test_operations.py | 2 +- 8 files changed, 50 insertions(+), 114 deletions(-) diff --git a/examples/subclass.py b/examples/subclass.py index 730d218..f557a34 100644 --- a/examples/subclass.py +++ b/examples/subclass.py @@ -111,9 +111,7 @@ def get( return NoneNode() if single else [NoneNode()] if single and item.__class__.__name__ != cls.__name__: - raise Exception( - f"Found {item.__class__.__name__}, and not {cls.__name__}." - ) + raise Exception(f"Found {item.__class__.__name__}, and not {cls.__name__}.") return item @@ -137,9 +135,7 @@ def make_filter(key, value): else: return fltr.format(value) - filters = [ - make_filter(k, v) for k, v in kwargs.items() if k in cls._filters - ] + filters = [make_filter(k, v) for k, v in kwargs.items() if k in cls._filters] if hasattr(cls, "_required_filters"): filters += list(cls._required_filters) filters = " and ".join((filter(lambda x: x, filters))) @@ -194,10 +190,7 @@ class Person(BaseAbstract): "other": f'eq(gender, "{Gender.OTHER.value}")', }, "family": ("family", "uid(family)"), - "living": { - "true": "(not has(death_year))", - "false": "has(death_year)", - }, + "living": {"true": "(not has(death_year))", "false": "has(death_year)"}, } _subqueries = { "family": """ @@ -284,16 +277,8 @@ class Person(BaseAbstract): @classmethod def _get_parents(cls, person, step=1): - father = ( - Person.get(person.father.uid) - if hasattr(person, "father") - else None - ) - mother = ( - Person.get(person.mother.uid) - if hasattr(person, "mother") - else None - ) + father = Person.get(person.father.uid) if hasattr(person, "father") else None + mother = Person.get(person.mother.uid) if hasattr(person, "mother") else None generation = [ {"person": father, "step": step}, {"person": mother, "step": step}, @@ -317,6 +302,4 @@ def _get_parents(cls, person, step=1): @property def ancestors(self): - return [{"person": self, "step": 0}] + self.__class__._get_parents( - self - ) + return [{"person": self, "step": 0}] + self.__class__._get_parents(self) diff --git a/pydiggy/_types.py b/pydiggy/_types.py index a74a8d6..d961c96 100644 --- a/pydiggy/_types.py +++ b/pydiggy/_types.py @@ -30,6 +30,7 @@ class Directive: A directive adds extra instructions to a schema or query. Annotated with the '@' symbol and optional arguments in parens. """ + def __str__(self): args = [] if "__annotations__" in self.__class__.__dict__: diff --git a/pydiggy/node.py b/pydiggy/node.py index 2dc9099..0983095 100644 --- a/pydiggy/node.py +++ b/pydiggy/node.py @@ -83,8 +83,7 @@ def get_node_type(name: str) -> Node: def _force_instance( - directive: Union[Directive, reverse, count, upsert, lang], - prop_type: str = None, + directive: Union[Directive, reverse, count, upsert, lang], prop_type: str = None ) -> Directive: # TODO: # - Make sure directive is an instance of, or a class defined as a directive @@ -104,9 +103,7 @@ def _force_instance( class NodeMeta(type): def __new__(cls, name, bases, attrs, **kwargs): - directives = [ - x for x in attrs if x in attrs.get("__annotations__", {}).keys() - ] + directives = [x for x in attrs if x in attrs.get("__annotations__", {}).keys()] attrs["_directives"] = dict() attrs["_instances"] = dict() @@ -139,7 +136,6 @@ class Node(metaclass=NodeMeta): _instances = dict() _staged = dict() - @classmethod def _get_staged(cls): return cls._staged @@ -212,8 +208,7 @@ def __hash__(self): return hash(self.uid) def __getattr__(self, attribute): - if (not attribute == "__annotations__")\ - and (attribute in self.__annotations__): + if (not attribute == "__annotations__") and (attribute in self.__annotations__): raise MissingAttribute(self, attribute) super().__getattribute__(attribute) @@ -229,17 +224,13 @@ def __setattr__(self, name: str, value: Any): orig = self.__dict__.get(name, None) self.__dict__[name] = value # TODO: This causes issues with Node types that want private variables - if hasattr(self, "_init")\ - and self._init\ - and not name.startswith("_"): + if hasattr(self, "_init") and self._init and not name.startswith("_"): self._dirty.add(name) if name in self._directives and any( isinstance(d, reverse) for d in self._directives[name] ): directive = list( - filter( - lambda d: isinstance(d, reverse), self._directives[name] - ) + filter(lambda d: isinstance(d, reverse), self._directives[name]) )[0] reverse_name = directive.name if directive.name else f"_{name}" @@ -274,9 +265,7 @@ def _assign(obj, key, value, do_many, remove=False): if value is not None: _assign(value, reverse_name, self, directive.many) elif orig: - _assign( - orig, reverse_name, self, directive.many, remove=True - ) + _assign(orig, reverse_name, self, directive.many, remove=True) @classmethod def create(cls, **kwargs) -> Node: @@ -320,9 +309,7 @@ def _explode( data = filter(lambda x: x[1] is not None, data) annotations = ( - instance.obj._annotations - if is_facets(instance) - else instance._annotations + instance.obj._annotations if is_facets(instance) else instance._annotations ) for key, value in data: if ( @@ -334,9 +321,7 @@ def _explode( if isinstance(value, (str, int, float, bool)): obj[key] = value elif issubclass(value.__class__, Node): - explode = ( - depth < max_depth if max_depth is not None else True - ) + explode = depth < max_depth if max_depth is not None else True if explode: obj[key] = cls._explode( value, depth=(depth + 1), max_depth=max_depth @@ -344,14 +329,10 @@ def _explode( else: obj[key] = str(value) elif isinstance(value, (list,)): - explode = ( - depth < max_depth if max_depth is not None else True - ) + explode = depth < max_depth if max_depth is not None else True if explode: obj[key] = [ - cls._explode( - x, depth=(depth + 1), max_depth=max_depth - ) + cls._explode(x, depth=(depth + 1), max_depth=max_depth) for x in value ] else: @@ -408,17 +389,14 @@ def save( client = get_client(host=host, port=9080) def _make_obj(node, pred, obj): - #TODO: Remove this in favor of the _make_obj in operations + # TODO: Remove this in favor of the _make_obj in operations annotation = annotations.get(pred, "") - if ( - hasattr(annotation, "__origin__") - and annotation.__origin__ == list - ): + if hasattr(annotation, "__origin__") and annotation.__origin__ == list: annotation = annotation.__args__[0] try: if annotation == str: - obj = re.sub('"','\\"', obj.rstrip()) + obj = re.sub('"', '\\"', obj.rstrip()) obj = f'"{obj}"' elif annotation == bool: obj = f'"{str(obj).lower()}"' @@ -435,9 +413,7 @@ def _make_obj(node, pred, obj): and passed not in staged and not isinstance(passed, int) ): - raise NotStaged( - f"<{node.__class__.__name__} {pred}={obj}>" - ) + raise NotStaged(f"<{node.__class__.__name__} {pred}={obj}>") except ValueError: raise ValueError( f"Incorrect value type. Received <{node.__class__.__name__} {pred}={obj}>. Expecting <{node.__class__.__name__} {pred}={annotation.__name__}>" @@ -451,9 +427,7 @@ def _make_obj(node, pred, obj): setters = [] deleters = [] - saveable = ( - x for x in self._dirty if x != "computed" and x in annotations - ) + saveable = (x for x in self._dirty if x != "computed" and x in annotations) for pred in saveable: obj = getattr(self, pred) @@ -525,8 +499,6 @@ def _make_obj(node, pred, obj): transaction.discard() - - class NodeTypeRegistry: _i = _count() _node_types = [] @@ -589,33 +561,24 @@ def _generate_schema(cls) -> str: prop_type = prop_type.__args__[0] prop_type = PropType( - prop_type, - is_list_type, - node._directives.get(prop_name, []), + prop_type, is_list_type, node._directives.get(prop_name, []) ) if prop_name in edges: - if prop_type != edges.get( - prop_name - ) and not cls._is_node_type(prop_type[0]): + if prop_type != edges.get(prop_name) and not cls._is_node_type( + prop_type[0] + ): # Check if there is a type conflict if ( - edges.get(prop_name).directives - != prop_type.directives + edges.get(prop_name).directives != prop_type.directives and all( - ( - inspect.isclass(x) - and issubclass(x, Directive) - ) + (inspect.isclass(x) and issubclass(x, Directive)) or issubclass(x.__class__, Directive) for x in edges.get(prop_name).directives ) and all( - ( - inspect.isclass(x) - and issubclass(x, Directive) - ) + (inspect.isclass(x) and issubclass(x, Directive)) or issubclass(x.__class__, Directive) for x in prop_type.directives ) @@ -631,9 +594,7 @@ def _generate_schema(cls) -> str: edges[prop_name] = prop_type elif cls._is_node_type(prop_type[0]): edges[prop_name] = PropType( - "uid", - is_list_type, - node._directives.get(prop_name, []), + "uid", is_list_type, node._directives.get(prop_name, []) ) else: if prop_name != "uid": @@ -695,7 +656,7 @@ def _clear_staged(cls): @classmethod def _hydrate( cls, raw: Dict[str, Any], types: Dict[str, Node] = None - ) -> Node: # -> Dict[str: List[Node]] + ) -> Node: # -> Dict[str: List[Node]] # TODO: # - Accept types that are passed. Loop thru them and register if needed # and raising an exception if they are not valid. @@ -723,14 +684,10 @@ def _hydrate( computed = {} pred_items = [ - (pred, value) - for pred, value in raw.items() - if not pred.startswith("_") + (pred, value) for pred, value in raw.items() if not pred.startswith("_") ] - annotations = get_type_hints( - k, globalns=globals(), localns=localns - ) + annotations = get_type_hints(k, globalns=globals(), localns=localns) for pred, value in pred_items: """ The pred falls into one of three categories: @@ -773,9 +730,7 @@ def _hydrate( for x in value: keys = deepcopy(list(x.keys())) value_facet_data = [ - (k.split("|")[1], x.pop(k)) - for k in keys - if "|" in k + (k.split("|")[1], x.pop(k)) for k in keys if "|" in k ] item = NodeTypeRegistry._hydrate(x) @@ -784,11 +739,7 @@ def _hydrate( delay.append((item, p, value_facet_data)) elif isinstance(value, dict): delay.append( - ( - get_node(value.get("_type"))._hydrate(value), - p, - None, - ) + (get_node(value.get("_type"))._hydrate(value), p, None) ) else: if pred.endswith("_uid"): diff --git a/pydiggy/operations.py b/pydiggy/operations.py index 45cd47a..9db8244 100644 --- a/pydiggy/operations.py +++ b/pydiggy/operations.py @@ -13,10 +13,7 @@ def _make_obj(node, pred, obj): annotation = node._annotations.get(pred, "") - if ( - hasattr(annotation, "__origin__") - and annotation.__origin__ == list - ): + if hasattr(annotation, "__origin__") and annotation.__origin__ == list: annotation = annotation.__args__[0] if issubclass(obj.__class__, Enum): @@ -52,7 +49,7 @@ def _make_obj(node, pred, obj): elif isinstance(obj, datetime): obj = f'"{obj.isoformat()}"' else: - obj = re.sub('"','\\"', obj.rstrip()) + obj = re.sub('"', '\\"', obj.rstrip()) obj = f'"{obj}"' except ValueError: raise ValueError( diff --git a/pydiggy/utils.py b/pydiggy/utils.py index f6e2b71..4de1953 100644 --- a/pydiggy/utils.py +++ b/pydiggy/utils.py @@ -1,5 +1,6 @@ import re + def _parse_subject(uid): if isinstance(uid, int): return f"<{hex(uid)}>", uid @@ -12,7 +13,7 @@ def _rdf_value(value): Translates a value into a string annotated with an RDF type """ if isinstance(value, str): - value = re.sub('"','\\"', value.rstrip()) + value = re.sub('"', '\\"', value.rstrip()) value = f'"{value}"' elif isinstance(value, bool): value = f'"{str(value).lower()}"' diff --git a/tests/test_mutation.py b/tests/test_mutation.py index e46ddf7..f80c9d9 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -1,5 +1,6 @@ from pydiggy import Facets, generate_mutation, NodeTypeRegistry + def test_mutations(RegionClass): Region = RegionClass @@ -58,7 +59,7 @@ def test__mutation__with__quotes(RegionClass): NodeTypeRegistry._reset() - florida = Region(name="Florida \'The \"Sunshine\" State\'") + florida = Region(name="Florida 'The \"Sunshine\" State'") florida.stage() @@ -68,4 +69,4 @@ def test__mutation__with__quotes(RegionClass): _:unsaved.0 <_type> "Region" . _:unsaved.0 "Florida 'The \\"Sunshine\\" State'" .""" - assert mutation == control \ No newline at end of file + assert mutation == control diff --git a/tests/test_node.py b/tests/test_node.py index 107e373..316ca95 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -55,14 +55,16 @@ def test__node__with__quotes(RegionClass): NodeTypeRegistry._reset() - florida = Region(name="Florida \'The \"Sunshine\" State\'") + florida = Region(name="Florida 'The \"Sunshine\" State'") regions = NodeTypeRegistry.json().get("Region") control = [ - {'_type': 'Region', - 'name': 'Florida \'The "Sunshine" State\'', - 'uid': 'unsaved.0'} + { + "_type": "Region", + "name": "Florida 'The \"Sunshine\" State'", + "uid": "unsaved.0", + } ] - assert regions == control \ No newline at end of file + assert regions == control diff --git a/tests/test_operations.py b/tests/test_operations.py index e3a6708..21d02dd 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -8,7 +8,7 @@ def test__parse_subject(): subject = operations._parse_subject(123) assert subject == ("<0x7b>", 123) - subject = operations._parse_subject(0x7b) + subject = operations._parse_subject(0x7B) assert subject == ("<0x7b>", 123) From 0a885f0295f119dd4b335235e6197c720f713003 Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Thu, 13 Jun 2019 14:15:53 -0400 Subject: [PATCH 7/7] Add Node.uid back, with comment. --- pydiggy/node.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pydiggy/node.py b/pydiggy/node.py index 0983095..45eac82 100644 --- a/pydiggy/node.py +++ b/pydiggy/node.py @@ -136,6 +136,10 @@ class Node(metaclass=NodeMeta): _instances = dict() _staged = dict() + # uid is not used as a class variable, it is part of the required + # schema of a Node, and is thus part of the superclass. + uid : int + @classmethod def _get_staged(cls): return cls._staged