-
Notifications
You must be signed in to change notification settings - Fork 186
Add comprehensive directive support to DSL module #563
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
Add comprehensive directive support to DSL module #563
Conversation
Implement GraphQL directives across all executable directive locations including operations, fields, fragments, variables, and inline fragments. Add DSLDirective class with validation, DSLDirectable mixin for reusable directive functionality, and DSLFragmentSpread for fragment-specific directives.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #563 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 38 40 +2
Lines 2908 3231 +323
==========================================
+ Hits 2908 3231 +323 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
because it already inherits DSLExecutable
That's really great, many thanks! Please check the few commits I made. Set ast_field in init in DSLFragment and DSLFragmentSpread: I also simplified the inheritance hierarchy. Please check the diagram and tell me if this is ok. Now, if you could please:
|
@leszekhanusz, I made a few commits for the following:
A couple more notes: When I was updating the test, I saw that if you use the DSL to add a variable as a directive argument on a variable definition it is possible to serialize a string with I also feel like it is a bit awkward to need to specify class DSLSchema:
def __init__(self, schema: GraphQLSchema):
self._schema: GraphQLSchema = schema
self._get_def = self._schema.get_type
def __call__(self, get_directive: bool: = False) -> "DSLSchema":
if get_directive:
self._get_def = self._schema.get_directive
else:
self._get_def = self._schema.get_type
def __getattr__(self, name: str) -> Union["DSLType", "DSLDirective"]:
ast_def = self._get_def(name)
... # validate and construct the obj
self._get_def = self._schema.get_type # reset to default behavior
return obj This seems kind of complicated and might be confusing to use, so I'm not sure if you feel like it's a good idea or not. |
What about using another class for Schema directives (named You'll use it like this: ds = DSLSchema(StarWarsSchema)
di = DSLSchemaDirective(StarWarsSchema)
query = DSLQuery(ds.Query.hero.select(ds.Character.name)).directives(
di.customDirective
) |
Yes, that would work and I thought about that option too. Since directives aren’t super common and executable ones are mostly used on fields it seems simpler to just add it as an argument than deal with another class. I think it’s good to discuss this now before the interface is locked in and I’m open to making changes if you feel strongly about them. |
Well, I agreed with you when you said that it is a bit awkward to need to specify It is a bit more consistent with how the dsl code is currently working. Using attributes of |
Yes, I agree that the need to specify I would like to make a change and here are a few details I am considering: 1. Should
|
If you are open to extending Examples: ds = DSLSchema(schema)
# "__schema", "__type", "__typename" -> DSLMetaField
ds.Query.hero.select(ds.Character.name, ds("__typename"))
"""
hero {
name
__typename
}
"""
# "..." -> DSLInlineFragment
ds.Query.hero.args(episode=6).select(
ds.Character.name,
ds("...").on(ds.Droid).select(ds.Droid.primaryFunction),
ds("...").on(ds.Human).select(ds.Human.homePlanet),
)
"""
hero(episode: JEDI) {
name
... on Droid {
primaryFunction
}
... on Human {
homePlanet
}
}
"""
# "fragment" -> DSLFragment
name_and_appearances = (
ds("fragment").NameAndAppearances.on(ds.Character)
.select(ds.Character.name, ds.Character.appearsIn)
)
ds.Query.hero.select(name_and_appearances)
"""
fragment NameAndAppearances on Character {
name
appearsIn
}
{
hero {
...NameAndAppearances
}
}
"""
# "@" -> DSLDirective
ds.Query.hero.select(ds.Character.name)).directives(
ds("@").customDirective(value="foo)
)
"""
hero {
name @customDirective(value: "foo")
}
""" |
That's really interesting. If we're going with the "magic" ds.Query.hero.select(ds.Character.name)).directives(
ds("@customDirective")(value="foo)
) Then the
Why not then use instead use Then the
|
After reflection, having the We could use overloads to indicate which type is returned in which case, but with the proposed implementation above, there is no way to distinguish between fragments and directives (both accepting custom strings starting with "@" or "fragment ") So I propose that for fragments, we use instead The @overload
def __call__(
self,
shortcut: Literal["__typename", "__schema", "__type"],
) -> DSLMetaField: ... # pragma: no cover
@overload
def __call__(
self,
shortcut: Literal["..."],
) -> DSLInlineFragment: ... # pragma: no cover
@overload
def __call__(
self,
shortcut: Literal["fragment"],
name: str,
) -> DSLFragment: ... # pragma: no cover
@overload
def __call__(
self,
shortcut: Any,
) -> DSLDirective: ... # pragma: no cover
def __call__(
self, shortcut: str, name: Optional[str] = None
) -> Union[DSLDirective, DSLFragment, DSLInlineFragment, DSLMetaField]:
if shortcut[0] == "@":
return DSLDirective(shortcut[1:], dsl_schema=self)
elif shortcut.startswith("__"):
return DSLMetaField(shortcut)
elif shortcut == "...":
return DSLInlineFragment()
elif shortcut == "fragment":
return DSLFragment(name)
raise ValueError(f"Unsupported shortcut: {shortcut}") |
Refactor gql/dsl.py update tests/starwars/test_dsl.py and tests/starwars/schema.py Update dsl_module.rst with new usage examples.
@leszekhanusz I just pushed a refactor where I enabled DSLDirective to be created by I do see what you are saying about how mypy will treat the shortcut argument for @overload
def __call__(
self,
shortcut: Literal["__"],
name: Literal["typename", "schema", "type"],
) -> DSLMetaField: ... # pragma: no cover
@overload
def __call__(
self,
shortcut: Literal["..."],
) -> DSLInlineFragment: ... # pragma: no cover
@overload
def __call__(
self,
shortcut: Literal["fragment"],
name: str,
) -> DSLFragment: ... # pragma: no cover
@overload
def __call__(
self,
shortcut: Literal["@"],
name: str,
) -> DSLDirective: ... # pragma: no cover
def __call__(
self, shortcut: str, name: str = ""
) -> Union[DSLDirective, DSLFragment, DSLInlineFragment, DSLMetaField]:
if shortcut == "@":
return DSLDirective(name, dsl_schema=self)
elif shortcut.startswith("__"):
return DSLMetaField(shortcut + name)
elif shortcut == "...":
return DSLInlineFragment()
elif shortcut == "fragment":
return DSLFragment(name)
raise ValueError(f"Unsupported shortcut: {shortcut}") However, this means that we have the slightly more awkward syntax of metafield = ds("__", "typename")
directive = ds("@", "someDirective") This one reason why I originally suggested defining One thing to think about in terms of extension is that the For example, instead of this: good_query_dsl = dsl.dsl_gql(
dsl.DSLQuery(
ds.Query.continents(
filter={
'code': {'eq': 'AN'}
}
).select(
ds.Continent.code,
ds.Continent.name,
)
)
) We could instead allow this (or dynamically generate it): good_query_dsl = dsl.dsl_gql(
dsl.DSLQuery(
ds.Query.continents(
filter=ds("input", "ContinentFilterInput").args(
code=ds("input", "StringQueryOperatorInput").args(eq="AN")
)
).select(
ds.Continent.code,
ds.Continent.name,
)
)
) Which is more verbose, but contains explicit GraphQL input type information that makes it simpler to validate nested arguments. If we adopt a syntax like this, then the architecture follows a pattern where the existing syntax I think we need to balance convenience and simplicity. Please let me know what you think about this idea! |
@leszekhanusz I think I fixed the mypy and and linter errors, but I haven't set up CI to run on my fork yet, so hopefully it passes. I also annotated |
@kasbaker Thanks again for this great PR. I'll let you open another one for the syntaxic sugar We'll continue the discussion there, but after reflection:
|
This PR adds GraphQL directive support to the DSL module, addressing the feature request in #479 where users requested the ability to use directives like
@skip
and@include
with the DSL module.New Features:
DSLDirective
class: Represents GraphQL directives with argument validation and AST generationDSLDirectable
mixin: Provides reusable.directives()
method for all DSL elements that support directivesDSLFragmentSpread
class: Represents fragment spreads with their own directives, separate from fragment definitionsThe implementation follows the October 2021 GraphQL specification for executable directive locations and maintains backward compatibility with existing DSL code. Users can now use both built-in directives (
@skip
,@include
) and custom schema directives across all supported GraphQL locations.Additional Changes:
tmp_path_as_home
fixture so that AWS Appsync tests intest_cli.py
don't try to use the real~/.aws
directoryCloses #479