Skip to content

Commit

Permalink
Merge pull request #54 from kindermax/add-deprecated-directive
Browse files Browse the repository at this point in the history
add deprecated directive
  • Loading branch information
kindermax committed Jul 23, 2021
2 parents 13e9eb5 + 53dfd97 commit 451d0e7
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 17 deletions.
32 changes: 32 additions & 0 deletions hiku/directives/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import (
Optional,
Union,
)

from hiku.graph import (
Field,
Link,
)


class DirectiveBase:
pass


class Deprecated(DirectiveBase):
"""
https://spec.graphql.org/June2018/#sec--deprecated
"""
def __init__(self, reason: Optional[str] = None):
self.reason = reason

def accept(self, visitor):
return visitor.visit_deprecated_directive(self)


def get_deprecated(field: Union[Field, Link]) -> Optional[Deprecated]:
"""Get deprecated directive"""
return next(
(d for d in field.directives if isinstance(d, Deprecated)),
None
)
13 changes: 6 additions & 7 deletions hiku/federation/directive.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
class _DirectiveBase:
pass
from hiku.directives import DirectiveBase


class Key(_DirectiveBase):
class Key(DirectiveBase):
"""
https://www.apollographql.com/docs/federation/federation-spec/#key
"""
Expand All @@ -13,7 +12,7 @@ def accept(self, visitor):
return visitor.visit_key_directive(self)


class Provides(_DirectiveBase):
class Provides(DirectiveBase):
"""
https://www.apollographql.com/docs/federation/federation-spec/#provides
"""
Expand All @@ -24,7 +23,7 @@ def accept(self, visitor):
return visitor.visit_provides_directive(self)


class Requires(_DirectiveBase):
class Requires(DirectiveBase):
"""
https://www.apollographql.com/docs/federation/federation-spec/#requires
"""
Expand All @@ -35,15 +34,15 @@ def accept(self, visitor):
return visitor.visit_requires_directive(self)


class External(_DirectiveBase):
class External(DirectiveBase):
"""
https://www.apollographql.com/docs/federation/federation-spec/#external
"""
def accept(self, visitor):
return visitor.visit_external_directive(self)


