Skip to content

Commit

Permalink
Merge 97ff155 into f6079c6
Browse files Browse the repository at this point in the history
  • Loading branch information
pmantica1 committed May 1, 2019
2 parents f6079c6 + 97ff155 commit 01790d1
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 90 deletions.
80 changes: 41 additions & 39 deletions graphql_compiler/schema_generation/graphql_schema.py
Expand Up @@ -122,45 +122,47 @@ def _get_fields_for_class(schema_graph, graphql_types, field_type_overrides, hid

# Add edge GraphQL fields (edges to other vertex classes).
schema_element = schema_graph.get_element_by_class_name(cls_name)
outbound_edges = (
('out_{}'.format(out_edge_name),
schema_graph.get_element_by_class_name(out_edge_name).properties[
EDGE_DESTINATION_PROPERTY_NAME].qualifier)
for out_edge_name in schema_element.out_connections
)
inbound_edges = (
('in_{}'.format(in_edge_name),
schema_graph.get_element_by_class_name(in_edge_name).properties[
EDGE_SOURCE_PROPERTY_NAME].qualifier)
for in_edge_name in schema_element.in_connections
)
for field_name, to_type_name in chain(outbound_edges, inbound_edges):
edge_endpoint_type_name = None
subclasses = schema_graph.get_subclass_set(to_type_name)

to_type_abstract = schema_graph.get_element_by_class_name(to_type_name).abstract
if not to_type_abstract and len(subclasses) > 1:
# If the edge endpoint type has no subclasses, it can't be coerced into any other type.
# If the edge endpoint type is abstract (an interface type), we can already
# coerce it to the proper type with a GraphQL fragment. However, if the endpoint type
# is non-abstract and has subclasses, we need to return its subclasses as an union type.
# This is because GraphQL fragments cannot be applied on concrete types, and
# GraphQL does not support inheritance of concrete types.
type_names_to_union = [
subclass
for subclass in subclasses
if subclass not in hidden_classes
]
if type_names_to_union:
edge_endpoint_type_name = _get_union_type_name(type_names_to_union)
else:
if to_type_name not in hidden_classes:
edge_endpoint_type_name = to_type_name

if edge_endpoint_type_name is not None:
# If we decided to not hide this edge due to its endpoint type being non-representable,
# represent the edge field as the GraphQL type List(edge_endpoint_type_name).
result[field_name] = GraphQLList(graphql_types[edge_endpoint_type_name])
if not schema_element.is_non_graph:
outbound_edges = (
('out_{}'.format(out_edge_name),
schema_graph.get_element_by_class_name(out_edge_name).properties[
EDGE_DESTINATION_PROPERTY_NAME].qualifier)
for out_edge_name in schema_element.out_connections
)
inbound_edges = (
('in_{}'.format(in_edge_name),
schema_graph.get_element_by_class_name(in_edge_name).properties[
EDGE_SOURCE_PROPERTY_NAME].qualifier)
for in_edge_name in schema_element.in_connections
)
for field_name, to_type_name in chain(outbound_edges, inbound_edges):
edge_endpoint_type_name = None
subclasses = schema_graph.get_subclass_set(to_type_name)

to_type_abstract = schema_graph.get_element_by_class_name(to_type_name).abstract
if not to_type_abstract and len(subclasses) > 1:
# If the edge endpoint type has no subclasses, it can't be coerced into any other
# type. If the edge endpoint type is abstract (an interface type), we can already
# coerce it to the proper type with a GraphQL fragment. However, if the endpoint
# type is non-abstract and has subclasses, we need to return its subclasses as an
# union type. This is because GraphQL fragments cannot be applied on concrete
# types, and GraphQL does not support inheritance of concrete types.
type_names_to_union = [
subclass
for subclass in subclasses
if subclass not in hidden_classes
]
if type_names_to_union:
edge_endpoint_type_name = _get_union_type_name(type_names_to_union)
else:
if to_type_name not in hidden_classes:
edge_endpoint_type_name = to_type_name

if edge_endpoint_type_name is not None:
# If we decided to not hide this edge due to its endpoint type being
# non-representable, represent the edge field as the GraphQL type
# List(edge_endpoint_type_name).
result[field_name] = GraphQLList(graphql_types[edge_endpoint_type_name])

for field_name, field_type in six.iteritems(field_type_overrides):
if field_name not in result:
Expand Down
144 changes: 93 additions & 51 deletions graphql_compiler/schema_generation/schema_graph.py
@@ -1,4 +1,5 @@
# Copyright 2019-present Kensho Technologies, LLC.
from abc import ABCMeta, abstractmethod
from itertools import chain

import six
Expand Down Expand Up @@ -88,19 +89,19 @@ def get_superclasses_from_class_definition(class_definition):
return []


@six.python_2_unicode_compatible
@six.add_metaclass(ABCMeta)
class SchemaElement(object):
ELEMENT_KIND_VERTEX = 'vertex'
ELEMENT_KIND_EDGE = 'edge'
ELEMENT_KIND_NON_GRAPH = 'non-graph'

def __init__(self, class_name, kind, abstract, properties, class_fields):
# Since an abstract class without any abstract methods can be instantiated, we make the
# init method abstract in order to make it impossible to instantiate this class.
# Inspiration came from: https://stackoverflow.com/questions/44800659/python-abstract-class.
@abstractmethod
def __init__(self, class_name, abstract, properties, class_fields, *args, **kwargs):
"""Create a new SchemaElement object.
Args:
class_name: string, the name of the schema element class.
kind: string, one of the values of ELEMENT_KIND_VERTEX, ELEMENT_KIND_EDGE or
ELEMENT_KIND_NON_GRAPH. Describes whether the schema class is a vertex, edge,
or non-graph class.
abstract: bool, True if the class is abstract, and False otherwise.
properties: dict, property name -> PropertyDescriptor describing the properties of
the schema element.
Expand All @@ -109,38 +110,15 @@ def __init__(self, class_name, kind, abstract, properties, class_fields):
Returns:
a SchemaElement with the given parameters
"""
if kind == SchemaElement.ELEMENT_KIND_EDGE:
_validate_edges_do_not_have_extra_links(class_name, properties)
if not abstract:
_validate_non_abstract_edge_has_defined_endpoint_types(class_name, properties)

else:
# Non-edges must not have properties like "in" or "out" defined, and
# must not have properties of type "Link".
_validate_non_edges_do_not_have_edge_like_properties(class_name, properties)

_validate_property_names(class_name, properties)

self._class_name = class_name
self._kind = kind
self._abstract = abstract
self._properties = properties
self._class_fields = class_fields

# In the schema graph, both vertices and edges are represented with vertices.
# These dicts have the name of the adjacent schema vertex in the appropriate direction.
#
# For vertex classes:
# in = the edge is attached with its head / arrow side
# out = the edge is attached with its tail side
#
# For edge classes:
# in = the tail side of the edge
# out = the head / arrow side of the edge
#
# For non-graph classes, these properties are always empty sets.
self.in_connections = set()
self.out_connections = set()
self._print_args = (class_name, abstract, properties, class_fields) + args
self._print_kwargs = kwargs

@property
def abstract(self):
Expand All @@ -165,32 +143,96 @@ def class_fields(self):
@property
def is_vertex(self):
"""Return True if the schema element represents a vertex type, and False otherwise."""
return self._kind == SchemaElement.ELEMENT_KIND_VERTEX
return isinstance(self, VertexType)

@property
def is_edge(self):
"""Return True if the schema element represents an edge type, and False otherwise."""
return self._kind == SchemaElement.ELEMENT_KIND_EDGE
return isinstance(self, EdgeType)

@property
def is_non_graph(self):
"""Return True if the schema element represents a non-graph type, and False otherwise."""
return self._kind == SchemaElement.ELEMENT_KIND_NON_GRAPH
return isinstance(self, NonGraphElement)

def freeze(self):
"""Do nothing."""
pass

def __str__(self):
"""Return a human-readable unicode representation of this SchemaElement."""
printed_args = []
if self._print_args:
printed_args.append('{args}')
if self._print_kwargs:
printed_args.append('{kwargs}')

template = u'{cls_name}(' + u', '.join(printed_args) + u')'
return template.format(cls_name=type(self).__name__,
args=self._print_args,
kwargs=self._print_kwargs)

def __repr__(self):
"""Return a human-readable str representation of the SchemaElement object."""
return self.__str__()


class GraphElement(SchemaElement):

# Since an abstract class without any abstract methods can be instantiated, we make the
# init method abstract in order to make it impossible to instantiate this class.
# Inspiration came from: https://stackoverflow.com/questions/44800659/python-abstract-class.
@abstractmethod
def __init__(self, class_name, abstract, properties, class_fields):
super(GraphElement, self).__init__(class_name, abstract, properties, class_fields)

# In the schema graph, both vertices and edges are represented with vertices.
# These dicts have the name of the adjacent schema vertex in the appropriate direction.
#
# For vertex classes:
# in = the edge is attached with its head / arrow side
# out = the edge is attached with its tail side
#
# For edge classes:
# in = the tail side of the edge
# out = the head / arrow side of the edge
#
# For non-graph classes, these properties are always empty sets.
self.in_connections = set()
self.out_connections = set()

def freeze(self):
"""Make the SchemaElement's connections immutable."""
super(GraphElement, self).freeze()
self.in_connections = frozenset(self.in_connections)
self.out_connections = frozenset(self.out_connections)

def __str__(self):
"""Stringify the SchemaElement."""
return (
'SchemaElement({}, {}, abstract={}, properties={}, in_conn={}, out_conn={})'
.format(self._class_name, self._kind, self._abstract, self._properties,
self.in_connections, self.out_connections)
)

__repr__ = __str__
class VertexType(GraphElement):
def __init__(self, class_name, abstract, properties, class_fields):
super(VertexType, self).__init__(class_name, abstract, properties, class_fields)

# Non-edges must not have properties like "in" or "out" defined, and
# must not have properties of type "Link".
_validate_non_edges_do_not_have_edge_like_properties(class_name, properties)


class EdgeType(GraphElement):
def __init__(self, class_name, abstract, properties, class_fields):
super(EdgeType, self).__init__(class_name, abstract, properties, class_fields)

_validate_edges_do_not_have_extra_links(class_name, properties)
if not abstract:
_validate_non_abstract_edge_has_defined_endpoint_types(class_name, properties)


class NonGraphElement(SchemaElement):
def __init__(self, class_name, abstract, properties, class_fields):
super(NonGraphElement, self).__init__(class_name, abstract, properties, class_fields)

# Non-edges must not have properties like "in" or "out" defined, and
# must not have properties of type "Link".
_validate_non_edges_do_not_have_edge_like_properties(class_name, properties)


class SchemaGraph(object):
Expand Down Expand Up @@ -269,9 +311,9 @@ class is an edge class, and
self._split_classes_by_kind(class_name_to_definition)

kind_to_class_names = {
SchemaElement.ELEMENT_KIND_NON_GRAPH: self._non_graph_class_names,
SchemaElement.ELEMENT_KIND_VERTEX: self._vertex_class_names,
SchemaElement.ELEMENT_KIND_EDGE: self._edge_class_names,
NonGraphElement: self._non_graph_class_names,
VertexType: self._vertex_class_names,
EdgeType: self._edge_class_names,
}
for kind, class_names in six.iteritems(kind_to_class_names):
self._set_up_schema_elements_of_kind(class_name_to_definition, kind, class_names)
Expand Down Expand Up @@ -460,7 +502,7 @@ def _split_classes_by_kind(self, class_name_to_definition):
self._edge_class_names = frozenset(self._edge_class_names)
self._non_graph_class_names = frozenset(self._non_graph_class_names)

def _set_up_schema_elements_of_kind(self, class_name_to_definition, kind, class_names):
def _set_up_schema_elements_of_kind(self, class_name_to_definition, kind_cls, class_names):
"""Load all schema classes of the given kind. Used as part of __init__."""
allowed_duplicated_edge_property_names = frozenset({
EDGE_DESTINATION_PROPERTY_NAME, EDGE_SOURCE_PROPERTY_NAME
Expand Down Expand Up @@ -501,7 +543,7 @@ def _set_up_schema_elements_of_kind(self, class_name_to_definition, kind, class_
# in the entire inheritance hierarchy of any schema class, of any kind.
duplication_allowed = all((
property_name in allowed_duplicated_edge_property_names,
kind == SchemaElement.ELEMENT_KIND_EDGE
issubclass(kind_cls, EdgeType)
))

if not duplication_allowed and property_name in property_name_to_descriptor:
Expand Down Expand Up @@ -537,13 +579,13 @@ def _set_up_schema_elements_of_kind(self, class_name_to_definition, kind, class_
property_name_to_descriptor[property_name] = property_descriptor

if (property_name not in property_name_to_descriptor and not abstract and
kind == SchemaElement.ELEMENT_KIND_EDGE):
issubclass(kind_cls, EdgeType)):
raise AssertionError(u'For property "{}" of non-abstract edge class "{}", '
u'no such subclass-of-all-elements exists.'
.format(property_name, class_name))

self._elements[class_name] = SchemaElement(class_name, kind, abstract,
property_name_to_descriptor, class_fields)
self._elements[class_name] = kind_cls(class_name, abstract, property_name_to_descriptor,
class_fields)

def _create_descriptor_from_property_definition(self, class_name, property_definition,
class_name_to_definition):
Expand Down

0 comments on commit 01790d1

Please sign in to comment.