Skip to main content

Ariadne Codegen 0.7

· 6 min read

Ariadne Codegen 0.7 is now available!

This release brings support for subscriptions, changes how fragments are represented in generated code, introduces the ShorterResultsPlugin plugin developed by our amazing community, and more features and fixes.

Subscriptions

Version 0.7 introduces support for subscriptions. We generate them as async generators, which means that we don't support subscriptions when the generated client is not async (async_client is set to false).

For example, given the following operation:

subscription GetUsersCounter {
usersCounter
}

Generated client will have following method:

    async def get_users_counter(self) -> AsyncIterator[GetUsersCounter]:
query = gql(
"""
subscription GetUsersCounter {
usersCounter
}
"""
)
variables: dict[str, object] = {}
async for data in self.execute_ws(query=query, variables=variables):
yield GetUsersCounter.parse_obj(data)

Our default async base client uses websockets package and implements graphql-transport-ws subprotocol.

Required dependencies can be installed with pip:

$ pip install ariadne-codegen[subscriptions]

Fragments

In previous versions of Codegen fragments were "unpacked" in queries. For example, given the following operations:

query GetA {
getTypeA {
...FragmentA
}
}

query ListA {
listTypeA {
...FragmentA
}
}

fragment FragmentA on TypeA {
id
name
}

Generated get_a.py and list_a.py files had types looking like this:

# get_a.py

class GetA(BaseModel):
get_type_a: "GetAGetTypeA" = Field(alias="GetTypeA")


class GetAGetTypeA(BaseModel):
id: str
name: str
# list_a.py

class ListA(BaseModel):
list_type_a: List["ListAListTypeA"] = Field(alias="ListTypeA")


class ListAListTypeA(BaseModel):
id: str
name: str

Both of these operations use the same FragmentA to represent TypeA, but generated models didn't reflect that.

To make working with fragments easier, in Ariadne Codegen 0.7 we are changing this behavior. Instead of unpacking fragments, we generate separate models from them and use those as mixins. The above operation will now result in 3 files being generated: get_a.py, list_a.py, and fragments.py

# get_a.py

class GetA(BaseModel):
get_type_a: "GetAGetTypeA" = Field(alias="GetTypeA")


class GetAGetTypeA(FragmentA):
pass
# list_a.py

class ListA(BaseModel):
list_type_a: List["ListAListTypeA"] = Field(alias="ListTypeA")


class ListAListTypeA(FragmentA):
pass
# fragments.py

class FragmentA(BaseModel):
id: str
name: str

With this change you can use fragments as reusable types in your Python logic using the client, eg. def process_a(a: FragmentA).... New fragments.py consists of fragments collected from all parsed operations.

Unions and Interfaces

There is an exception from new fragments behaviour. If a fragment represents Union then we unpack it as before:

query getAnimal {
animal {
...AnimalData
}
}

fragment AnimalData on AnimalInterface {
name
... on Dog {
dogField
}
... on Cat {
catField
}
}

For the above fragment, this Python code will be generated:

class GetAnimal(BaseModel):
animal: Union[
"GetAnimalAnimalAnimalInterface", "GetAnimalAnimalDog", "GetAnimalAnimalCat"
] = Field(discriminator="typename__")


class GetAnimalAnimalAnimalInterface(BaseModel):
typename__: Literal["AnimalInterface", "Fish"] = Field(alias="__typename")
name: str


class GetAnimalAnimalDog(BaseModel):
typename__: Literal["Dog"] = Field(alias="__typename")
name: str
dog_field: str = Field(alias="dogField")


class GetAnimalAnimalCat(BaseModel):
typename__: Literal["Cat"] = Field(alias="__typename")
name: str
cat_field: str = Field(alias="catField")

ShorterResultsPlugin

In version 0.7 we are including ShorterResultsPlugin developed by our community. It can be used when operations have only one top-level field. For example, given the following operation:

