Skip to content

Commit

Permalink
Add files
Browse files Browse the repository at this point in the history
  • Loading branch information
tonynamy committed Aug 17, 2023
1 parent 641cba4 commit f4acc79
Show file tree
Hide file tree
Showing 23 changed files with 717 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.7
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015-Present Syrus Akbary

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
147 changes: 147 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# typed-graphene

typed-graphene is a library that provides a type-safe interface for graphene-python.

## Examples

### Type-Safe Query

```python
from graphene import ObjectType, Field, String

class ExampleQuery(ObjectType):
foo = Field(FooType, required=True, input_a=String(), input_b=String(required=True))

def resolve_foo(self, info, **data) -> FooType:
input_a = data.get("input_a") # any
input_b = data["input_b"] # any
```

**graphene example**

```python
from typing import TypedDict, NotRequired

from graphene import Field
from typed_graphene import TypedField

class FooFieldArguments(TypedDict):
input_a: NotRequired[str]
input_b: str

class ExampleQuery(ObjectType):
# `required=True` by default
foo = TypedField(FooType, **FooFieldArguments.__annotations__)

def resolve_foo(self, info, **data: Unpack[FooFieldArguments]) -> FooType:
input_a = data.get("input_a") # str | None
input_b = data["input_b"] # str
```

**typed-graphene exmaple**

### Type-Safe Mutation

```python
from graphene import Mutation, Field, String, Boolean

class Example(Mutation):
class Arguments:
input_a = String()
input_b = String(required=True)

ok = Field(Boolean, required=True)
errors = Field(ExampleErrors)

@classmethod
def mutate(cls, root, info, **data) -> Self:
input_a = data.get("input_a") # any
input_b = data["input_b"] # any

return cls(ok=True)
```

**graphene example**

```python
from dataclasses import dataclass
from typing import TypedDict, NotRequired

from typed_graphene import TypedBaseMutation, TypedField

class Example(TypedBaseMutation):
class TypedArguments(TypedBaseMutation.TypedArguments):
input_a: NotRequired[str]
input_b: str

ok = TypedField(bool)
# concat type with ` | None` for `required=False`
errors = TypedField(ExampleErrors | None)

@classmethod
def validate(cls, root, info, **data: Unpack[TypedArguments]) -> ExampleErrors:
errors = ExampleErrors()

errors.input_a = "error" # no error

return errors

@classmethod
def execute(cls, root, info, **data: Unpack[TypedArguments]) -> Self:
input_a = data.get("input_a") # str | None
input_b = data["input_b"] # str

return cls(ok=True)
```

**typed-graphene example**

## Topics

### Defining custom types

You can define custom types by inheriting from `BaseTransformer` and registering it with `register`.

```python
from graphene import ID
from typed_graphene import BaseTransformer, register

class IDStr(str):
pass

@register
class IDStrTransformer(BaseTransformer):
python_type = IDStr
graphene_type = ID
```

You can also override `check_type` and `transform_type` to define custom type checking and transformation.
It is recommended to add `@cache` to the `check_type` and `transform_type` methods for performance and type-consistency.

```python
from typing import Literal, get_origin

from graphene import Literal
from typed_graphene import BaseTransformer, register

@register
class LiteralTransformer(BaseTransformer[Literal, Enum]):
python_type = Literal
graphene_type = Enum

@classmethod
@cache
def check_type(cls, T):
return get_origin(T) == Literal

@classmethod
@cache
def transform_type(cls, T: type[Literal]) -> type[Enum]:
"""Transform the type into the graphene type."""
return Enum.from_enum(literal_to_enum(T))

```

## Author

Jeong Yeon Nam([email protected])
19 changes: 19 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from setuptools import find_packages, setup

with open("README.md", encoding="utf-8") as f:
long_description = f.read()

setup(
name="typed_graphene",
packages=find_packages(include=["typed_graphene", "typed_graphene.*"]),
version="0.0.2",
description="Type-safe interface for graphene-python.",
long_description=long_description,
long_description_content_type="text/markdown",
author="Jeong Yeon Nam<[email protected]>",
license="MIT",
install_requires=[],
setup_requires=["graphene>=2.0.0, <3"],
tests_require=["pytest==7.4.0"],
test_suite="tests",
)
Empty file added tests/__init__.py
Empty file.
Empty file added tests/utils/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions tests/utils/test_get_nested_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typed_graphene.utils import get_nested_type
import pytest


