diff --git a/docs/requirements.txt b/docs/requirements.txt index dcc403123..e94d216b7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,6 @@ # Required library Sphinx==1.5.3 +jinja2<3.1.0 sphinx-autobuild==0.7.1 # Docs template http://graphene-python.org/sphinx_graphene_theme.zip diff --git a/docs/types/schema.rst b/docs/types/schema.rst index a82addc9a..e7d2c101e 100644 --- a/docs/types/schema.rst +++ b/docs/types/schema.rst @@ -92,3 +92,153 @@ To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema in query=MyRootQuery, auto_camelcase=False, ) + +.. _SchemaTypeNamePrefix: + +Type name prefix +-------------------------- + +You can specify a prefix for all type names in the schema by setting the ``type_name_prefix`` argument upon schema instantiation: + +.. code:: python + + my_schema = Schema( + query=MyRootQuery, + mutation=MyRootMutation, + subscription=MyRootSubscription + type_name_prefix='MyPrefix', + ) + +This is useful in a micro-services architecture to prepend the service name to all types and avoid conflicts for example. + +The prefix will be added to the name of: + +* Query / Mutation / Subscription +* Scalar +* ObjectType +* InputType +* Enum +* Interface +* Union + +While fields and arguments name will be left untouched. + +More specifically, the following schema: + +.. code:: + + type Query { + inner: MyType + } + + type MyType { + field: String + myUnion: MyUnion + myBarType: MyBarType + myFooType: MyFooType + } + + union MyUnion = MyBarType | MyFooType + + type MyBarType { + field(input: MyInputObjectType): String + myInterface: MyInterface + } + + input MyInputObjectType { + field: String + } + + interface MyInterface { + field: String + } + + type MyFooType { + field: String + myEnum: MyEnum + } + + scalar MyScalar + + enum MyEnum { + FOO + BAR + } + + type Mutation { + createUser(name: String): CreateUser + } + + type CreateUser { + name: String + } + + type Subscription { + countToTen: Int + } + +Will be transformed to: + +.. code:: + + type Query { + myPrefixInner: MyPrefixMyType + } + + type MyPrefixMyType { + field: String + myUnion: MyPrefixMyUnion + myBarType: MyPrefixMyBarType + myFooType: MyPrefixMyFooType + } + + union MyPrefixMyUnion = MyPrefixMyBarType | MyPrefixMyFooType + + type MyPrefixMyBarType { + field(input: MyPrefixMyInputObjectType): String + myInterface: MyPrefixMyInterface + } + + input MyPrefixMyInputObjectType { + field: String + } + + interface MyPrefixMyInterface { + field: String + } + + type MyPrefixMyFooType { + field: String + myEnum: MyPrefixMyEnum + } + + scalar MyPrefixMyScalar + + enum MyPrefixMyEnum { + FOO + BAR + } + + type Mutation { + myPrefixCreateUser(name: String): MyPrefixCreateUser + } + + type MyPrefixCreateUser { + name: String + } + + type Subscription { + myPrefixCountToTen: Int + } + +You can override the prefix for a specific type by setting the ``type_name_prefix`` property on the ``Meta`` class: + +.. code:: python + + from graphene import ObjectType + + class MyGraphQlType(ObjectType): + class Meta: + type_name_prefix = '' + +This is useful when defining external types in a federated schema for example. diff --git a/graphene/types/base.py b/graphene/types/base.py index 84cb377a2..451851e0f 100644 --- a/graphene/types/base.py +++ b/graphene/types/base.py @@ -36,13 +36,14 @@ def create_type(cls, class_name, **options): @classmethod def __init_subclass_with_meta__( - cls, name=None, description=None, _meta=None, **_kwargs + cls, name=None, description=None, type_name_prefix=None, _meta=None, **_kwargs ): assert "_meta" not in cls.__dict__, "Can't assign meta directly" if not _meta: return _meta.name = name or cls.__name__ _meta.description = description or trim_docstring(cls.__doc__) + _meta.type_name_prefix = type_name_prefix _meta.freeze() cls._meta = _meta super(BaseType, cls).__init_subclass_with_meta__() diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 1a33a93d3..4819d76ab 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -49,7 +49,7 @@ from .scalars import ID, Boolean, Float, Int, Scalar, String from .structures import List, NonNull from .union import Union -from .utils import get_field_as +from .utils import get_field_as, get_type_name introspection_query = get_introspection_query() IntrospectionSchema = introspection_types["__Schema"] @@ -91,6 +91,7 @@ def __init__( subscription=None, types=None, auto_camelcase=True, + type_name_prefix=None, ): assert_valid_root_type(query) assert_valid_root_type(mutation) @@ -101,9 +102,18 @@ def __init__( assert is_graphene_type(type_) self.auto_camelcase = auto_camelcase + self.type_name_prefix = type_name_prefix create_graphql_type = self.add_type + self.root_type_names = [] + if query: + self.root_type_names.append(query._meta.name) + if mutation: + self.root_type_names.append(mutation._meta.name) + if subscription: + self.root_type_names.append(subscription._meta.name) + self.query = create_graphql_type(query) if query else None self.mutation = create_graphql_type(mutation) if mutation else None self.subscription = create_graphql_type(subscription) if subscription else None @@ -131,9 +141,9 @@ def add_type(self, graphene_type): elif issubclass(graphene_type, Interface): graphql_type = self.create_interface(graphene_type) elif issubclass(graphene_type, Scalar): - graphql_type = self.create_scalar(graphene_type) + graphql_type = self.create_scalar(graphene_type, self.type_name_prefix) elif issubclass(graphene_type, Enum): - graphql_type = self.create_enum(graphene_type) + graphql_type = self.create_enum(graphene_type, self.type_name_prefix) elif issubclass(graphene_type, Union): graphql_type = self.construct_union(graphene_type) else: @@ -142,7 +152,10 @@ def add_type(self, graphene_type): return graphql_type @staticmethod - def create_scalar(graphene_type): + def create_scalar( + graphene_type, + type_name_prefix=None, + ): # We have a mapping to the original GraphQL types # so there are no collisions. _scalars = { @@ -157,7 +170,7 @@ def create_scalar(graphene_type): return GrapheneScalarType( graphene_type=graphene_type, - name=graphene_type._meta.name, + name=get_type_name(graphene_type, type_name_prefix), description=graphene_type._meta.description, serialize=getattr(graphene_type, "serialize", None), parse_value=getattr(graphene_type, "parse_value", None), @@ -165,7 +178,7 @@ def create_scalar(graphene_type): ) @staticmethod - def create_enum(graphene_type): + def create_enum(graphene_type, type_name_prefix=None): values = {} for name, value in graphene_type._meta.enum.__members__.items(): description = getattr(value, "description", None) @@ -193,7 +206,7 @@ def create_enum(graphene_type): return GrapheneEnumType( graphene_type=graphene_type, values=values, - name=graphene_type._meta.name, + name=get_type_name(graphene_type, type_name_prefix), description=type_description, ) @@ -215,9 +228,14 @@ def interfaces(): else: is_type_of = graphene_type.is_type_of + if graphene_type._meta.name in self.root_type_names: + name = graphene_type._meta.name + else: + name = self.get_type_name(graphene_type) + return GrapheneObjectType( graphene_type=graphene_type, - name=graphene_type._meta.name, + name=name, description=graphene_type._meta.description, fields=partial(self.create_fields_for_type, graphene_type), is_type_of=is_type_of, @@ -243,7 +261,7 @@ def interfaces(): return GrapheneInterfaceType( graphene_type=graphene_type, - name=graphene_type._meta.name, + name=self.get_type_name(graphene_type), description=graphene_type._meta.description, fields=partial(self.create_fields_for_type, graphene_type), interfaces=interfaces, @@ -253,7 +271,7 @@ def interfaces(): def create_inputobjecttype(self, graphene_type): return GrapheneInputObjectType( graphene_type=graphene_type, - name=graphene_type._meta.name, + name=self.get_type_name(graphene_type), description=graphene_type._meta.description, out_type=graphene_type._meta.container, fields=partial( @@ -282,7 +300,7 @@ def types(): return GrapheneUnionType( graphene_type=graphene_type, - name=graphene_type._meta.name, + name=self.get_type_name(graphene_type), description=graphene_type._meta.description, types=types, resolve_type=resolve_type, @@ -357,7 +375,10 @@ def create_fields_for_type(self, graphene_type, is_input_type=False): deprecation_reason=field.deprecation_reason, description=field.description, ) - field_name = field.name or self.get_name(name) + if field.name: + field_name = field.name + else: + field_name = self.get_field_name(graphene_type, name) fields[field_name] = _field return fields @@ -391,6 +412,26 @@ def resolve_type(self, resolve_type_func, type_name, root, info, _type): return_type = self[type_name] return default_type_resolver(root, info, return_type) + def get_type_name(self, graphene_type): + return get_type_name(graphene_type, self.type_name_prefix) + + def get_field_name(self, graphene_type, name): + if graphene_type._meta.name in self.root_type_names: + # We only add the prefix to the root types and types defined prefixes take precedence + # over schema defined prefix. + type_name_prefix = ( + graphene_type._meta.type_name_prefix + if graphene_type._meta.type_name_prefix is not None + else self.type_name_prefix + ) + if type_name_prefix: + if self.auto_camelcase: + return to_camel_case( + type_name_prefix[0].lower() + type_name_prefix[1:] + "_" + name + ) + return type_name_prefix + name + return self.get_name(name) + class Schema: """Schema Definition. @@ -421,12 +462,18 @@ def __init__( types=None, directives=None, auto_camelcase=True, + type_name_prefix=None, ): self.query = query self.mutation = mutation self.subscription = subscription type_map = TypeMap( - query, mutation, subscription, types, auto_camelcase=auto_camelcase + query, + mutation, + subscription, + types, + auto_camelcase=auto_camelcase, + type_name_prefix=type_name_prefix, ) self.graphql_schema = GraphQLSchema( type_map.query, diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 4a7ad3c7c..1f13109c2 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -218,3 +218,48 @@ class Query(ObjectType): ) assert not result.errors assert result.data == {"createUserWithPlanet": {"name": "Peter", "planet": "earth"}} + + +def test_type_name_prefix(): + class BaseCreateUser(Mutation): + class Arguments: + name = String() + + name = String() + + def mutate(self, info, **args): + return args + + class CreateUserWithPlanet(BaseCreateUser): + class Arguments(BaseCreateUser.Arguments): + planet = String() + + planet = String() + + def mutate(self, info, **args): + return CreateUserWithPlanet(**args) + + class MyMutation(ObjectType): + create_user_with_planet = CreateUserWithPlanet.Field() + + class Query(ObjectType): + a = String() + + schema = Schema( + query=Query, + mutation=MyMutation, + type_name_prefix="MyPrefix", + ) + result = schema.execute( + """ mutation mymutation { + myPrefixCreateUserWithPlanet(name:"Peter", planet: "earth") { + name + planet + } + } + """ + ) + assert not result.errors + assert result.data == { + "myPrefixCreateUserWithPlanet": {"name": "Peter", "planet": "earth"} + } diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index e117754fe..e808750b0 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -497,3 +497,28 @@ def resolve_user(self, *args, **kwargs): assert not result.errors assert result.data == expected + + +def test_type_name_prefix(): + class Cat(ObjectType): + name = String() + + class User(ObjectType): + name = String() + cat = Field(Cat) + + def resolve_cat(self, *args, **kwargs): + return Cat(name="bar") + + class Query(ObjectType): + user = Field(User) + + def resolve_user(self, *args, **kwargs): + return User(name="foo") + + schema = Schema(query=Query, type_name_prefix="MyPrefix") + expected = {"myPrefixUser": {"name": "foo", "cat": {"name": "bar"}}} + result = schema.execute("{ myPrefixUser { name cat { name } } }") + + assert not result.errors + assert result.data == expected diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index c03c81ba0..22941970f 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -5,17 +5,76 @@ from graphql.type import GraphQLObjectType, GraphQLSchema from ..field import Field +from ..enum import Enum +from ..inputobjecttype import InputObjectType +from ..interface import Interface +from ..mutation import Mutation from ..objecttype import ObjectType -from ..scalars import String +from ..scalars import Int, String, Scalar from ..schema import Schema +from ..union import Union -class MyOtherType(ObjectType): +class MyInputObjectType(InputObjectType): field = String() +class MyScalar(Scalar): + ... + + +class MyEnum(Enum): + FOO = "foo" + BAR = "bar" + + +class MyInterface(Interface): + field = String() + + +class MyBarType(ObjectType): + field = String(input=MyInputObjectType()) + my_interface = Field(MyInterface) + + +class MyFooType(ObjectType): + field = String() + my_scalar = MyScalar() + my_enum = MyEnum() + + +class MyUnion(Union): + class Meta: + types = (MyBarType, MyFooType) + + +class MyType(ObjectType): + field = String() + my_union = MyUnion() + my_bar_type = Field(MyBarType) + my_foo_type = Field("graphene.types.tests.test_schema.MyFooType") + + class Query(ObjectType): - inner = Field(MyOtherType) + inner = Field(MyType) + + +class CreateUser(Mutation): + class Arguments: + name = String() + + name = String() + + def mutate(self, info, name): + return CreateUser(name=name) + + +class Mutation(ObjectType): + create_user = CreateUser.Field() + + +class Subscription(ObjectType): + count_to_ten = Field(Int) def test_schema(): @@ -31,7 +90,12 @@ def test_schema(): def test_schema_get_type(): schema = Schema(Query) assert schema.Query == Query - assert schema.MyOtherType == MyOtherType + assert schema.MyType == MyType + assert schema.MyBarType == MyBarType + assert schema.MyFooType == MyFooType + assert schema.MyInputObjectType == MyInputObjectType + assert schema.MyInterface == MyInterface + assert schema.MyEnum == MyEnum def test_schema_get_type_error(): @@ -49,11 +113,42 @@ def test_schema_str(): == dedent( """ type Query { - inner: MyOtherType + inner: MyType + } + + type MyType { + field: String + myUnion: MyUnion + myBarType: MyBarType + myFooType: MyFooType + } + + union MyUnion = MyBarType | MyFooType + + type MyBarType { + field(input: MyInputObjectType): String + myInterface: MyInterface + } + + input MyInputObjectType { + field: String + } + + interface MyInterface { + field: String } - type MyOtherType { + type MyFooType { field: String + myScalar: MyScalar + myEnum: MyEnum + } + + scalar MyScalar + + enum MyEnum { + FOO + BAR } """ ).strip() diff --git a/graphene/types/tests/test_type_name_prefix.py b/graphene/types/tests/test_type_name_prefix.py new file mode 100644 index 000000000..49cb9b0a3 --- /dev/null +++ b/graphene/types/tests/test_type_name_prefix.py @@ -0,0 +1,352 @@ +from textwrap import dedent + +from ..field import Field +from ..enum import Enum +from ..inputobjecttype import InputObjectType +from ..interface import Interface +from ..mutation import Mutation as Mutation_ +from ..objecttype import ObjectType +from ..scalars import Int, String, Scalar +from ..schema import Schema +from ..union import Union + + +class MyInputObjectType(InputObjectType): + field = String() + + +class MyScalar(Scalar): + ... + + +class MyEnum(Enum): + FOO = "foo" + BAR = "bar" + + +class MyInterface(Interface): + field = String() + + +class MyBarType(ObjectType): + field = String(input=MyInputObjectType()) + my_interface = Field(MyInterface) + my_scalar = MyScalar() + my_enum = MyEnum() + + +class MyFooType(ObjectType): + field = String() + + +class MyUnion(Union): + class Meta: + types = (MyBarType, MyFooType) + + +class MyType(ObjectType): + field = String() + my_union = MyUnion() + my_bar_type = Field(MyBarType) + + +class Query(ObjectType): + inner = Field(MyType) + + +class CreateUser(Mutation_): + class Arguments: + name = String() + + name = String() + + def mutate(self, info, name): + return CreateUser(name=name) + + +class Mutation(ObjectType): + create_user = CreateUser.Field() + + +class Subscription(ObjectType): + count_to_ten = Field(Int) + + +def test_schema_type_name_prefix_camelcase(): + + schema = Schema( + Query, + Mutation, + Subscription, + auto_camelcase=True, + type_name_prefix="MyPrefix", + ) + assert ( + str(schema).strip() + == dedent( + """ + type Query { + myPrefixInner: MyPrefixMyType + } + + type MyPrefixMyType { + field: String + myUnion: MyPrefixMyUnion + myBarType: MyPrefixMyBarType + } + + union MyPrefixMyUnion = MyPrefixMyBarType | MyPrefixMyFooType + + type MyPrefixMyBarType { + field(input: MyPrefixMyInputObjectType): String + myInterface: MyPrefixMyInterface + myScalar: MyPrefixMyScalar + myEnum: MyPrefixMyEnum + } + + input MyPrefixMyInputObjectType { + field: String + } + + interface MyPrefixMyInterface { + field: String + } + + scalar MyPrefixMyScalar + + enum MyPrefixMyEnum { + FOO + BAR + } + + type MyPrefixMyFooType { + field: String + } + + type Mutation { + myPrefixCreateUser(name: String): MyPrefixCreateUser + } + + type MyPrefixCreateUser { + name: String + } + + type Subscription { + myPrefixCountToTen: Int + } + """ + ).strip() + ) + + +def test_schema_type_name_prefix_camelcase_disabled(): + schema = Schema( + Query, + Mutation, + Subscription, + auto_camelcase=False, + type_name_prefix="MyPrefix", + ) + assert ( + str(schema).strip() + == dedent( + """ + type Query { + MyPrefixinner: MyPrefixMyType + } + + type MyPrefixMyType { + field: String + my_union: MyPrefixMyUnion + my_bar_type: MyPrefixMyBarType + } + + union MyPrefixMyUnion = MyPrefixMyBarType | MyPrefixMyFooType + + type MyPrefixMyBarType { + field(input: MyPrefixMyInputObjectType): String + my_interface: MyPrefixMyInterface + my_scalar: MyPrefixMyScalar + my_enum: MyPrefixMyEnum + } + + input MyPrefixMyInputObjectType { + field: String + } + + interface MyPrefixMyInterface { + field: String + } + + scalar MyPrefixMyScalar + + enum MyPrefixMyEnum { + FOO + BAR + } + + type MyPrefixMyFooType { + field: String + } + + type Mutation { + MyPrefixcreate_user(name: String): MyPrefixCreateUser + } + + type MyPrefixCreateUser { + name: String + } + + type Subscription { + MyPrefixcount_to_ten: Int + } + """ + ).strip() + ) + + +def test_schema_type_name_prefix_override(): + class MyInputObjectType(InputObjectType): + class Meta: + type_name_prefix = "OverridePrefix" + + field = String() + + class MyScalar(Scalar): + class Meta: + type_name_prefix = "" + + class MyEnum(Enum): + class Meta: + type_name_prefix = "" + + FOO = "foo" + BAR = "bar" + + class MyInterface(Interface): + class Meta: + type_name_prefix = "" + + field = String() + + class MyBarType(ObjectType): + class Meta: + type_name_prefix = "" + + field = String(input=MyInputObjectType()) + my_interface = Field(MyInterface) + my_scalar = MyScalar() + my_enum = MyEnum() + + class MyFooType(ObjectType): + class Meta: + type_name_prefix = "" + + field = String() + + class MyUnion(Union): + class Meta: + type_name_prefix = "" + types = (MyBarType, MyFooType) + + class MyType(ObjectType): + class Meta: + type_name_prefix = "" + + field = String() + my_union = MyUnion() + my_bar_type = Field(MyBarType) + + class Query(ObjectType): + class Meta: + type_name_prefix = "OverridePrefix" + + inner = Field(MyType) + + class CreateUser(Mutation_): + class Meta: + type_name_prefix = "" + + class Arguments: + name = String() + + name = String() + + def mutate(self, info, name): + return CreateUser(name=name) + + class Mutation(ObjectType): + class Meta: + type_name_prefix = "" + + create_user = CreateUser.Field() + + class Subscription(ObjectType): + class Meta: + type_name_prefix = "" + + count_to_ten = Field(Int) + + schema = Schema( + Query, + Mutation, + Subscription, + auto_camelcase=True, + type_name_prefix="MyPrefix", + ) + assert ( + str(schema).strip() + == dedent( + """ + type Query { + overridePrefixInner: MyType + } + + type MyType { + field: String + myUnion: MyUnion + myBarType: MyBarType + } + + union MyUnion = MyBarType | MyFooType + + type MyBarType { + field(input: OverridePrefixMyInputObjectType): String + myInterface: MyInterface + myScalar: MyScalar + myEnum: MyEnum + } + + input OverridePrefixMyInputObjectType { + field: String + } + + interface MyInterface { + field: String + } + + scalar MyScalar + + enum MyEnum { + FOO + BAR + } + + type MyFooType { + field: String + } + + type Mutation { + createUser(name: String): CreateUser + } + + type CreateUser { + name: String + } + + type Subscription { + countToTen: Int + } + """ + ).strip() + ) diff --git a/graphene/types/utils.py b/graphene/types/utils.py index 1976448aa..e609e6a28 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -48,3 +48,18 @@ def get_underlying_type(_type): while hasattr(_type, "of_type"): _type = _type.of_type return _type + + +def get_type_name(graphene_type, type_name_prefix): + type_name_prefix = ( + graphene_type._meta.type_name_prefix + if graphene_type._meta.type_name_prefix is not None + else type_name_prefix + ) + if type_name_prefix: + return ( + type_name_prefix[0].upper() + + type_name_prefix[1:] + + graphene_type._meta.name + ) + return graphene_type._meta.name diff --git a/setup.py b/setup.py index dce6aa6c0..243b6bae6 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def run_tests(self): "coveralls>=3.3,<4", "promise>=2.3,<3", "mock>=4,<5", - "pytz==2022.1", + "pytz==2022.5", "iso8601>=1,<2", ]