query GetUser($userId: ID!) {
user(id: $userId) {
id
}
}

From this operation, the generated method looks like this:

async def get_user(self, user_id: str) -> GetUser:
query = gql(
"""
query GetUser($userId: ID!) {
user(id: $userId) {
id
}
}
"""
)
variables: dict[str, object] = {"userId": user_id}
response = await self.execute(query=query, variables=variables)
data = self.get_data(response)
return GetUser.parse_obj(data)

To get the value of user, we need to always get it by attribute, eg. await get_user("1").user. By using ShorterResultsPlugin our get_user returns the value of user directly.

[tool.ariadne-codegen]
...
plugins = ["ariadne_codegen.contrib.shorter_results.ShorterResultsPlugin"]
async def get_user(self, user_id: str) -> GetUserUser:
...
return GetUser.parse_obj(data).user

Discriminated unions

To ensure that data is represented as a correct class we use pydantic's discriminated unions. We add __typename to queries with unions and then use its value as discriminator. Let's take an example schema and query:

type Query {
animal: Animal!
}

interface Animal {
name: String!
}

type Dog implements Animal {
name: String!
dogField: String!
}

type Cat implements Animal {
name: String!
catField: String!
}

type Fish implements Animal {
name: String!
}
query GetAnimal {
animal {
name
... on Dog {
dogField
}
... on Cat {
catField
}
}
}

From this query and operation, we generate following types:

class GetAnimal(BaseModel):
animal: Union[
"GetAnimalAnimalAnimal", "GetAnimalAnimalDog", "GetAnimalAnimalCat"
] = Field(discriminator="typename__")


class GetAnimalAnimalAnimal(BaseModel):
typename__: Literal["Animal", "Fish"] = Field(alias="__typename")
name: str


class GetAnimalAnimalDog(BaseModel):
typename__: Literal["Dog"] = Field(alias="__typename")
name: str
dog_field: str = Field(alias="dogField")


class GetAnimalAnimalCat(BaseModel):
typename__: Literal["Cat"] = Field(alias="__typename")
name: str
cat_field: str = Field(alias="catField")

We added typename__ to this query, and by its value pydantic determines which model to choose.

Leading underscores

Ariadne Codegen 0.7 will remove leading _ from field names. Fields with _ are ignored by pydantic and it is impossible to save the value of such fields.

Removal of mixin directive from operation sent to a server

We support a custom mixin directive, which allows extending of generated types. In 0.7 we are removing it from the operation string included in generated client's methods. This directive is only used in the process of generation and caused servers to return errors because of an unknown directive.

process_schema plugin hook

Plugins can now define a process_schema hook to change schema before Codegen uses it for generation. From now on we allow invalid schemas to be parsed from files or URLs, and then we call this plugin hook. After process_schema is finished, the processed schema must pass graphql.assert_valid_schema validation.

For example, it can be used to add Apollo Federation directives definitions:

class MyPlugin:
def process_schema(self, schema: GraphQLSchema) -> GraphQLSchema:
extends_directive_def = GraphQLDirective(...)
schema.directives += (extends_directive_def, )

return schema

Changelog

  • Added support for subscriptions as async generators.
  • Changed how fragments are handled to generate separate module with fragments as mixins.
  • Fixed ResultTypesGenerator to trigger generate_result_class for each result model.
  • Changed processing of models fields to trim leading underscores.
  • Added ShorterResultsPlugin to standard plugins.
  • Fixed handling of inline fragments inside other fragments.
  • Changed generated unions to use pydantic's discriminated unions feature.
  • Replaced HTTPX's json= serializer for query payloads with pydantic's pydantic_encoder.
  • Removed mixin directive from operation string sent to server.
  • Fixed ShorterResultsPlugin that generated faulty code for discriminated unions.
  • Changed generator to ignore unused fragments which should be unpacked in queries.
  • Changed type hints for parse and serialize methods of scalars to typing.Any.
  • Added process_schema plugin hook.