Skip to content

Commit

Permalink
Merge pull request #49 from jhnnsrs/union-support
Browse files Browse the repository at this point in the history
- Adds union support for both schema and document generation.
- Also adds support for model options parameters
  • Loading branch information
jhnnsrs authored Feb 24, 2023
2 parents ee97031 + dd56105 commit 5b78fa2
Show file tree
Hide file tree
Showing 14 changed files with 484 additions and 21 deletions.
1 change: 1 addition & 0 deletions tests/documents/countries/basic.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ query Countries {
countries {
phone
capital
emojiU
}
}
10 changes: 10 additions & 0 deletions tests/documents/unions/test.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
query Nana {
hallo {
... on Bar {
nana
}
... on Foo {
forward
}
}
}
16 changes: 16 additions & 0 deletions tests/schemas/union.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"This is foo"
type Foo {
"This is a forward ref"
forward: String!
}

type Bar {
"This is a forward ref"
nana: Int!
}

union Element = Foo | Bar

type Query {
hallo: Element
}
127 changes: 127 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import ast

import pytest
from .utils import build_relative_glob, unit_test_with, ExecuteError
from turms.config import GeneratorConfig, OptionsConfig
from turms.plugins.funcs import (
FunctionDefinition,
FuncsPlugin,
FuncsPluginConfig,
)
from turms.run import generate_ast
from turms.plugins.enums import EnumsPlugin
from turms.plugins.inputs import InputsPlugin
from turms.plugins.fragments import FragmentsPlugin
from turms.plugins.operations import OperationsPlugin
from turms.stylers.default import DefaultStyler
from turms.helpers import build_schema_from_introspect_url


@pytest.fixture()
def countries_schema():
return build_schema_from_introspect_url("https://countries.trevorblades.com/")


def test_allow_population_by_field_name(countries_schema):
config = GeneratorConfig(
documents=build_relative_glob("/documents/countries/**.graphql"),
options=OptionsConfig(
enabled=True,
allow_population_by_field_name=True,
)
)

generated_ast = generate_ast(
config,
countries_schema,
stylers=[DefaultStyler()],
plugins=[
EnumsPlugin(),
InputsPlugin(),
FragmentsPlugin(),
OperationsPlugin(),
],
)

md = ast.Module(body=generated_ast, type_ignores=[])
generated = ast.unparse(ast.fix_missing_locations(md))
unit_test_with(generated_ast, "Countries(countries=[CountriesCountries(emoji_u='soinsisn',phone='sdf', capital='dfsdf')]).countries[0].emoji_u")
assert "from enum import Enum" in generated, "EnumPlugin not working"


def test_extra_forbid(countries_schema):
config = GeneratorConfig(
documents=build_relative_glob("/documents/countries/**.graphql"),
options=OptionsConfig(
enabled=True,
extra="forbid",
)
)

generated_ast = generate_ast(
config,
countries_schema,
stylers=[DefaultStyler()],
plugins=[
EnumsPlugin(),
InputsPlugin(),
FragmentsPlugin(),
OperationsPlugin(),
],
)

md = ast.Module(body=generated_ast, type_ignores=[])
generated = ast.unparse(ast.fix_missing_locations(md))
with pytest.raises(ExecuteError):
unit_test_with(generated_ast, "Countries(countries=[CountriesCountries(emojiU='soinsisn', phone='sdf', capital='dfsdf', hundi='soinsoin')]).countries[0].emoji_u")

def test_extra_allow(countries_schema):
config = GeneratorConfig(
documents=build_relative_glob("/documents/countries/**.graphql"),
options=OptionsConfig(
enabled=True,
extra="allow",
)
)

generated_ast = generate_ast(
config,
countries_schema,
stylers=[DefaultStyler()],
plugins=[
EnumsPlugin(),
InputsPlugin(),
FragmentsPlugin(),
OperationsPlugin(),
],
)