def test_not_nested(): # type: ignore[no-untyped-def]
T = str

with pytest.raises(IndexError):
get_nested_type(T)


def test_nested(): # type: ignore[no-untyped-def]
T = list[str]

expected_result = str
result = get_nested_type(T)

assert result == expected_result


def test_double_nested(): # type: ignore[no-untyped-def]
T = list[list[str]]

expected_result = list[str]
result = get_nested_type(T)

assert result == expected_result


def test_double_nested_complex(): # type: ignore[no-untyped-def]
T = dict[int, list[str]]

expected_result = int
result = get_nested_type(T)

assert result == expected_result
87 changes: 87 additions & 0 deletions tests/utils/test_get_type_and_required.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from typing import NotRequired, Optional, TypedDict, Union

from typed_graphene.utils import get_type_and_required


def test_union() -> None:
T = Union[str, int]

expected_result = Union[str, int], True
result = get_type_and_required(T) # type: ignore[arg-type]

assert expected_result == result


def test_union_complex_none() -> None:
T = Union[str, int, None]

expected_result = Union[str, int], False
result = get_type_and_required(T) # type: ignore[arg-type]

assert expected_result == result


def test_union_none() -> None:
T = Union[str, None]

expected_result = str, False
result = get_type_and_required(T) # type: ignore[arg-type]

assert expected_result == result


def test_union_type() -> None:
T = str | int

expected_result = str | int, True
result = get_type_and_required(T) # type: ignore[arg-type]

assert expected_result == result


def test_union_type_complex_none() -> None:
T = str | int | None

expected_result = str | int, False
result = get_type_and_required(T) # type: ignore[arg-type]

assert expected_result == result


def test_union_type_none() -> None:
T = str | None

expected_result = str, False
result = get_type_and_required(T) # type: ignore[arg-type]

assert expected_result == result


def test_not_required() -> None:
class TestTypedDict(TypedDict):
field: NotRequired[str]

T = TestTypedDict.__annotations__["field"]

expected_result = str, False
result = get_type_and_required(T)

assert expected_result == result


def test_optional() -> None:
T = Optional[str]

expected_result = str, False
result = get_type_and_required(T) # type: ignore[arg-type]

assert expected_result == result


def test_required() -> None:
T = str

expected_result = str, True
result = get_type_and_required(T)

assert expected_result == result
13 changes: 13 additions & 0 deletions typed_graphene/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .transformers import BaseTransformer, register
from .typed_field import TypedField
from .typed_mutation import TypedMutation

__version__ = "0.0.2"

__all__ = [
"__version__",
"TypedMutation",
"TypedField",
"register",
"BaseTransformer",
]
31 changes: 31 additions & 0 deletions typed_graphene/transformers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from .base_transformer import BaseTransformer
from .bool_transformer import BoolTransformer
from .date_transformer import DateTransformer
from .datetime_transformer import DateTimeTransformer
from .decimal_transformer import DeicmalTransformer
from .float_transformer import FloatTransformer
from .int_transformer import IntTransformer
from .register import register
from .str_transformer import StrTransformer

__all__ = [
"BaseTransformer",
"BoolTransformer",
"DateTimeTransformer",
"IntTransformer",
"StrTransformer",
"DateTransformer",
"DeicmalTransformer",
"FloatTransformer",
"register",
]

DEFAULT_TRANSFORMERS = [
BoolTransformer,
DateTimeTransformer,
DeicmalTransformer,
IntTransformer,
StrTransformer,
DateTransformer,
FloatTransformer,
]
23 changes: 23 additions & 0 deletions typed_graphene/transformers/base_transformer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from functools import cache
from typing import Any, Generic, TypeGuard, TypeVar
from graphene import Scalar

_T = TypeVar("_T", bound=object)
_S = TypeVar("_S", bound=Scalar)


class BaseTransformer(Generic[_T, _S]):
python_type: type[_T]
graphene_type: type[_S]

@classmethod
@cache
def check_type(cls, T: type[Any]) -> TypeGuard[type[_T]]:
"""Check if the type is the guarded type."""
return T == cls.python_type

@classmethod
@cache
def transform_type(cls, T: type[_T]) -> type[_S]:
"""Transform the type into the graphene type."""
return cls.graphene_type
Loading

0 comments on commit f4acc79

Please sign in to comment.