Skip to content

Commit

Permalink
refactor directives fields, drop type_ident - infer type from annotat…
Browse files Browse the repository at this point in the history
…ions
  • Loading branch information
m.kindritskiy committed May 25, 2023
1 parent 9104ac3 commit 0ee753e
Show file tree
Hide file tree
Showing 17 changed files with 392 additions and 146 deletions.
2 changes: 2 additions & 0 deletions docs/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Run linters, formatters and other checks
$ pdm run black
$ pdm run mypy
Or you can run `pdm check` - it will reformat code, run linters and test in one command.

Build docs
Docs will be available at ``docs/build``

Expand Down
6 changes: 4 additions & 2 deletions docs/directives.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,8 @@ You can also define your own directives (reimplementation of `Deprecated` direct
description='Marks a field as deprecated',
)
class Deprecated(SchemaDirective):
why: int = SchemaDirectiveField(
why: int = schema_directive_field(
name='why',
type_ident=SCALAR('String'),
description='Why deprecated ?',
)
Expand All @@ -130,3 +129,6 @@ You can also define your own directives (reimplementation of `Deprecated` direct
directives=[Deprecated("Old field")]),
]),
], directives=[Deprecated])
Note that type annotations for fields such as `why: int` are required, because `hiku`
will use them to generate types for schema introspection.
38 changes: 38 additions & 0 deletions hiku/custom_scalar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from dataclasses import dataclass
from typing import Any, Optional, Type


@dataclass
class ScalarInfo:
name: str
typ: Any


def _process_scalar(
cls: Type,
*,
name: Optional[str] = None,
) -> Type:
cls.__scalar_info__ = ScalarInfo(
name=name or cls.__name__,
typ=cls,
)

return cls


def scalar(
cls: Optional[Type] = None,
*,
name: Optional[str] = None,
) -> Any:
def wrap(cls: Type) -> Type:
return _process_scalar(
cls,
name=name,
)

if cls is None:
return wrap

return wrap(cls)
50 changes: 30 additions & 20 deletions hiku/directives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from enum import Enum
from typing_extensions import dataclass_transform

from hiku.introspection.types import NON_NULL, SCALAR
from hiku.utils.typing import builtin_to_introspection_type

if TYPE_CHECKING:
from hiku.graph import Field, Link
Expand Down Expand Up @@ -63,15 +63,9 @@ class Location(Enum):


class DirectiveField(dataclasses.Field, t.Generic[_T]):
if t.TYPE_CHECKING:

def __get__(self, instance: t.Any, owner: t.Any) -> _T:
...