md = ast.Module(body=generated_ast, type_ignores=[])
generated = ast.unparse(ast.fix_missing_locations(md))
unit_test_with(generated_ast, "Countries(countries=[CountriesCountries(emojiU='soinsisn', phone='sdf', capital='dfsdf', hundi='soinsoin')]).countries[0].emoji_u")


def test_orm_mode(countries_schema):
config = GeneratorConfig(
documents=build_relative_glob("/documents/countries/**.graphql"),
options=OptionsConfig(
enabled=True,
orm_mode=True,
)
)

generated_ast = generate_ast(
config,
countries_schema,
stylers=[DefaultStyler()],
plugins=[
EnumsPlugin(),
InputsPlugin(),
FragmentsPlugin(),
OperationsPlugin(),
],
)

md = ast.Module(body=generated_ast, type_ignores=[])
generated = ast.unparse(ast.fix_missing_locations(md))
unit_test_with(generated_ast, "Countries(countries=[CountriesCountries(emojiU='soinsisn', phone='sdf', capital='dfsdf')]).countries[0].emoji_u")
20 changes: 20 additions & 0 deletions tests/test_schema_strawberry.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ def multi_interface_schema():
build_relative_glob("/schemas/multi_interface.graphql")
)

@pytest.fixture()
def union_schema():
return build_schema_from_glob(
build_relative_glob("/schemas/union.graphql")
)

@pytest.fixture()
def schema_directive_schema():
Expand All @@ -54,6 +59,21 @@ def test_countries_schema(countries_schema):

unit_test_with(generated_ast, "")

def test_union_schema(union_schema):
config = GeneratorConfig(scalar_definitions={"_Any": "typing.Any"})

generated_ast = generate_ast(
config,
union_schema,
stylers=[DefaultStyler()],
plugins=[
StrawberryPlugin(),
],
skip_forwards=True,
)

unit_test_with(generated_ast, "")


