Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added object_type_name_prefix #1466

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Required library
Sphinx==1.5.3
jinja2<3.1.0
tcleonard marked this conversation as resolved.
Show resolved Hide resolved
sphinx-autobuild==0.7.1
# Docs template
http://graphene-python.org/sphinx_graphene_theme.zip
133 changes: 133 additions & 0 deletions docs/types/schema.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,136 @@ 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
* ObjectType
* InputType
* Interface
* Union
* Enum

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
}

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
}

enum MyPrefixMyEnum {
FOO
BAR
}

type Mutation {
myPrefixCreateUser(name: String): MyPrefixCreateUser
}

type MyPrefixCreateUser {
name: String
}

type Subscription {
myPrefixCountToTen: Int
}
61 changes: 52 additions & 9 deletions graphene/types/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -164,8 +174,7 @@ def create_scalar(graphene_type):
parse_literal=getattr(graphene_type, "parse_literal", None),
tcleonard marked this conversation as resolved.
Show resolved Hide resolved
)

@staticmethod
erikwrede marked this conversation as resolved.
Show resolved Hide resolved
def create_enum(graphene_type):
def create_enum(self, graphene_type):
values = {}
for name, value in graphene_type._meta.enum.__members__.items():
description = getattr(value, "description", None)
Expand Down Expand Up @@ -193,7 +202,7 @@ def create_enum(graphene_type):
return GrapheneEnumType(
graphene_type=graphene_type,
values=values,
name=graphene_type._meta.name,
name=self.add_prefix_to_type_name(graphene_type._meta.name),
description=type_description,
)

Expand All @@ -215,9 +224,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.add_prefix_to_type_name(graphene_type._meta.name)

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,
Expand All @@ -243,7 +257,7 @@ def interfaces():

return GrapheneInterfaceType(
graphene_type=graphene_type,
name=graphene_type._meta.name,
name=self.add_prefix_to_type_name(graphene_type._meta.name),
description=graphene_type._meta.description,
fields=partial(self.create_fields_for_type, graphene_type),
interfaces=interfaces,
Expand All @@ -253,7 +267,7 @@ def interfaces():
def create_inputobjecttype(self, graphene_type):
return GrapheneInputObjectType(
graphene_type=graphene_type,
name=graphene_type._meta.name,
name=self.add_prefix_to_type_name(graphene_type._meta.name),
description=graphene_type._meta.description,
out_type=graphene_type._meta.container,
fields=partial(
Expand Down Expand Up @@ -282,7 +296,7 @@ def types():

return GrapheneUnionType(
graphene_type=graphene_type,
name=graphene_type._meta.name,
name=self.add_prefix_to_type_name(graphene_type._meta.name),
description=graphene_type._meta.description,
types=types,
resolve_type=resolve_type,
Expand Down Expand Up @@ -357,7 +371,13 @@ 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:
if graphene_type._meta.name in self.root_type_names:
field_name = self.add_prefix_to_field_name(name)
else:
field_name = self.get_name(name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not completely sure but I think that even if the name comes from field.name we want to add the prefix... would be good to add a unit test for this.

Suggested change
if field.name:
field_name = field.name
else:
if graphene_type._meta.name in self.root_type_names:
field_name = self.add_prefix_to_field_name(name)
else:
field_name = self.get_name(name)
field_name = field.name or self.get_name(name)
if graphene_type._meta.name in self.root_type_names:
field_name = self.add_prefix_to_field_name(name)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking again at the code I see that we bypass auto_camelcase when the field name is explicitly defined, shouldn't we expect the same for type_name_prefix ?

I'm not sure neither to be honest

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I saw that we bypass the auto camel case but I am not sure it makes sense to remove the prefixing... 🤷‍♂️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this really be part of type_name_prefix, or should we rather add a meta option field_name_prefix to ObjectType? Covers more use cases than this.

The drawback is having to configure the same prefix in different places. But we are more exact as the type_name_prefix only applies to type names, not to fields.

If we go for field_name_prefix, I agree with @tcleonard to add the prefix to all field names.
If we go for type_name_prefixes I am not sure either.

fields[field_name] = _field
return fields

Expand Down Expand Up @@ -391,6 +411,23 @@ 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 add_prefix_to_type_name(self, name):
if self.type_name_prefix:
return self.type_name_prefix[0].upper() + self.type_name_prefix[1:] + name
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to go from camelCase to PascalCase as per convention for type names.

Ideally we'd need a auto_pascalcase argument for Schema to control this like we do with auto_camelcase.

It feels a bit overkill though, any thoughts on this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return self.type_name_prefix[0].upper() + self.type_name_prefix[1:] + name
return self.type_name_prefix.capitalize() + name

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that at first actually, but:

image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmmm that's a fair point! (that also means that if we have auto camel case on and we have my_fieldIsGreat = Field(...) it would become myFieldisgreat because it does use .capitalize()... should we change that @erikwrede in your opinion?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcleonard great catch! I'm not sure about changing that since my_fieldIsGreat is invalid snake case and the contract for auto_camelcase is to transform valid snake_case to PascalCase. So the specified behavior is currently 'undefined'. Would definitely be a breaking change, so we should ask for more opinions on Slack.

For prefixes, we should expect the use of proper PascalCase.

return name

def add_prefix_to_field_name(self, name):
if self.type_name_prefix:
if self.auto_camelcase:
return self.get_name(
self.type_name_prefix[0].lower()
+ self.type_name_prefix[1:]
+ "_"
+ name
)
return self.get_name(self.type_name_prefix + name)
return self.get_name(name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is a need to deal with the auto_camelcase case here as it is already dealt with in the get_name.

Suggested change
def add_prefix_to_field_name(self, name):
if self.type_name_prefix:
if self.auto_camelcase:
return self.get_name(
self.type_name_prefix[0].lower()
+ self.type_name_prefix[1:]
+ "_"
+ name
)
return self.get_name(self.type_name_prefix + name)
return self.get_name(name)
def add_prefix_to_field_name(self, name):
if self.type_name_prefix:
return self.get_name(
self.type_name_prefix[0].lower()
+ self.type_name_prefix[1:]
+ "_"
+ name
)
return self.get_name(name)

also note that in your current implementation where you don't lower the first character in the case we are not in auto camel case means the output is not exactly what you give in the doc:

    type Query {
        myPrefixInner: MyPrefixMyType
    }

indeed that would be MyPrefixInner...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed that would be MyPrefixInner...

By default auto_camelcase=True so the query name would be myPrefixInner indeed isn't it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but my point was if auto_camelcase is:

  • True -> myPrefixInner
  • False -> MyPrefixInner
    which doesn't seem very intuitive... no?

Copy link
Author

@superlevure superlevure Oct 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, what would be more intuitive in your view?

Because

  • MyPrefixInner (PascalCase) -> auto_camelcase=True -> myPrefixInner (camelCase)
  • MyPrefixInner (PascalCase) -> auto_camelcase=False -> MyPrefixInner (PascalCase)

Feels the most natural to me 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gentle ping on that one @tcleonard



class Schema:
"""Schema Definition.
Expand Down Expand Up @@ -421,12 +458,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,
Expand Down
45 changes: 45 additions & 0 deletions graphene/types/tests/test_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
}
25 changes: 25 additions & 0 deletions graphene/types/tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading