From 07dc224b6e6b5befff98d2ce502fb6c2949852b2 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sat, 13 Jan 2024 00:58:43 +0530 Subject: [PATCH 1/7] feat: add support for field & non field level validation --- README.md | 9 ++-- example/complex_uses.py | 21 +++++++-- graphene_directives/__init__.py | 2 +- .../data_models/custom_directive_meta.py | 5 +- graphene_directives/directive.py | 25 ++++++---- graphene_directives/parsers.py | 15 ++++-- graphene_directives/schema.py | 47 ++++++++++++++----- tests/test_directive.py | 21 +++++++-- 8 files changed, 106 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 6762cab..f6bcac3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +from typing import Any + # Graphene Directives Schema Directives implementation for graphene @@ -144,16 +146,15 @@ schema = build_schema( import graphene from graphql import ( GraphQLArgument, - GraphQLDirective, GraphQLInt, GraphQLNonNull, GraphQLString, ) -from graphene_directives import CustomDirective, DirectiveLocation, build_schema, directive_decorator +from graphene_directives import CustomDirective, DirectiveLocation, ValidatorLocation, build_schema, directive_decorator -def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool: +def validate_input(_type: Any, _location_type: ValidatorLocation, inputs: dict) -> bool: if inputs.get("max_age") > 2500: return False return True @@ -175,7 +176,7 @@ CacheDirective = CustomDirective( ), }, description="Caching directive to control cache behavior of fields or fragments.", - validator=validate_input, + non_field_validator=validate_input, ) # This returns a partial of directive function diff --git a/example/complex_uses.py b/example/complex_uses.py index 09c0484..090d14b 100644 --- a/example/complex_uses.py +++ b/example/complex_uses.py @@ -1,10 +1,10 @@ import os +from typing import Any import graphene from graphql import ( GraphQLArgument, GraphQLBoolean, - GraphQLDirective, GraphQLInt, GraphQLNonNull, GraphQLString, @@ -21,7 +21,21 @@ curr_dir = os.path.dirname(os.path.realpath(__file__)) -def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool: +def validate_non_field_input(_type: Any, inputs: dict) -> bool: + """ + def validator (type_: graphene type, inputs: Any) -> bool, + if validator returns False, library raises DirectiveCustomValidationError + """ + if inputs.get("max_age") > 2500: + return False + return True + + +def validate_field_input(_parent_type: Any, _field_type: Any, inputs: dict) -> bool: + """ + def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any) -> bool, + if validator returns False, library raises DirectiveCustomValidationError + """ if inputs.get("max_age") > 2500: return False return True @@ -52,7 +66,8 @@ def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool: ), }, description="Caching directive to control cache behavior of fields or fragments.", - validator=validate_input, + non_field_validator=validate_non_field_input, + field_validator=validate_field_input, ) diff --git a/graphene_directives/__init__.py b/graphene_directives/__init__.py index 0fab068..05fabdf 100644 --- a/graphene_directives/__init__.py +++ b/graphene_directives/__init__.py @@ -1,7 +1,7 @@ from .constants import DirectiveLocation +from .data_models import SchemaDirective from .directive import ACCEPTED_TYPES from .directive import CustomDirective, directive, directive_decorator -from .data_models import SchemaDirective from .exceptions import DirectiveCustomValidationError, DirectiveValidationError from .main import build_schema diff --git a/graphene_directives/data_models/custom_directive_meta.py b/graphene_directives/data_models/custom_directive_meta.py index 7bbae0d..0767678 100644 --- a/graphene_directives/data_models/custom_directive_meta.py +++ b/graphene_directives/data_models/custom_directive_meta.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Any, Callable, Union -from graphql import DirectiveLocation as GrapheneDirectiveLocation, GraphQLDirective +from graphql import DirectiveLocation as GrapheneDirectiveLocation @dataclass @@ -13,4 +13,5 @@ class CustomDirectiveMeta: non_field_types: set[GrapheneDirectiveLocation] supports_field_types: bool supports_non_field_types: bool - validator: Union[Callable[[GraphQLDirective, Any], bool], None] + non_field_validator: Union[Callable[[Any, dict[str, Any]], bool], None] + field_validator: Union[Callable[[Any, Any, dict[str, Any]], bool], None] diff --git a/graphene_directives/directive.py b/graphene_directives/directive.py index f36afb5..49de7f1 100644 --- a/graphene_directives/directive.py +++ b/graphene_directives/directive.py @@ -28,7 +28,8 @@ def CustomDirective( # noqa ast_node: Optional[ast.DirectiveDefinitionNode] = None, allow_all_directive_locations: bool = False, add_definition_to_schema: bool = True, - validator: Callable[[GraphQLDirective, Any], bool] = None, + non_field_validator: Callable[[Any, dict[str, Any]], bool] = None, + field_validator: Callable[[Any, Any, dict[str, Any]], bool] = None, ) -> GraphQLDirective: """ Creates a GraphQLDirective @@ -43,7 +44,11 @@ def CustomDirective( # noqa :param locations: list[DirectiveLocation], if need to use unsupported locations, set allow_all_directive_locations True :param allow_all_directive_locations: Allow other DirectiveLocation other than the ones supported by library :param add_definition_to_schema: If false, the @directive definition is not added to the graphql schema - :param validator: a validator function def validator (directive: GraphQLDirective, inputs: Any) -> bool, + :param non_field_validator: a validator function + def validator (type_: graphene type, inputs: Any) -> bool, + if validator returns False, library raises DirectiveCustomValidationError + :param field_validator: a validator function + def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any) -> bool, if validator returns False, library raises DirectiveCustomValidationError @@ -59,7 +64,7 @@ def CustomDirective( # noqa f"directive @{name} add_definition_to_schema type invalid expected bool" ) - if not (isinstance(validator, Callable) or validator is None): + if not (isinstance(non_field_validator, Callable) or non_field_validator is None): raise DirectiveInvalidArgTypeError( f"directive @{name} validator type invalid expected Callable[[GraphQLDirective, Any], bool] " ) @@ -109,7 +114,8 @@ def CustomDirective( # noqa non_field_types=non_field_types, supports_field_types=supports_field_types, supports_non_field_types=supports_non_field_types, - validator=validator, + non_field_validator=non_field_validator, + field_validator=field_validator, ) # Check if target_directive.locations have accepted types @@ -148,13 +154,9 @@ def directive( kwargs = {to_camel_case(field): value for (field, value) in _kwargs.items()} directive_name = str(target_directive) - custom_validator = meta_data.validator + non_field_validator = meta_data.non_field_validator kwargs = parse_argument_values(target_directive, kwargs) - if custom_validator is not None and not custom_validator(target_directive, _kwargs): - raise DirectiveCustomValidationError( - f"Custom Validation Failed for {directive_name} with args: ({kwargs})" - ) def decorator(type_: Any) -> Any: if not meta_data.supports_non_field_types: @@ -170,6 +172,11 @@ def decorator(type_: Any) -> Any: f"{directive_name} cannot be used for {type_}, valid levels are: {[str(i) for i in meta_data.valid_types]}" ) + if non_field_validator is not None and not non_field_validator(type_, _kwargs): + raise DirectiveCustomValidationError( + f"Custom Validation Failed for {directive_name} with args: ({kwargs}) at non field level {type_}" + ) + set_attribute_value( type_=type_, attribute_name=non_field_attribute_name(target_directive), diff --git a/graphene_directives/parsers.py b/graphene_directives/parsers.py index f995ecf..dc9b3d2 100644 --- a/graphene_directives/parsers.py +++ b/graphene_directives/parsers.py @@ -1,3 +1,4 @@ +import json import re from typing import Any, Collection, Dict, Union, cast @@ -63,7 +64,7 @@ def decorator_string(directive: GraphQLDirective, **kwargs: dict) -> str: formatted_args = [ ( f"{to_camel_case(key)}: " - + (f'"{value}"' if isinstance(value, str) else str(value)) + + (f'"{value}"' if isinstance(value, str) else json.dumps(value)) ) for key, value in kwargs.items() if value is not None and to_camel_case(key) in directive.args @@ -78,11 +79,15 @@ def extend_schema_string( ) -> str: schema_directives_strings = [] for schema_directive in schema_directives: + args = parse_argument_values( + schema_directive.target_directive, + { + to_camel_case(field): value + for (field, value) in schema_directive.arguments.items() + }, + ) schema_directives_strings.append( - "\t" - + decorator_string( - schema_directive.target_directive, **schema_directive.arguments - ) + "\t" + decorator_string(schema_directive.target_directive, **args) ) if len(schema_directives_strings) != 0: diff --git a/graphene_directives/schema.py b/graphene_directives/schema.py index b7a367a..eef301b 100644 --- a/graphene_directives/schema.py +++ b/graphene_directives/schema.py @@ -28,6 +28,7 @@ print_input_value, ) +from . import DirectiveCustomValidationError from .data_models import SchemaDirective from .directive import CustomDirectiveMeta from .exceptions import DirectiveValidationError @@ -243,22 +244,43 @@ def add_field_decorators(self, graphene_types: set, string_schema: str) -> str: continue for directive in self.directives: - if has_field_attribute(field, directive): - directive_values = get_field_attribute_value(field, directive) - if required_directive_field_types in set(directive.locations): - raise DirectiveValidationError( + if not has_field_attribute(field, directive): + continue + directive_values = get_field_attribute_value(field, directive) + + meta_data: CustomDirectiveMeta = getattr( + directive, "_graphene_directive" + ) + field_validator = meta_data.field_validator + + if required_directive_field_types in set(directive.locations): + raise DirectiveValidationError( + ", ".join( + [ + f"{str(directive)} cannot be used at field level", + allowed_locations, + f"at {entity_name}", + ] + ) + ) + for directive_value in directive_values: + if field_validator is not None and not field_validator( + entity_type, + field, + {to_snake_case(k): v for k, v in directive_value.items()}, + ): + raise DirectiveCustomValidationError( ", ".join( [ - f"{str(directive)} cannot be used at field level", - allowed_locations, - f"at {entity_name}", + f"Custom Validation Failed for {str(directive)} with args: ({directive_value})" + f"at field level {entity_name}:{field}" ] ) ) - for directive_value in directive_values: - str_field += ( - f" {decorator_string(directive, **directive_value)}" - ) + + str_field += ( + f" {decorator_string(directive, **directive_value)}" + ) str_fields.append(str_field) @@ -383,7 +405,8 @@ def get_directive_applied_field_types(self) -> set: for field in fields: field_type = ( - getattr(entity_type.graphene_type, field, None) + getattr(entity_type.graphene_type, to_camel_case(field), None) + or getattr(entity_type.graphene_type, to_snake_case(field), None) if not is_enum_type(entity_type) else field.value ) diff --git a/tests/test_directive.py b/tests/test_directive.py index b2fabf9..3130317 100644 --- a/tests/test_directive.py +++ b/tests/test_directive.py @@ -1,10 +1,10 @@ import os +from typing import Any import graphene from graphql import ( GraphQLArgument, GraphQLBoolean, - GraphQLDirective, GraphQLInt, GraphQLNonNull, GraphQLString, @@ -21,7 +21,21 @@ curr_dir = os.path.dirname(os.path.realpath(__file__)) -def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool: +def validate_non_field_input(_type: Any, inputs: dict) -> bool: + """ + def validator (type_: graphene type, inputs: Any) -> bool, + if validator returns False, library raises DirectiveCustomValidationError + """ + if inputs.get("max_age") > 2500: + return False + return True + + +def validate_field_input(_parent_type: Any, _field_type: Any, inputs: dict) -> bool: + """ + def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any) -> bool, + if validator returns False, library raises DirectiveCustomValidationError + """ if inputs.get("max_age") > 2500: return False return True @@ -52,7 +66,8 @@ def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool: ), }, description="Caching directive to control cache behavior of fields or fragments.", - validator=validate_input, + non_field_validator=validate_non_field_input, + field_validator=validate_field_input, ) From 95fae7da29a3e634b294b08bf5b2a0d71fdee3e5 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sat, 13 Jan 2024 14:38:05 +0530 Subject: [PATCH 2/7] fix: argument bool outputted as camelcase of python --- example/complex_uses.graphql | 18 ++++++++--------- .../test_arg_add_definition_to_schema.graphql | 8 ++++---- tests/schema_files/test_directive.graphql | 20 +++++++++---------- .../test_directive_with_repeatable.graphql | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/example/complex_uses.graphql b/example/complex_uses.graphql index 5b418f5..6daf0d2 100644 --- a/example/complex_uses.graphql +++ b/example/complex_uses.graphql @@ -30,7 +30,7 @@ directive @repeatable_directive( serviceName: String! ) repeatable on OBJECT | FIELD_DEFINITION -union SearchResult @cache(maxAge: 500) @authenticated(required: True) = Human | Droid | Starship +union SearchResult @cache(maxAge: 500) @authenticated(required: true) = Human | Droid | Starship type Human @cache(maxAge: 60) { name: String @@ -48,7 +48,7 @@ type Starship @cache(maxAge: 200) { length: Int @deprecated(reason: "Use another field") @cache(maxAge: 60) } -interface Animal @cache(maxAge: 100) @authenticated(required: True) { +interface Animal @cache(maxAge: 100) @authenticated(required: true) { age: Int! kind: Int! @cache(maxAge: 60) } @@ -58,26 +58,26 @@ type Admin @internal @key { password: String } -input HumanInput @cache(maxAge: 60) @authenticated(required: True) { +input HumanInput @cache(maxAge: 60) @authenticated(required: true) { bornIn: String """Test Description""" name: String @deprecated(reason: "Deprecated use born in") @cache(maxAge: 300) } -enum TruthEnum @cache(maxAge: 100) @authenticated(required: True) { - A @authenticated(required: True) +enum TruthEnum @cache(maxAge: 100) @authenticated(required: true) { + A @authenticated(required: true) B } -scalar DateNewScalar @cache(maxAge: 500) @authenticated(required: True) +scalar DateNewScalar @cache(maxAge: 500) @authenticated(required: true) -type User @authenticated(required: True) { +type User @authenticated(required: true) { name: String password: String - price(currency: Int @internal, country: Int @authenticated(required: True) @internal): String + price(currency: Int @internal, country: Int @authenticated(required: true) @internal): String } -type Company @authenticated(required: True) @repeatable_directive(serviceName: "CompanyService") @repeatable_directive(serviceName: "ProductService") { +type Company @authenticated(required: true) @repeatable_directive(serviceName: "CompanyService") @repeatable_directive(serviceName: "ProductService") { established: Int! name: String! @deprecated(reason: "This field is deprecated and will be removed in future") @repeatable_directive(serviceName: "CompanyService Field") @repeatable_directive(serviceName: "ProductService Field") } diff --git a/tests/schema_files/test_arg_add_definition_to_schema.graphql b/tests/schema_files/test_arg_add_definition_to_schema.graphql index 7685632..bd72716 100644 --- a/tests/schema_files/test_arg_add_definition_to_schema.graphql +++ b/tests/schema_files/test_arg_add_definition_to_schema.graphql @@ -16,17 +16,17 @@ directive @authenticated( required: Boolean! ) on OBJECT | INTERFACE | ENUM | ENUM_VALUE | UNION | INPUT_OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | SCALAR -interface Animal @cache(maxAge: 100) @authenticated(required: True) { +interface Animal @cache(maxAge: 100) @authenticated(required: true) { age: Int! kind: Int! @cache(maxAge: 60) } -enum TruthEnum @cache(maxAge: 100) @authenticated(required: True) { - A @authenticated(required: True) +enum TruthEnum @cache(maxAge: 100) @authenticated(required: true) { + A @authenticated(required: true) B } -input HumanInput @cache(maxAge: 60) @authenticated(required: True) { +input HumanInput @cache(maxAge: 60) @authenticated(required: true) { bornIn: String """Test Description""" name: String @deprecated(reason: "Deprecated use born in") @cache(maxAge: 300) diff --git a/tests/schema_files/test_directive.graphql b/tests/schema_files/test_directive.graphql index c2aa045..162e71d 100644 --- a/tests/schema_files/test_directive.graphql +++ b/tests/schema_files/test_directive.graphql @@ -31,7 +31,7 @@ directive @link( url: String! ) on SCHEMA -union SearchResult @cache(maxAge: 500) @authenticated(required: True) = Human | Droid | Starship +union SearchResult @cache(maxAge: 500) @authenticated(required: true) = Human | Droid | Starship type Human @cache(maxAge: 60) { name: String @@ -49,39 +49,39 @@ type Starship @cache(maxAge: 20) { length: Int @deprecated(reason: "Koo") @cache(maxAge: 60) } -interface Animal @cache(maxAge: 100) @authenticated(required: True) { +interface Animal @cache(maxAge: 100) @authenticated(required: true) { age: Int! kind: Int! @cache(maxAge: 60) } -type Admin @authenticated(required: True) { +type Admin @authenticated(required: true) { name: String password: String price( currency: Int @deprecated(reason: "Use country") @hidden """Country""" - country: Int @authenticated(required: True) @hidden + country: Int @authenticated(required: true) @hidden ): String } -input HumanInput @cache(maxAge: 60) @authenticated(required: True) { +input HumanInput @cache(maxAge: 60) @authenticated(required: true) { bornIn: String """Test Description""" name: String @deprecated(reason: "Deprecated use born in") @cache(maxAge: 300) } -enum TruthEnum @cache(maxAge: 100) @authenticated(required: True) { - A @authenticated(required: True) +enum TruthEnum @cache(maxAge: 100) @authenticated(required: true) { + A @authenticated(required: true) B } -scalar DateNewScalar @cache(maxAge: 500) @authenticated(required: True) +scalar DateNewScalar @cache(maxAge: 500) @authenticated(required: true) -type User @authenticated(required: True) { +type User @authenticated(required: true) { name: String password: String - price(currency: Int @hidden, country: Int @authenticated(required: True) @hidden): String + price(currency: Int @hidden, country: Int @authenticated(required: true) @hidden): String } type Query { diff --git a/tests/schema_files/test_directive_with_repeatable.graphql b/tests/schema_files/test_directive_with_repeatable.graphql index 28b8e37..09a8d3b 100644 --- a/tests/schema_files/test_directive_with_repeatable.graphql +++ b/tests/schema_files/test_directive_with_repeatable.graphql @@ -20,7 +20,7 @@ type Query { animals: Animal @deprecated(reason: "Koo") } -interface Animal @cache(maxAge: 30) @cache(maxAge: 100) @authenticated(required: True) { +interface Animal @cache(maxAge: 30) @cache(maxAge: 100) @authenticated(required: true) { age: Int! kind: Int! @deprecated(reason: "This field is deprecated and will be removed in future") @cache(maxAge: 20) @cache(maxAge: 60) } \ No newline at end of file From 8cc5f02074b14fb1df0adb9fce271e1c4583ff17 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:06:32 +0530 Subject: [PATCH 3/7] fix: fields went missing --- graphene_directives/schema.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/graphene_directives/schema.py b/graphene_directives/schema.py index eef301b..a65ccfc 100644 --- a/graphene_directives/schema.py +++ b/graphene_directives/schema.py @@ -211,10 +211,13 @@ def add_field_decorators(self, graphene_types: set, string_schema: str) -> str: # Replace Arguments with directives if hasattr(entity_type, "_fields"): - arg_field = getattr( - entity_type._fields.args[0], # noqa - to_snake_case(field_name), - ) + _arg = entity_type._fields.args[0] # noqa + if hasattr(_arg, to_snake_case(field_name)): + arg_field = getattr(_arg, to_snake_case(field_name)) + elif hasattr(_arg, to_camel_case(field_name)): + arg_field = getattr(_arg, to_camel_case(field_name)) + else: + arg_field = {} if ( hasattr(arg_field, "args") @@ -241,6 +244,8 @@ def add_field_decorators(self, graphene_types: set, string_schema: str) -> str: graphene_type, get_field_graphene_type(field_name), None ) if field is None: + # Append the string, but skip the directives + str_fields.append(str_field) continue for directive in self.directives: From 9e4e0cba3982419b0b0405f7a33a70c63c085d95 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:20:11 +0530 Subject: [PATCH 4/7] fix: schema padding --- graphene_directives/schema.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/graphene_directives/schema.py b/graphene_directives/schema.py index a65ccfc..3a9498e 100644 --- a/graphene_directives/schema.py +++ b/graphene_directives/schema.py @@ -392,16 +392,14 @@ def get_directive_applied_field_types(self) -> set: }, } - for schema_type_name, schema_type in schema_types.items(): + for _, entity_type in schema_types.items(): if ( - not hasattr(schema_type, "graphene_type") # noqa:SIM101 - or isinstance(schema_type.graphene_type._meta, UnionOptions) # noqa - or isinstance(schema_type.graphene_type._meta, ScalarOptions) # noqa + not hasattr(entity_type, "graphene_type") # noqa:SIM101 + or isinstance(entity_type.graphene_type._meta, UnionOptions) # noqa + or isinstance(entity_type.graphene_type._meta, ScalarOptions) # noqa ): continue - entity_type = self.graphql_schema.get_type(schema_type_name) - fields = ( list(entity_type.values.values()) # Enum class fields if is_enum_type(entity_type) @@ -441,11 +439,7 @@ def get_directive_applied_field_types(self) -> set: def __str__(self): string_schema = "" string_schema += extend_schema_string(string_schema, self.schema_directives) - string_schema += print_schema(self.graphql_schema) - regex = r"schema \{(\w|\!|\s|\:)*\}" - pattern = re.compile(regex) - string_schema = pattern.sub(" ", string_schema) field_types = self.get_directive_applied_field_types() non_field_types = self.get_directive_applied_non_field_types() From 7a5831653e3a08ca8c93480e7647503b36f6d917 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:54:31 +0530 Subject: [PATCH 5/7] refact: tests --- graphene_directives/schema.py | 1 - tests/test_arg_add_definition_to_schema.py | 4 ++-- tests/test_arg_allow_all_directive_locations.py | 4 ++-- tests/test_directive.py | 4 ++-- tests/test_directive_arguments.py | 5 ++--- tests/test_directive_using_helper.py | 4 ++-- tests/test_directive_with_repeatable.py | 4 ++-- 7 files changed, 12 insertions(+), 14 deletions(-) diff --git a/graphene_directives/schema.py b/graphene_directives/schema.py index 3a9498e..4c1d5ec 100644 --- a/graphene_directives/schema.py +++ b/graphene_directives/schema.py @@ -62,7 +62,6 @@ def __init__( self.directives = directives or [] self.schema_directives = schema_directives or [] self.auto_camelcase = auto_camelcase - self.schema_directives = schema_directives or [] super().__init__( query=query, mutation=mutation, diff --git a/tests/test_arg_add_definition_to_schema.py b/tests/test_arg_add_definition_to_schema.py index aacd05f..ae70171 100644 --- a/tests/test_arg_add_definition_to_schema.py +++ b/tests/test_arg_add_definition_to_schema.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import graphene from graphql import ( @@ -16,7 +16,7 @@ directive, ) -curr_dir = os.path.dirname(os.path.realpath(__file__)) +curr_dir = Path(__file__).parent CacheDirective = CustomDirective( name="cache", diff --git a/tests/test_arg_allow_all_directive_locations.py b/tests/test_arg_allow_all_directive_locations.py index 499aa5e..98c1c49 100644 --- a/tests/test_arg_allow_all_directive_locations.py +++ b/tests/test_arg_allow_all_directive_locations.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import pytest from graphql import ( @@ -12,7 +12,7 @@ from graphene_directives import CustomDirective, DirectiveLocation from graphene_directives.exceptions import DirectiveInvalidArgTypeError -curr_dir = os.path.dirname(os.path.realpath(__file__)) +curr_dir = Path(__file__).parent def test_invalid_location_types() -> None: diff --git a/tests/test_directive.py b/tests/test_directive.py index 3130317..9f4f48e 100644 --- a/tests/test_directive.py +++ b/tests/test_directive.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path from typing import Any import graphene @@ -18,7 +18,7 @@ directive, ) -curr_dir = os.path.dirname(os.path.realpath(__file__)) +curr_dir = Path(__file__).parent def validate_non_field_input(_type: Any, inputs: dict) -> bool: diff --git a/tests/test_directive_arguments.py b/tests/test_directive_arguments.py index 89f96f3..712cd60 100644 --- a/tests/test_directive_arguments.py +++ b/tests/test_directive_arguments.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import graphene import pytest @@ -7,8 +7,7 @@ from graphene_directives import CustomDirective, DirectiveLocation, directive from graphene_directives.exceptions import DirectiveInvalidArgValueTypeError -curr_dir = os.path.dirname(os.path.realpath(__file__)) - +curr_dir = Path(__file__).parent CacheDirective = CustomDirective( name="cache", diff --git a/tests/test_directive_using_helper.py b/tests/test_directive_using_helper.py index 3dd7fb2..4fac171 100644 --- a/tests/test_directive_using_helper.py +++ b/tests/test_directive_using_helper.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import graphene from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString @@ -10,7 +10,7 @@ directive_decorator, ) -curr_dir = os.path.dirname(os.path.realpath(__file__)) +curr_dir = Path(__file__).parent CacheDirective = CustomDirective( name="cache", diff --git a/tests/test_directive_with_repeatable.py b/tests/test_directive_with_repeatable.py index 8d2136a..64fd154 100644 --- a/tests/test_directive_with_repeatable.py +++ b/tests/test_directive_with_repeatable.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import graphene import pytest @@ -18,7 +18,7 @@ directive, ) -curr_dir = os.path.dirname(os.path.realpath(__file__)) +curr_dir = Path(__file__).parent CacheDirective = CustomDirective( From 5126ea3b05c39c77cdc8a2ab385827f9d76eae25 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sat, 13 Jan 2024 20:30:34 +0530 Subject: [PATCH 6/7] feat: add helper for getting used_directives used and validation for unique directive names --- graphene_directives/main.py | 6 ++++++ graphene_directives/schema.py | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/graphene_directives/main.py b/graphene_directives/main.py index 561b884..64c7af2 100644 --- a/graphene_directives/main.py +++ b/graphene_directives/main.py @@ -28,6 +28,12 @@ def build_schema( else: _schema_directive_set.add(schema_directive.target_directive.name) + _directive_set: set[str] = set() + for directive in directives or []: + if directive.name in _directive_set: + raise DirectiveValidationError(f"Duplicate {directive} found") + _directive_set.add(directive.name) + return Schema( query=query, mutation=mutation, diff --git a/graphene_directives/schema.py b/graphene_directives/schema.py index 4c1d5ec..949b5c0 100644 --- a/graphene_directives/schema.py +++ b/graphene_directives/schema.py @@ -62,6 +62,7 @@ def __init__( self.directives = directives or [] self.schema_directives = schema_directives or [] self.auto_camelcase = auto_camelcase + self.directives_used: dict[str, GraphQLDirective] = {} super().__init__( query=query, mutation=mutation, @@ -360,9 +361,7 @@ def add_non_field_decorators( def get_directive_applied_non_field_types(self) -> set: """ - Find all the extended types from the schema. - They can be easily distinguished from the other type as - the `@directive` decorator adds a `_{name}` attribute to them. + Find all the directive applied non-field types from the schema. """ directives_types = set() schema_types = { @@ -378,10 +377,14 @@ def get_directive_applied_non_field_types(self) -> set: continue for directive in self.directives: if has_non_field_attribute(schema_type.graphene_type, directive): + self.directives_used[directive.name] = directive directives_types.add(schema_type.graphene_type) return directives_types def get_directive_applied_field_types(self) -> set: + """ + Find all the directive applied field types from the schema. + """ directives_fields = set() schema_types = { **self.graphql_schema.type_map, @@ -414,6 +417,7 @@ def get_directive_applied_field_types(self) -> set: ) for directive_ in self.directives: if has_field_attribute(field_type, directive_): + self.directives_used[directive_.name] = directive_ directives_fields.add(entity_type.graphene_type) # Handle Argument Decorators @@ -431,10 +435,19 @@ def get_directive_applied_field_types(self) -> set: raise DirectiveValidationError( f"{directive_} cannot be used at argument level at {entity_type}->{field}" ) + self.directives_used[directive_.name] = directive_ directives_fields.add(entity_type.graphene_type) return directives_fields + def get_directives_used(self) -> list[GraphQLDirective]: + """ + Returns a list of directives used in the schema + """ + self.get_directive_applied_field_types() + self.get_directive_applied_non_field_types() + return list(self.directives_used.values()) + def __str__(self): string_schema = "" string_schema += extend_schema_string(string_schema, self.schema_directives) From e7efbdd336fe29aafb14ec6bb8ba500219df5b8c Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sat, 13 Jan 2024 21:02:56 +0530 Subject: [PATCH 7/7] bump: 0.4.0 --- README.md | 18 ++++++++++++++++-- pyproject.toml | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f6bcac3..6b533b9 100644 --- a/README.md +++ b/README.md @@ -154,12 +154,25 @@ from graphql import ( from graphene_directives import CustomDirective, DirectiveLocation, ValidatorLocation, build_schema, directive_decorator -def validate_input(_type: Any, _location_type: ValidatorLocation, inputs: dict) -> bool: +def validate_non_field_input(_type: Any, inputs: dict) -> bool: + """ + def validator (type_: graphene type, inputs: Any) -> bool, + if validator returns False, library raises DirectiveCustomValidationError + """ if inputs.get("max_age") > 2500: return False return True +def validate_field_input(_parent_type: Any, _field_type: Any, inputs: dict) -> bool: + """ + def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any) -> bool, + if validator returns False, library raises DirectiveCustomValidationError + """ + if inputs.get("max_age") > 2500: + return False + return True + CacheDirective = CustomDirective( name="cache", locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT], @@ -176,7 +189,8 @@ CacheDirective = CustomDirective( ), }, description="Caching directive to control cache behavior of fields or fragments.", - non_field_validator=validate_input, + non_field_validator=validate_non_field_input, + field_validator=validate_field_input, ) # This returns a partial of directive function diff --git a/pyproject.toml b/pyproject.toml index 87b0f71..fd3e4a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "graphene-directives" -version = "0.3.3" +version = "0.4.0" packages = [{include = "graphene_directives"}] description = "Schema Directives implementation for graphene" authors = ["Strollby "]