def test_arkitekt_schema(arkitekt_schema):
config = GeneratorConfig(
Expand Down
60 changes: 60 additions & 0 deletions tests/test_unions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import ast

import pytest
from .utils import build_relative_glob, unit_test_with
from turms.config import GeneratorConfig
from turms.run import generate_ast
from turms.plugins.enums import EnumsPlugin
from turms.plugins.inputs import InputsPlugin
from turms.plugins.fragments import FragmentsPlugin
from turms.plugins.operations import OperationsPlugin
from turms.plugins.funcs import (
FunctionDefinition,
FuncsPlugin,
FuncsPluginConfig,
)
from turms.stylers.snake_case import SnakeCaseStyler
from turms.stylers.capitalize import CapitalizeStyler
from turms.helpers import build_schema_from_glob


@pytest.fixture()
def union_schema():
return build_schema_from_glob(build_relative_glob("/schemas/union.graphql"))


def test_nested_input_funcs(union_schema):
config = GeneratorConfig(
documents=build_relative_glob("/documents/unions/*.graphql"),
)
generated_ast = generate_ast(
config,
union_schema,
stylers=[CapitalizeStyler(), SnakeCaseStyler()],
plugins=[
EnumsPlugin(),
InputsPlugin(),
FragmentsPlugin(),
OperationsPlugin(),
FuncsPlugin(
config=FuncsPluginConfig(
definitions=[
FunctionDefinition(
type="mutation",
use="mocks.aquery",
is_async=False,
),
FunctionDefinition(
type="mutation",
use="mocks.aquery",
is_async=True,
),
]
),
),
],
)

unit_test_with(generated_ast, 'Nana(hallo={"__typename": "Foo","forward": "yes"}).hallo.forward')


2 changes: 1 addition & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,4 @@ def unit_test_with(generated_ast: List[ast.AST], test_string: str):
return True
else:
# If the supbrocess failed we can break out of the sandbox and just return the actual error
raise ExecuteError(f"Failed with: {s.stderr.decode().strip()}")
raise ExecuteError(f"Failed with: {s.stderr.decode().strip()} Code: {parsed_code}" )
3 changes: 3 additions & 0 deletions turms/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def main(script: TurmsOptions, project=None):
if script == TurmsOptions.GEN:
gen(os.path.join(app_directory, "graphql.config.yaml"))

if script == TurmsOptions.DOWNLOAD:
gen(os.path.join(app_directory, "graphql.config.yaml"))


def entrypoint():
parser = argparse.ArgumentParser(description="Say hello")
Expand Down
66 changes: 59 additions & 7 deletions turms/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import builtins
from pydantic import AnyHttpUrl, BaseModel, BaseSettings, Field, validator
from typing import Any, Dict, List, Optional, Union, Protocol
from typing import Any, Dict, List, Optional, Union, Protocol, Literal
from turms.helpers import import_string
from enum import Enum

Expand All @@ -20,12 +20,7 @@ def __get_validators__(cls):
# the value returned from the previous validator
yield cls.validate

@classmethod
def __modify_schema__(cls, field_schema):
# __modify_schema__ should mutate the dict it receives in place,
# the returned value will be ignored
pass


@classmethod
def validate(cls, v):
if not callable(v):
Expand Down Expand Up @@ -104,6 +99,55 @@ class FreezeConfig(BaseSettings):
"""Convert GraphQL List to tuple (with varying length)"""


ExtraOptions = Optional[Union[Literal["ignore"], Literal["allow"], Literal["forbid"]]]



class OptionsConfig(BaseSettings):
"""Configuration for freezing the generated pydantic
models
This is useful for when you want to generate the models
that are faux immutable and hashable by default. The configuration
allows you to customize the way the models are frozen and specify
which types (operation, fragment, input,...) should be frozen.
"""

enabled: bool = Field(False, description="Enabling this, will freeze the schema")
"""Enabling this, will freeze the schema"""
extra: ExtraOptions
"""Extra options for pydantic"""
allow_mutation: Optional[bool]
"""Allow mutation"""
allow_population_by_field_name: Optional[bool]
"""Allow population by field name"""
orm_mode: Optional[bool]
"""ORM mode"""
use_enum_values: Optional[bool]
"""Use enum values"""

validate_assignment: Optional[bool]
"""Validate assignment"""


types: List[GraphQLTypes] = Field(
[GraphQLTypes.INPUT, GraphQLTypes.FRAGMENT, GraphQLTypes.OBJECT],
description="The types to freeze",
)
"""The core types (Input, Fragment, Object, Operation) to enable this option"""

exclude: Optional[List[str]] = Field(
description="List of types to exclude from setting this option"
)
"""List of types to exclude from setting this option"""
include: Optional[List[str]] = Field(
description="List of types to include in setting these options"
)
"""The types to freeze"""



class GeneratorConfig(BaseSettings):
"""Configuration for the generator
Expand Down Expand Up @@ -133,6 +177,8 @@ class GeneratorConfig(BaseSettings):
"""List of base classes for interfaces"""
always_resolve_interfaces: bool = True
"""Always resolve interfaces to concrete types"""
exclude_typenames: bool = False
"""Exclude __typename from generated models when calling dict or json"""

scalar_definitions: Dict[str, PythonType] = Field(
default_factory=dict,
Expand All @@ -145,6 +191,12 @@ class GeneratorConfig(BaseSettings):
)
"""Configuration for freezing the generated models: by default disabled"""

options: OptionsConfig = Field(
default_factory=OptionsConfig,
description="Configuration for pydantic options",
)
"""Configuration for pydantic options: by default disabled"""

skip_forwards: bool = False
"""Skip generating automatic forwards reference for the generated models"""

Expand Down
4 changes: 2 additions & 2 deletions turms/plugins/fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def generate_fragment(

inline_fragment_fields += [
generate_typename_field(
sub_node.type_condition.name.value, registry
sub_node.type_condition.name.value, registry, config
)
]

Expand Down Expand Up @@ -236,7 +236,7 @@ def generate_fragment(
f.type_condition.name.value, config, registry
)

fields += [generate_typename_field(type.name, registry)]
fields += [generate_typename_field(type.name, registry, config)]

for field in f.selection_set.selections:

Expand Down
Loading

0 comments on commit 5b78fa2

Please sign in to comment.