def __init__(
self,
name: str,
type_ident: t.Any, # hiku.introspection.types.*
description: str = "",
default_value: t.Any = dataclasses.MISSING,
):
Expand All @@ -93,14 +87,21 @@ def __init__(

# name attribute is used by dataclasses.Field
self.field_name = name
# TODO: infer typeident from annotations
self.type_ident = type_ident
self.type_ident = None
self.description = description
self.default_value = (
None if default_value is dataclasses.MISSING else default_value
)


def directive_field(
name: str,
description: str = "",
default_value: t.Any = dataclasses.MISSING,
) -> t.Any: # note: t.Any is a workaround for mypy
return DirectiveField(name, description, default_value)


@dataclass
class DirectiveInfo:
name: str
Expand Down Expand Up @@ -136,10 +137,14 @@ def directive(
"""Decorator to mark class as operation directive.
A class with a @directive decorator will become a dataclass.
"""
# TODO: validation locations

def _wrap(cls: T) -> T:
cls = t.cast(T, wrap_dataclass(cls))
fields = get_fields(cls, DirectiveField)
for field in fields:
field.type_ident = builtin_to_introspection_type(field.type)

cls.__directive_info__ = DirectiveInfo(
name=name,
locations=locations,
Expand Down Expand Up @@ -168,9 +173,8 @@ def __hash(self, *args, **kwargs) -> int: # type: ignore[no-untyped-def] # noq
description="Caches node and all its fields",
)
class Cached(Directive):
ttl: int = DirectiveField( # type: ignore[assignment]
ttl: int = directive_field(
name="ttl",
type_ident=NON_NULL(SCALAR("Int")),
description="How long field will live in cache.",
)

Expand All @@ -189,9 +193,8 @@ class Cached(Directive):
),
)
class _SkipDirective(Directive):
if_: DirectiveField[bool] = DirectiveField(
if_: bool = directive_field(
name="if",
type_ident=NON_NULL(SCALAR("Boolean")),
description="Skipped when true.",
)

Expand All @@ -209,9 +212,8 @@ class _SkipDirective(Directive):
),
)
class _IncludeDirective(Directive):
if_: DirectiveField[bool] = DirectiveField(
if_: bool = directive_field(
name="if",
type_ident=NON_NULL(SCALAR("Boolean")),
description="Included when true.",
)

Expand All @@ -220,7 +222,6 @@ class SchemaDirectiveField(dataclasses.Field, t.Generic[_T]):
def __init__(
self,
name: str,
type_ident: t.Any, # hiku.introspection.types.*
description: str = "",
default_value: t.Any = dataclasses.MISSING,
):
Expand All @@ -242,14 +243,21 @@ def __init__(

# name attribute is used by dataclasses.Field
self.field_name = name
# TODO: infer typeident from annotations
self.type_ident = type_ident
self.type_ident = None
self.description = description
self.default_value = (
None if default_value is dataclasses.MISSING else default_value
)


def schema_directive_field(
name: str,
description: str = "",
default_value: t.Any = dataclasses.MISSING,
) -> t.Any: # note: t.Any is a workaround for mypy
return SchemaDirectiveField(name, description, default_value)


@dataclass
class SchemaDirectiveInfo:
name: str
Expand All @@ -274,6 +282,9 @@ def schema_directive(
def _wrap(cls: T) -> T:
cls = t.cast(T, wrap_dataclass(cls))
fields = get_fields(cls, SchemaDirectiveField)
for field in fields:
field.type_ident = builtin_to_introspection_type(field.type)

cls.__directive_info__ = SchemaDirectiveInfo(
name=name,
locations=locations,
Expand Down Expand Up @@ -315,9 +326,8 @@ def accept(self, visitor: t.Any) -> t.Any:
class Deprecated(SchemaDirective):
"""https://spec.graphql.org/June2018/#sec--deprecated"""

reason: SchemaDirectiveField[t.Optional[str]] = SchemaDirectiveField(
reason: t.Optional[str] = schema_directive_field(
name="reason",
type_ident=SCALAR("String"),
description="Deprecation reason.",
default_value=None,
)
Expand Down
59 changes: 59 additions & 0 deletions hiku/enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import dataclasses
from enum import EnumMeta
from typing import Any, List, Optional, TypeVar

EnumType = TypeVar("EnumType", bound=EnumMeta)


@dataclasses.dataclass
class EnumValue:
name: str
value: Any


@dataclasses.dataclass
class EnumInfo:
wrapped_cls: EnumMeta
name: str
values: List[EnumValue]


def _process_enum(
cls: EnumType,
name: Optional[str] = None,
) -> EnumType:
if not isinstance(cls, EnumMeta):
raise TypeError(f"{cls} is not an Enum")

if not name:
name = cls.__name__

values = []
for item in cls: # type: ignore
value = EnumValue(
item.name,
item.value,
)
values.append(value)

cls.__enum_info__ = EnumInfo( # type: ignore
wrapped_cls=cls,
name=name,
values=values,
)

return cls


def enum(
_cls: Optional[EnumType] = None,
*,
name: Optional[str] = None,
) -> Any:
def wrap(cls: EnumType) -> EnumType:
return _process_enum(cls, name)

if not _cls:
return wrap

return wrap(_cls)
Loading

0 comments on commit 0ee753e

Please sign in to comment.