class Extends(_DirectiveBase):
class Extends(DirectiveBase):
"""
Apollo Federation supports using an @extends directive in place of extend
type to annotate type references
Expand Down
14 changes: 13 additions & 1 deletion hiku/federation/sdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ def visit_link(self, obj: Link):
return ast.FieldDefinitionNode(
name=_name(obj.name),
arguments=[self.visit(o) for o in obj.options],
type=_encode_type(obj.type)
type=_encode_type(obj.type),
directives=[self.visit(d) for d in obj.directives]
)

def visit_option(self, obj: Option):
Expand Down Expand Up @@ -209,6 +210,17 @@ def visit_external_directive(self, obj):
def visit_extends_directive(self, obj):
return ast.DirectiveNode(name=_name('extends'))

def visit_deprecated_directive(self, obj):
return ast.DirectiveNode(
name=_name('deprecated'),
arguments=[
ast.ArgumentNode(
name=_name('reason'),
value=ast.StringValueNode(value=obj.reason),
),
],
)


def get_ast(graph: Graph) -> ast.DocumentNode:
graph = _StripGraph().visit(graph)
Expand Down
29 changes: 27 additions & 2 deletions hiku/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ class Field(AbstractField):
]),
])
Example with directives::
graph = Graph([
Root([
Field('lorem-ipsum', String, func,
options=[Option('words', Integer, default=50)],
directives=[Deprecated('use another field')]),
]),
])
Data loading protocol::
# root node fields
Expand All @@ -118,6 +128,7 @@ def __init__(self, name, type_, func, *, options=None, description=None,
:param func: function to load field's data
:param options: list of acceptable options
:param description: description of the field
:param directives: list of directives for the field
"""
self.name = name
self.type = type_
Expand Down Expand Up @@ -189,6 +200,17 @@ class Link(AbstractLink):
]),
])
Example with directives::
graph = Graph([
Node('user', [...]),
Root([
Link('users', Sequence[TypeRef['user']], func, requires=None,
options=[Option('limit', Integer, default=100)],
directives=[Deprecated('do not use')]),
]),
])
Identifiers loading function protocol::
# From root node or if requires is None:
Expand Down Expand Up @@ -225,7 +247,8 @@ def func(ids, options) -> List[List[T]]
options.
"""
def __init__(
self, name, type_, func, *, requires, options=None, description=None
self, name, type_, func, *, requires, options=None, description=None,
directives=None
):
"""
:param name: name of the link
Expand All @@ -235,6 +258,7 @@ def __init__(
identifiers of the linked node
:param options: list of acceptable options
:param description: description of the link
:param directives: list of directives for the link
"""
type_enum, node = get_type_enum(type_)

Expand All @@ -246,6 +270,7 @@ def __init__(
self.requires = requires
self.options = options or ()
self.description = description
self.directives = directives or ()

def __repr__(self):
return '{}({!r}, {!r}, {!r}, ...)'.format(self.__class__.__name__,
Expand Down Expand Up @@ -468,7 +493,7 @@ def visit_link(self, obj):
return Link(obj.name, obj.type, obj.func,
requires=obj.requires,
options=[self.visit(op) for op in obj.options],
description=obj.description)
description=obj.description, directives=obj.directives)

def visit_node(self, obj):
return Node(obj.name, [self.visit(f) for f in obj.fields],
Expand Down
22 changes: 20 additions & 2 deletions hiku/introspection/graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from functools import partial
from collections import OrderedDict

from ..directives import get_deprecated
from ..graph import Graph, Root, Node, Link, Option, Field, Nothing
from ..graph import GraphVisitor, GraphTransformer
from ..types import (
Expand Down Expand Up @@ -84,6 +85,19 @@ def args_map(self):
),
],
),
Directive(
name='deprecated',
locations=['FIELD_DEFINITION', 'ENUM_VALUE'],
description='Marks the field or enum value as deprecated',
args=[
Directive.Argument(
name='reason',
type_ident=SCALAR('String'),
description='Deprecation reason.',
default_value=None,
),
],
),
)


Expand Down Expand Up @@ -327,11 +341,15 @@ def field_info(schema, fields, ids):
if ident.node in nodes_map:
node = nodes_map[ident.node]
field = node.fields_map[ident.name]
deprecated = None
if isinstance(field, (Field, Link)):
deprecated = get_deprecated(field)

info = {'id': ident,
'name': field.name,
'description': field.description,
'isDeprecated': False,
'deprecationReason': None}
'isDeprecated': bool(deprecated),
'deprecationReason': deprecated and deprecated.reason}
else:
info = {'id': ident,
'name': ident.name,
Expand Down
24 changes: 23 additions & 1 deletion hiku/validate/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
from contextlib import contextmanager
from collections import Counter

from ..graph import GraphVisitor, Root
from ..directives import Deprecated
from ..graph import (
GraphVisitor,
Root,
Field,
)
from ..graph import AbstractNode, AbstractField, AbstractLink, AbstractOption

from .errors import Errors
Expand Down Expand Up @@ -78,10 +83,22 @@ def _format_path(self, obj=None):
path = self._ctx + ([obj] if obj is not None else [])
return ''.join(self._name_formatter.visit(i) for i in path)

def _validate_deprecated_duplicates(self, obj):
deprecated_count = sum(
(1 for d in obj.directives if isinstance(d, Deprecated)))
if deprecated_count > 1:
self.errors.report(
'Deprecated directive must be used only once for "{}", found {}'
.format(self._format_path(obj), deprecated_count)
)

def visit_option(self, obj):
# TODO: check option default value according to the option type
pass

def visit_field(self, obj: Field):
self._validate_deprecated_duplicates(obj)

def visit_link(self, obj):
invalid = [f for f in obj.options
if not isinstance(f, self._link_accept_types)]
Expand Down Expand Up @@ -109,6 +126,8 @@ def visit_link(self, obj):
.format(obj.name, obj.requires, self._format_path())
)

self._validate_deprecated_duplicates(obj)

def visit_node(self, obj):
node_name = obj.name or 'root'
invalid = [f for f in obj.fields
Expand All @@ -131,6 +150,9 @@ def visit_node(self, obj):
.format(node_name, self._format_names(duplicates))
)

if sum((1 for d in obj.directives if isinstance(d, Deprecated))) > 0:
self.errors.report('Deprecated directive can not be used in Node')

def visit_root(self, obj):
self.visit_node(obj)

Expand Down
19 changes: 18 additions & 1 deletion tests/test_federation/test_introspection_graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ def _field_directive(name, args):
}


def _field_enum_directive(name, args):
return {
'name': name,
'description': ANY,
'locations': ['FIELD_DEFINITION', 'ENUM_VALUE'],
'args': args,
}


def _object_directive(name, args):
return {
'name': name,
Expand Down Expand Up @@ -131,6 +140,9 @@ def _schema(types, with_mutation=False) -> dict:
_field_directive('include', [
_ival('if', _non_null(_BOOL), description=ANY),
]),
_field_enum_directive('deprecated', [
_ival('reason', _STR, description=ANY)
]),
_object_directive('key', [
_ival('fields', _non_null(_FIELDSET), description=ANY),
]),
Expand Down Expand Up @@ -177,6 +189,9 @@ def test_federated_introspection_query_entities(self):
exp = _schema([
_type('Order', 'OBJECT', fields=[
_field('cartId', _non_null(_INT)),
_field('oldCart', _non_null(_obj('Cart')),
isDeprecated=True,
deprecationReason='use cart instead'),
_field('cart', _non_null(_obj('Cart'))),
]),
_type('Cart', 'OBJECT', fields=[
Expand All @@ -187,7 +202,9 @@ def test_federated_introspection_query_entities(self):
_type('CartItem', 'OBJECT', fields=[
_field('id', _non_null(_INT)),
_field('cart_id', _non_null(_INT)),
_field('name', _non_null(_STR)),
_field('name', _non_null(_STR),
isDeprecated=True,
deprecationReason='do not use'),
_field('photo', _STR, args=[
_ival(
'width',
Expand Down
6 changes: 4 additions & 2 deletions tests/test_federation/test_sdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def test_print_graph_sdl(self):
'',
'type Order @key(fields: "cartId") @extends {',
' cartId: Int! @external',
' oldCart: Cart! @deprecated(reason: "use cart instead")',
' cart: Cart!',
'}',
'',
Expand All @@ -48,7 +49,7 @@ def test_print_graph_sdl(self):
'type CartItem {',
' id: Int!',
' cart_id: Int!',
' name: String!',
' name: String! @deprecated(reason: "do not use")',
' photo(width: Int!, height: Int!): String',
'}',
'',
Expand All @@ -70,6 +71,7 @@ def test_print_introspected_graph_sdl(self):
'',
'type Order @key(fields: "cartId") @extends {',
' cartId: Int! @external',
' oldCart: Cart! @deprecated(reason: "use cart instead")',
' cart: Cart!',
'}',
'',
Expand All @@ -82,7 +84,7 @@ def test_print_introspected_graph_sdl(self):
'type CartItem {',
' id: Int!',
' cart_id: Int!',
' name: String!',
' name: String! @deprecated(reason: "do not use")',
' photo(width: Int!, height: Int!): String',
'}',
'',
Expand Down
10 changes: 9 additions & 1 deletion tests/test_federation/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from hiku.directives import Deprecated
from hiku.federation.directive import (
External,
Key,
Expand Down Expand Up @@ -107,6 +108,8 @@ async def async_direct_link(ids):
Node('Order', [
Field('cartId', Integer, ids_resolver,
directives=[External()]),
Link('oldCart', TypeRef['Cart'], direct_link, requires='cartId',
directives=[Deprecated('use cart instead')]),
Link('cart', TypeRef['Cart'], direct_link, requires='cartId'),
], directives=[Key('cartId'), Extends()]),
Node('Cart', [
Expand All @@ -118,7 +121,12 @@ async def async_direct_link(ids):
Node('CartItem', [
Field('id', Integer, cart_item_resolver),
Field('cart_id', Integer, cart_item_resolver),
Field('name', String, cart_item_resolver),
Field(
'name',
String,
cart_item_resolver,
directives=[Deprecated('do not use')]
),
Field('photo', Optional[String], lambda: None, options=[
Option('width', Integer),
Option('height', Integer),
Expand Down

0 comments on commit 451d0e7

Please sign in to comment.