diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml deleted file mode 100644 index a8e2875c..00000000 --- a/.github/workflows/coveralls.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: 📊 Check Coverage -on: - push: - branches: - - master - - '*.x' - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' - pull_request: - branches: - - master - - '*.x' - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' -jobs: - coveralls_finish: - # check coverage increase/decrease - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: AndreMiras/coveralls-python-action@develop diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2a6cdc6b..07c0766f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,10 +11,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Build wheel and source tarball run: | pip install wheel diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 95251d9b..c9efc0cf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cdc4d01e..8a962ac6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,12 +25,14 @@ jobs: fail-fast: false matrix: include: - - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - - {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36} + - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} + - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} + - { name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38 } + - { name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37 } + - { name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36 } steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} @@ -45,10 +47,20 @@ jobs: run: echo "::set-output name=dir::$(pip cache dir)" - name: cache pip dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }} - run: pip install tox - run: tox -e ${{ matrix.tox }} + - name: Upload coverage.xml + if: ${{ matrix.python == '3.10' }} + uses: actions/upload-artifact@v3 + with: + name: graphene-sqlalchemy-coverage + path: coverage.xml + if-no-files-found: error + - name: Upload coverage.xml to codecov + if: ${{ matrix.python == '3.10' }} + uses: codecov/codecov-action@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bd6a7340..87fa4872 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,9 @@ default_language_version: - python: python3.8 + python: python3.9 repos: -- repo: git://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 hooks: - id: check-merge-conflict - id: check-json @@ -16,15 +16,15 @@ repos: - --autofix - id: trailing-whitespace exclude: README.md -- repo: git://github.com/asottile/pyupgrade - rev: v2.24.0 +- repo: https://github.com/asottile/pyupgrade + rev: v2.32.1 hooks: - id: pyupgrade -- repo: git://github.com/ambv/black - rev: 19.3b0 +- repo: https://github.com/ambv/black + rev: 22.3.0 hooks: - id: black -- repo: git://github.com/PyCQA/flake8 - rev: 3.8.4 +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - id: flake8 diff --git a/README.md b/README.md index 85849a3d..a7714e33 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,6 @@ **We are looking for contributors**! Please check the [ROADMAP](https://github.com/graphql-python/graphene/blob/master/ROADMAP.md) to see how you can help ❤️ ---- - -**The below readme is the documentation for the `dev` (prerelease) version of Graphene. To view the documentation for the latest stable Graphene version go to the [v2 docs](https://docs.graphene-python.org/en/stable/)** - ---- - ## Introduction [Graphene](http://graphene-python.org) is an opinionated Python library for building GraphQL schemas/types fast and easily. @@ -37,7 +31,7 @@ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly wit For instaling graphene, just run this command in your shell ```bash -pip install "graphene>=2.0" +pip install "graphene>=3.0" ``` ## Examples diff --git a/README.rst b/README.rst index 243215e4..3fb51df2 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +1,18 @@ +|Graphene Logo| `Graphene `__ |Build Status| |PyPI version| |Coverage Status| +========================================================================================================= + +`💬 Join the community on +Slack `__ + **We are looking for contributors**! Please check the `ROADMAP `__ to see how you can help ❤️ --------------- - -|Graphene Logo| `Graphene `__ |Build Status| |PyPI version| |Coverage Status| -========================================================================================================= - - Introduction ------------ -`Graphene `__ is a Python library for -building GraphQL schemas/types fast and easily. +`Graphene `__ is an opinionated Python +library for building GraphQL schemas/types fast and easily. - **Easy to use:** Graphene helps you use GraphQL in Python without effort. @@ -27,17 +27,18 @@ Integrations Graphene has multiple integrations with different frameworks: -+---------------------+----------------------------------------------------------------------------------------------+ -| integration | Package | -+=====================+==============================================================================================+ -| Django | `graphene-django `__ | -+---------------------+----------------------------------------------------------------------------------------------+ -| SQLAlchemy | `graphene-sqlalchemy `__ | -+---------------------+----------------------------------------------------------------------------------------------+ -| Google App Engine | `graphene-gae `__ | -+---------------------+----------------------------------------------------------------------------------------------+ -| Peewee | *In progress* (`Tracking Issue `__) | -+---------------------+----------------------------------------------------------------------------------------------+ ++-------------------+-------------------------------------------------+ +| integration | Package | ++===================+=================================================+ +| Django | `graphene-django `__ | ++-------------------+-------------------------------------------------+ +| SQLAlchemy | `graphene-sqlalchemy `__ | ++-------------------+-------------------------------------------------+ +| Google App Engine | `graphene-gae `__ | ++-------------------+-------------------------------------------------+ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly with all GraphQL clients, such as @@ -52,13 +53,7 @@ For instaling graphene, just run this command in your shell .. code:: bash - pip install "graphene>=2.0" - -2.0 Upgrade Guide ------------------ - -Please read `UPGRADE-v2.0.md `__ to learn how to -upgrade. + pip install "graphene>=3.0" Examples -------- @@ -67,26 +62,26 @@ Here is one example for you to get started: .. code:: python - import graphene + import graphene - class Query(graphene.ObjectType): - hello = graphene.String(description='A typical hello world') + class Query(graphene.ObjectType): + hello = graphene.String(description='A typical hello world') - def resolve_hello(self, info): - return 'World' + def resolve_hello(self, info): + return 'World' - schema = graphene.Schema(query=Query) + schema = graphene.Schema(query=Query) Then Querying ``graphene.Schema`` is as simple as: .. code:: python - query = ''' - query SayHello { - hello - } - ''' - result = schema.execute(query) + query = ''' + query SayHello { + hello + } + ''' + result = schema.execute(query) If you want to learn even more, you can also check the following `examples `__: @@ -110,20 +105,20 @@ dependencies are installed by running: .. code:: sh - virtualenv venv - source venv/bin/activate - pip install -e ".[test]" + virtualenv venv + source venv/bin/activate + pip install -e ".[test]" Well-written tests and maintaining good test coverage is important to this project. While developing, run new and existing tests with: .. code:: sh - py.test graphene/relay/tests/test_node.py # Single file - py.test graphene/relay # All tests in directory + py.test graphene/relay/tests/test_node.py # Single file + py.test graphene/relay # All tests in directory Add the ``-s`` flag if you have introduced breakpoints into the code for -debugging. Add the ``-v`` ("verbose") flag to get more detailed test +debugging. Add the ``-v`` (“verbose”) flag to get more detailed test output. For even more detailed output, use ``-vv``. Check out the `pytest documentation `__ for more options and test running controls. @@ -132,7 +127,7 @@ You can also run the benchmarks with: .. code:: sh - py.test graphene --benchmark-only + py.test graphene --benchmark-only Graphene supports several versions of Python. To make sure that changes do not break compatibility with any of those versions, we use ``tox`` to @@ -142,14 +137,14 @@ config file, just run: .. code:: sh - tox + tox If you wish to run against a specific version defined in the ``tox.ini`` file: .. code:: sh - tox -e py36 + tox -e py36 Tox can only use whatever versions of Python are installed on your system. When you create a pull request, Travis will also be running the @@ -168,7 +163,7 @@ An HTML version of the documentation is produced by running: .. code:: sh - make docs + make docs .. |Graphene Logo| image:: http://graphene-python.org/favicon.png .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master diff --git a/UPGRADE-v1.0.md b/UPGRADE-v1.0.md index 8ace8756..ecfa9da7 100644 --- a/UPGRADE-v1.0.md +++ b/UPGRADE-v1.0.md @@ -153,7 +153,7 @@ class Query(ObjectType): ``` Also, if you wanted to create an `ObjectType` that implements `Node`, you have to do it -explicity. +explicitly. ## Django diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 63f8f622..04926e7a 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -123,7 +123,7 @@ def resolve_my_field(root, info, my_arg): return ... ``` -**PS.: Take care with receiving args like `my_arg` as above. This doesn't work for optional (non-required) arguments as stantard `Connection`'s arguments (first, before, after, before).** +**PS.: Take care with receiving args like `my_arg` as above. This doesn't work for optional (non-required) arguments as standard `Connection`'s arguments (first, last, after, before).** You may need something like this: ```python diff --git a/docs/conf.py b/docs/conf.py index 26becbc2..0166d4c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,18 +64,18 @@ source_suffix = ".rst" master_doc = "index" # General information about the project. -project = u"Graphene" -copyright = u"Graphene 2016" -author = u"Syrus Akbary" +project = "Graphene" +copyright = "Graphene 2016" +author = "Syrus Akbary" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = u"1.0" +version = "1.0" # The full version, including alpha/beta/rc tags. -release = u"1.0" +release = "1.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -278,7 +278,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual") + (master_doc, "Graphene.tex", "Graphene Documentation", "Syrus Akbary", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -318,7 +318,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "graphene", u"Graphene Documentation", [author], 1)] +man_pages = [(master_doc, "graphene", "Graphene Documentation", [author], 1)] # If true, show URL addresses after external links. # @@ -334,7 +334,7 @@ texinfo_documents = [ ( master_doc, "Graphene", - u"Graphene Documentation", + "Graphene Documentation", author, "Graphene", "One line description of project.", diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 3f693075..61890951 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -4,7 +4,7 @@ Dataloader DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching -and caching. +and caching. It is provided by a separate package `aiodataloader `. Batching @@ -15,32 +15,31 @@ Create loaders by providing a batch loading function. .. code:: python - from promise import Promise - from promise.dataloader import DataLoader + from aiodataloader import DataLoader class UserLoader(DataLoader): - def batch_load_fn(self, keys): - # Here we return a promise that will result on the - # corresponding user for each key in keys - return Promise.resolve([get_user(id=key) for key in keys]) + async def batch_load_fn(self, keys): + # Here we call a function to return a user for each key in keys + return [get_user(id=key) for key in keys] -A batch loading function accepts a list of keys, and returns a ``Promise`` -which resolves to a list of ``values``. +A batch loading async function accepts a list of keys, and returns a list of ``values``. -Then load individual values from the loader. ``DataLoader`` will coalesce all -individual loads which occur within a single frame of execution (executed once -the wrapping promise is resolved) and then call your batch function with all -requested keys. + +``DataLoader`` will coalesce all individual loads which occur within a +single frame of execution (executed once the wrapping event loop is resolved) +and then call your batch function with all requested keys. .. code:: python user_loader = UserLoader() - user_loader.load(1).then(lambda user: user_loader.load(user.best_friend_id)) + user1 = await user_loader.load(1) + user1_best_friend = await user_loader.load(user1.best_friend_id)) - user_loader.load(2).then(lambda user: user_loader.load(user.best_friend_id)) + user2 = await user_loader.load(2) + user2_best_friend = await user_loader.load(user2.best_friend_id)) A naive application may have issued *four* round-trips to a backend for the @@ -54,9 +53,9 @@ make sure that you then order the query result for the results to match the keys .. code:: python class UserLoader(DataLoader): - def batch_load_fn(self, keys): + async def batch_load_fn(self, keys): users = {user.id: user for user in User.objects.filter(id__in=keys)} - return Promise.resolve([users.get(user_id) for user_id in keys]) + return [users.get(user_id) for user_id in keys] ``DataLoader`` allows you to decouple unrelated parts of your application without @@ -96,7 +95,7 @@ Consider the following GraphQL request: } -Naively, if ``me``, ``bestFriend`` and ``friends`` each need to request the backend, +If ``me``, ``bestFriend`` and ``friends`` each need to send a request to the backend, there could be at most 13 database requests! @@ -111,8 +110,8 @@ leaner code and at most 4 database requests, and possibly fewer if there are cac best_friend = graphene.Field(lambda: User) friends = graphene.List(lambda: User) - def resolve_best_friend(root, info): - return user_loader.load(root.best_friend_id) + async def resolve_best_friend(root, info): + return await user_loader.load(root.best_friend_id) - def resolve_friends(root, info): - return user_loader.load_many(root.friend_ids) + async def resolve_friends(root, info): + return await user_loader.load_many(root.friend_ids) diff --git a/docs/execution/fileuploading.rst b/docs/execution/fileuploading.rst index d92174c0..66ce9bd3 100644 --- a/docs/execution/fileuploading.rst +++ b/docs/execution/fileuploading.rst @@ -4,5 +4,5 @@ File uploading File uploading is not part of the official GraphQL spec yet and is not natively implemented in Graphene. -If your server needs to support file uploading then you can use the libary: `graphene-file-upload `_ which enhances Graphene to add file +If your server needs to support file uploading then you can use the library: `graphene-file-upload `_ which enhances Graphene to add file uploads and conforms to the unoffical GraphQL `multipart request spec `_. diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index 0c5458b2..c0a8c792 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -46,7 +46,7 @@ Functional example ------------------ Middleware can also be defined as a function. Here we define a middleware that -logs the time it takes to resolve each field +logs the time it takes to resolve each field: .. code:: python diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 62d11949..0b6c6993 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -60,14 +60,14 @@ Requirements ~~~~~~~~~~~~ - Python (2.7, 3.4, 3.5, 3.6, pypy) -- Graphene (2.0) +- Graphene (3.0) Project setup ~~~~~~~~~~~~~ .. code:: bash - pip install "graphene>=2.0" + pip install "graphene>=3.0" Creating a basic Schema ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/types/enums.rst b/docs/types/enums.rst index a3215cad..b9ac5333 100644 --- a/docs/types/enums.rst +++ b/docs/types/enums.rst @@ -86,7 +86,7 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu assert Color(1) == Color.RED -However, in Graphene ``Enum`` you need to call get to have the same effect: +However, in Graphene ``Enum`` you need to call `.get` to have the same effect: .. code:: python diff --git a/docs/types/interfaces.rst b/docs/types/interfaces.rst index eb7172e9..4d4e32a5 100644 --- a/docs/types/interfaces.rst +++ b/docs/types/interfaces.rst @@ -44,7 +44,7 @@ Both of these types have all of the fields from the ``Character`` interface, but also bring in extra fields, ``home_planet``, ``starships`` and ``primary_function``, that are specific to that particular type of character. -The full GraphQL schema defition will look like this: +The full GraphQL schema definition will look like this: .. code:: diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index f8c76f35..73866063 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -85,9 +85,9 @@ We should receive: InputFields and InputObjectTypes ---------------------------------- -InputFields are used in mutations to allow nested input data for mutations +InputFields are used in mutations to allow nested input data for mutations. -To use an InputField you define an InputObjectType that specifies the structure of your input data +To use an InputField you define an InputObjectType that specifies the structure of your input data: .. code:: python @@ -112,7 +112,7 @@ To use an InputField you define an InputObjectType that specifies the structure return CreatePerson(person=person) -Note that **name** and **age** are part of **person_data** now +Note that **name** and **age** are part of **person_data** now. Using the above mutation your new query would look like this: @@ -128,7 +128,7 @@ Using the above mutation your new query would look like this: } InputObjectTypes can also be fields of InputObjectTypes allowing you to have -as complex of input data as you need +as complex of input data as you need: .. code:: python @@ -160,7 +160,7 @@ To return an existing ObjectType instead of a mutation-specific type, set the ** def mutate(root, info, name): return Person(name=name) -Then, if we query (``schema.execute(query_str)``) the following: +Then, if we query (``schema.execute(query_str)``) with the following: .. code:: diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index db20a522..f47fffea 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -270,7 +270,7 @@ The following is an example for creating a DateTime scalar: return dt.isoformat() @staticmethod - def parse_literal(node): + def parse_literal(node, _variables=None): if isinstance(node, ast.StringValue): return datetime.datetime.strptime( node.value, "%Y-%m-%dT%H:%M:%S.%f") diff --git a/docs/types/schema.rst b/docs/types/schema.rst index 08ff27d0..a82addc9 100644 --- a/docs/types/schema.rst +++ b/docs/types/schema.rst @@ -44,7 +44,7 @@ There are some cases where the schema cannot access all of the types that we pla For example, when a field returns an ``Interface``, the schema doesn't know about any of the implementations. -In this case, we need to use the ``types`` argument when creating the Schema. +In this case, we need to use the ``types`` argument when creating the Schema: .. code:: python @@ -63,7 +63,7 @@ By default all field and argument names (that are not explicitly set with the ``name`` arg) will be converted from ``snake_case`` to ``camelCase`` (as the API is usually being consumed by a js/mobile client) -For example with the ObjectType +For example with the ObjectType the ``last_name`` field name is converted to ``lastName``: .. code:: python @@ -71,12 +71,10 @@ For example with the ObjectType last_name = graphene.String() other_name = graphene.String(name='_other_Name') -the ``last_name`` field name is converted to ``lastName``. - In case you don't want to apply this transformation, provide a ``name`` argument to the field constructor. ``other_name`` converts to ``_other_Name`` (without further transformations). -Your query should look like +Your query should look like: .. code:: @@ -86,7 +84,7 @@ Your query should look like } -To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation. +To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation: .. code:: python diff --git a/docs/types/unions.rst b/docs/types/unions.rst index 2c5c5a75..16ac24e8 100644 --- a/docs/types/unions.rst +++ b/docs/types/unions.rst @@ -7,7 +7,7 @@ to specify any common fields between the types. The basics: - Each Union is a Python class that inherits from ``graphene.Union``. -- Unions don't have any fields on it, just links to the possible objecttypes. +- Unions don't have any fields on it, just links to the possible ObjectTypes. Quick example ------------- diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index 7bce5ba3..b02a420c 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -49,7 +49,7 @@ type Faction implements Node { name: String """The ships used by the faction.""" - ships(before: String = null, after: String = null, first: Int = null, last: Int = null): ShipConnection + ships(before: String, after: String, first: Int, last: Int): ShipConnection } """An object with an ID""" @@ -115,5 +115,4 @@ input IntroduceShipInput { shipName: String! factionId: String! clientMutationId: String -} -''' +}''' diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index f280df04..c024f432 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -9,7 +9,7 @@ client = Client(schema) def test_str_schema(snapshot): - snapshot.assert_match(str(schema)) + snapshot.assert_match(str(schema).strip()) def test_correctly_fetches_id_name_rebels(snapshot): diff --git a/graphene/__init__.py b/graphene/__init__.py index c8ffc0c4..bf9831b5 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -41,7 +41,7 @@ from .types import ( from .utils.module_loading import lazy_import from .utils.resolve_only_args import resolve_only_args -VERSION = (3, 0, 0, "beta", 8) +VERSION = (3, 1, 0, "final", 0) __version__ = get_version(VERSION) diff --git a/graphene/pyutils/version.py b/graphene/pyutils/version.py index f2005442..8a3be07a 100644 --- a/graphene/pyutils/version.py +++ b/graphene/pyutils/version.py @@ -19,10 +19,7 @@ def get_version(version=None): sub = "" if version[3] == "alpha" and version[4] == 0: git_changeset = get_git_changeset() - if git_changeset: - sub = ".dev%s" % git_changeset - else: - sub = ".dev" + sub = ".dev%s" % git_changeset if git_changeset else ".dev" elif version[3] != "final": mapping = {"alpha": "a", "beta": "b", "rc": "rc"} sub = mapping[version[3]] + str(version[4]) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index 8defefff..dabcff6c 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -18,11 +18,7 @@ def is_node(objecttype): if not issubclass(objecttype, ObjectType): return False - for i in objecttype._meta.interfaces: - if issubclass(i, Node): - return True - - return False + return any(issubclass(i, Node) for i in objecttype._meta.interfaces) class GlobalID(Field): @@ -90,11 +86,13 @@ class Node(AbstractNode): def get_node_from_global_id(cls, info, global_id, only_type=None): try: _type, _id = cls.from_global_id(global_id) + if not _type: + raise ValueError("Invalid Global ID") except Exception as e: raise Exception( f'Unable to parse global ID "{global_id}". ' 'Make sure it is a base64 encoded string in the format: "TypeName:id". ' - f"Exception message: {str(e)}" + f"Exception message: {e}" ) graphene_type = info.schema.get_type(_type) diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index d46838ac..6b310fde 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -1,7 +1,7 @@ import re -from graphql_relay import to_global_id +from textwrap import dedent -from graphene.tests.utils import dedent +from graphql_relay import to_global_id from ...types import ObjectType, Schema, String from ..node import Node, is_node @@ -171,8 +171,10 @@ def test_node_field_only_lazy_type_wrong(): def test_str_schema(): - assert str(schema) == dedent( - ''' + assert ( + str(schema).strip() + == dedent( + ''' schema { query: RootQuery } @@ -213,4 +215,5 @@ def test_str_schema(): ): MyNode } ''' + ).strip() ) diff --git a/graphene/relay/tests/test_node_custom.py b/graphene/relay/tests/test_node_custom.py index 76a2cad3..762e3424 100644 --- a/graphene/relay/tests/test_node_custom.py +++ b/graphene/relay/tests/test_node_custom.py @@ -1,6 +1,6 @@ -from graphql import graphql_sync +from textwrap import dedent -from graphene.tests.utils import dedent +from graphql import graphql_sync from ...types import Interface, ObjectType, Schema from ...types.scalars import Int, String @@ -54,8 +54,10 @@ graphql_schema = schema.graphql_schema def test_str_schema_correct(): - assert str(schema) == dedent( - ''' + assert ( + str(schema).strip() + == dedent( + ''' schema { query: RootQuery } @@ -93,6 +95,7 @@ def test_str_schema_correct(): ): Node } ''' + ).strip() ) diff --git a/graphene/test/__init__.py b/graphene/test/__init__.py index 8591dc06..13b05dd3 100644 --- a/graphene/test/__init__.py +++ b/graphene/test/__init__.py @@ -1,5 +1,4 @@ from promise import Promise, is_thenable -from graphql.error import format_error as format_graphql_error from graphql.error import GraphQLError from graphene.types.schema import Schema @@ -7,7 +6,7 @@ from graphene.types.schema import Schema def default_format_error(error): if isinstance(error, GraphQLError): - return format_graphql_error(error) + return error.formatted return {"message": str(error)} diff --git a/graphene/tests/issues/test_1394.py b/graphene/tests/issues/test_1394.py new file mode 100644 index 00000000..39374381 --- /dev/null +++ b/graphene/tests/issues/test_1394.py @@ -0,0 +1,36 @@ +from ...types import ObjectType, Schema, String, NonNull + + +class Query(ObjectType): + hello = String(input=NonNull(String)) + + def resolve_hello(self, info, input): + if input == "nothing": + return None + return f"Hello {input}!" + + +schema = Schema(query=Query) + + +def test_required_input_provided(): + """ + Test that a required argument works when provided. + """ + input_value = "Potato" + result = schema.execute('{ hello(input: "%s") }' % input_value) + assert not result.errors + assert result.data == {"hello": "Hello Potato!"} + + +def test_required_input_missing(): + """ + Test that a required argument raised an error if not provided. + """ + result = schema.execute("{ hello }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Field 'hello' argument 'input' of type 'String!' is required, but it was not provided." + ) diff --git a/graphene/tests/issues/test_1419.py b/graphene/tests/issues/test_1419.py new file mode 100644 index 00000000..a08374da --- /dev/null +++ b/graphene/tests/issues/test_1419.py @@ -0,0 +1,53 @@ +import pytest + +from ...types.base64 import Base64 +from ...types.datetime import Date, DateTime +from ...types.decimal import Decimal +from ...types.generic import GenericScalar +from ...types.json import JSONString +from ...types.objecttype import ObjectType +from ...types.scalars import ID, BigInt, Boolean, Float, Int, String +from ...types.schema import Schema +from ...types.uuid import UUID + + +@pytest.mark.parametrize( + "input_type,input_value", + [ + (Date, '"2022-02-02"'), + (GenericScalar, '"foo"'), + (Int, "1"), + (BigInt, "12345678901234567890"), + (Float, "1.1"), + (String, '"foo"'), + (Boolean, "true"), + (ID, "1"), + (DateTime, '"2022-02-02T11:11:11"'), + (UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'), + (Decimal, '"1.1"'), + (JSONString, '"{\\"key\\":\\"foo\\",\\"value\\":\\"bar\\"}"'), + (Base64, '"Q2hlbG8gd29ycmxkCg=="'), + ], +) +def test_parse_literal_with_variables(input_type, input_value): + # input_b needs to be evaluated as literal while the variable dict for + # input_a is passed along. + + class Query(ObjectType): + generic = GenericScalar(input_a=GenericScalar(), input_b=input_type()) + + def resolve_generic(self, info, input_a=None, input_b=None): + return input + + schema = Schema(query=Query) + + query = f""" + query Test($a: GenericScalar){{ + generic(inputA: $a, inputB: {input_value}) + }} + """ + result = schema.execute( + query, + variables={"a": "bar"}, + ) + assert not result.errors diff --git a/graphene/tests/utils.py b/graphene/tests/utils.py deleted file mode 100644 index b9804d9b..00000000 --- a/graphene/tests/utils.py +++ /dev/null @@ -1,9 +0,0 @@ -from textwrap import dedent as _dedent - - -def dedent(text: str) -> str: - """Fix indentation of given text by removing leading spaces and tabs. - Also removes leading newlines and trailing spaces and tabs, but keeps trailing - newlines. - """ - return _dedent(text.lstrip("\n").rstrip(" \t")) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 71026d45..f9dc843b 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -1,4 +1,5 @@ from itertools import chain +from graphql import Undefined from .dynamic import Dynamic from .mountedtype import MountedType @@ -41,7 +42,7 @@ class Argument(MountedType): def __init__( self, type_, - default_value=None, + default_value=Undefined, description=None, name=None, required=False, diff --git a/graphene/types/base64.py b/graphene/types/base64.py index baedabeb..69bb3380 100644 --- a/graphene/types/base64.py +++ b/graphene/types/base64.py @@ -22,7 +22,7 @@ class Base64(Scalar): return b64encode(value).decode("utf-8") @classmethod - def parse_literal(cls, node): + def parse_literal(cls, node, _variables=None): if not isinstance(node, StringValueNode): raise GraphQLError( f"Base64 cannot represent non-string value: {print_ast(node)}" diff --git a/graphene/types/decimal.py b/graphene/types/decimal.py index b2acbe7e..0c6ccc97 100644 --- a/graphene/types/decimal.py +++ b/graphene/types/decimal.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from decimal import Decimal as _Decimal +from graphql import Undefined from graphql.language.ast import StringValueNode, IntValueNode from .scalars import Scalar @@ -22,13 +23,14 @@ class Decimal(Scalar): return str(dec) @classmethod - def parse_literal(cls, node): + def parse_literal(cls, node, _variables=None): if isinstance(node, (StringValueNode, IntValueNode)): return cls.parse_value(node.value) + return Undefined @staticmethod def parse_value(value): try: return _Decimal(value) - except ValueError: - return None + except Exception: + return Undefined diff --git a/graphene/types/definitions.py b/graphene/types/definitions.py index 908cc7c8..e5505fd3 100644 --- a/graphene/types/definitions.py +++ b/graphene/types/definitions.py @@ -7,7 +7,6 @@ from graphql import ( GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, - Undefined, ) @@ -50,7 +49,7 @@ class GrapheneEnumType(GrapheneGraphQLType, GraphQLEnumType): try: value = enum[value] except KeyError: - return Undefined + pass return super(GrapheneEnumType, self).serialize(value) diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 70e8ee8e..e5cc50ed 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -52,7 +52,10 @@ class EnumMeta(SubclassWithMeta_Meta): return super(EnumMeta, cls).__call__(*args, **kwargs) # return cls._meta.enum(*args, **kwargs) - def from_enum(cls, enum, description=None, deprecation_reason=None): # noqa: N805 + def from_enum( + cls, enum, name=None, description=None, deprecation_reason=None + ): # noqa: N805 + name = name or enum.__name__ description = description or enum.__doc__ meta_dict = { "enum": enum, @@ -60,7 +63,7 @@ class EnumMeta(SubclassWithMeta_Meta): "deprecation_reason": deprecation_reason, } meta_class = type("Meta", (object,), meta_dict) - return type(meta_class.enum.__name__, (Enum,), {"Meta": meta_class}) + return type(name, (Enum,), {"Meta": meta_class}) class Enum(UnmountedType, BaseType, metaclass=EnumMeta): diff --git a/graphene/types/generic.py b/graphene/types/generic.py index 5d1a6c4b..2a3c8d52 100644 --- a/graphene/types/generic.py +++ b/graphene/types/generic.py @@ -29,7 +29,7 @@ class GenericScalar(Scalar): parse_value = identity @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, (StringValueNode, BooleanValueNode)): return ast.value elif isinstance(ast, IntValueNode): diff --git a/graphene/types/interface.py b/graphene/types/interface.py index 77086dab..6503b78b 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -5,11 +5,12 @@ from .utils import yank_fields_from_attrs # For static type checking with Mypy MYPY = False if MYPY: - from typing import Dict # NOQA + from typing import Dict, Iterable, Type # NOQA class InterfaceOptions(BaseOptions): fields = None # type: Dict[str, Field] + interfaces = () # type: Iterable[Type[Interface]] class Interface(BaseType): @@ -45,7 +46,7 @@ class Interface(BaseType): """ @classmethod - def __init_subclass_with_meta__(cls, _meta=None, **options): + def __init_subclass_with_meta__(cls, _meta=None, interfaces=(), **options): if not _meta: _meta = InterfaceOptions(cls) @@ -58,6 +59,9 @@ class Interface(BaseType): else: _meta.fields = fields + if not _meta.interfaces: + _meta.interfaces = interfaces + super(Interface, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod diff --git a/graphene/types/json.py b/graphene/types/json.py index 4bb5061c..ca55836b 100644 --- a/graphene/types/json.py +++ b/graphene/types/json.py @@ -2,6 +2,7 @@ from __future__ import absolute_import import json +from graphql import Undefined from graphql.language.ast import StringValueNode from .scalars import Scalar @@ -20,9 +21,13 @@ class JSONString(Scalar): return json.dumps(dt) @staticmethod - def parse_literal(node): + def parse_literal(node, _variables=None): if isinstance(node, StringValueNode): - return json.loads(node.value) + try: + return json.loads(node.value) + except Exception as error: + raise ValueError(f"Badly formed JSONString: {str(error)}") + return Undefined @staticmethod def parse_value(value): diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index ca87775a..ad47c62a 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -29,21 +29,21 @@ class Mutation(ObjectType): .. code:: python - from graphene import Mutation, ObjectType, String, Boolean, Field + import graphene - class CreatePerson(Mutation): + class CreatePerson(graphene.Mutation): class Arguments: - name = String() + name = graphene.String() - ok = Boolean() - person = Field(Person) + ok = graphene.Boolean() + person = graphene.Field(Person) def mutate(parent, info, name): person = Person(name=name) ok = True return CreatePerson(person=person, ok=ok) - class Mutation(ObjectType): + class Mutation(graphene.ObjectType): create_person = CreatePerson.Field() Meta class options (optional): @@ -101,10 +101,7 @@ class Mutation(ObjectType): "Read more:" " https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#mutation-input" ) - if input_class: - arguments = props(input_class) - else: - arguments = {} + arguments = props(input_class) if input_class else {} if not resolver: mutate = getattr(cls, "mutate", None) assert mutate, "All mutations must define a mutate method in it" diff --git a/graphene/types/resolver.py b/graphene/types/resolver.py index 6a8ea02b..72d2edb8 100644 --- a/graphene/types/resolver.py +++ b/graphene/types/resolver.py @@ -7,9 +7,7 @@ def dict_resolver(attname, default_value, root, info, **args): def dict_or_attr_resolver(attname, default_value, root, info, **args): - resolver = attr_resolver - if isinstance(root, dict): - resolver = dict_resolver + resolver = dict_resolver if isinstance(root, dict) else attr_resolver return resolver(attname, default_value, root, info, **args) diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index 472f2d41..a468bb3e 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -1,5 +1,6 @@ from typing import Any +from graphql import Undefined from graphql.language.ast import ( BooleanValueNode, FloatValueNode, @@ -67,19 +68,21 @@ class Int(Scalar): try: num = int(float(value)) except ValueError: - return None + return Undefined if MIN_INT <= num <= MAX_INT: return num + return Undefined serialize = coerce_int parse_value = coerce_int @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, IntValueNode): num = int(ast.value) if MIN_INT <= num <= MAX_INT: return num + return Undefined class BigInt(Scalar): @@ -97,16 +100,17 @@ class BigInt(Scalar): try: num = int(float(value)) except ValueError: - return None + return Undefined return num serialize = coerce_int parse_value = coerce_int @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, IntValueNode): return int(ast.value) + return Undefined class Float(Scalar): @@ -122,15 +126,16 @@ class Float(Scalar): try: return float(value) except ValueError: - return None + return Undefined serialize = coerce_float parse_value = coerce_float @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, (FloatValueNode, IntValueNode)): return float(ast.value) + return Undefined class String(Scalar): @@ -143,16 +148,17 @@ class String(Scalar): @staticmethod def coerce_string(value): if isinstance(value, bool): - return u"true" if value else u"false" + return "true" if value else "false" return str(value) serialize = coerce_string parse_value = coerce_string @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, StringValueNode): return ast.value + return Undefined class Boolean(Scalar): @@ -164,9 +170,10 @@ class Boolean(Scalar): parse_value = bool @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, BooleanValueNode): return ast.value + return Undefined class ID(Scalar): @@ -182,6 +189,7 @@ class ID(Scalar): parse_value = str @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, (StringValueNode, IntValueNode)): return ast.value + return Undefined diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 0c6d4183..1a33a93d 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -233,11 +233,20 @@ class TypeMap(dict): else None ) + def interfaces(): + interfaces = [] + for graphene_interface in graphene_type._meta.interfaces: + interface = self.add_type(graphene_interface) + assert interface.graphene_type == graphene_interface + interfaces.append(interface) + return interfaces + return GrapheneInterfaceType( graphene_type=graphene_type, name=graphene_type._meta.name, description=graphene_type._meta.description, fields=partial(self.create_fields_for_type, graphene_type), + interfaces=interfaces, resolve_type=resolve_type, ) @@ -376,19 +385,11 @@ class TypeMap(dict): def resolve_type(self, resolve_type_func, type_name, root, info, _type): type_ = resolve_type_func(root, info) - if not type_: - return_type = self[type_name] - return default_type_resolver(root, info, return_type) - if inspect.isclass(type_) and issubclass(type_, ObjectType): - graphql_type = self.get(type_._meta.name) - assert graphql_type, f"Can't find type {type_._meta.name} in schema" - assert ( - graphql_type.graphene_type == type_ - ), f"The type {type_} does not match with the associated graphene type {graphql_type.graphene_type}." - return graphql_type + return type_._meta.name - return type_ + return_type = self[type_name] + return default_type_resolver(root, info, return_type) class Schema: diff --git a/graphene/types/tests/test_decimal.py b/graphene/types/tests/test_decimal.py index 9757e82c..1ba48bd1 100644 --- a/graphene/types/tests/test_decimal.py +++ b/graphene/types/tests/test_decimal.py @@ -39,8 +39,25 @@ def test_bad_decimal_query(): not_a_decimal = "Nobody expects the Spanish Inquisition!" result = schema.execute("""{ decimal(input: "%s") }""" % not_a_decimal) + assert result.errors assert len(result.errors) == 1 assert result.data is None + assert ( + result.errors[0].message + == "Expected value of type 'Decimal', found \"Nobody expects the Spanish Inquisition!\"." + ) + + result = schema.execute("{ decimal(input: true) }") + assert result.errors + assert len(result.errors) == 1 + assert result.data is None + assert result.errors[0].message == "Expected value of type 'Decimal', found true." + + result = schema.execute("{ decimal(input: 1.2) }") + assert result.errors + assert len(result.errors) == 1 + assert result.data is None + assert result.errors[0].message == "Expected value of type 'Decimal', found 1.2." def test_decimal_string_query_integer(): diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 6e204aa9..679de16e 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -251,19 +251,22 @@ def test_enum_types(): schema = Schema(query=Query) - assert str(schema) == dedent( - '''\ - type Query { - color: Color! - } + assert ( + str(schema).strip() + == dedent( + ''' + type Query { + color: Color! + } - """Primary colors""" - enum Color { - RED - YELLOW - BLUE - } - ''' + """Primary colors""" + enum Color { + RED + YELLOW + BLUE + } + ''' + ).strip() ) @@ -325,6 +328,52 @@ def test_enum_resolver_compat(): assert results.data["colorByName"] == Color.RED.name +def test_enum_with_name(): + from enum import Enum as PyEnum + + class Color(PyEnum): + RED = 1 + YELLOW = 2 + BLUE = 3 + + GColor = Enum.from_enum(Color, description="original colors") + UniqueGColor = Enum.from_enum( + Color, name="UniqueColor", description="unique colors" + ) + + class Query(ObjectType): + color = GColor(required=True) + unique_color = UniqueGColor(required=True) + + schema = Schema(query=Query) + + assert ( + str(schema).strip() + == dedent( + ''' + type Query { + color: Color! + uniqueColor: UniqueColor! + } + + """original colors""" + enum Color { + RED + YELLOW + BLUE + } + + """unique colors""" + enum UniqueColor { + RED + YELLOW + BLUE + } + ''' + ).strip() + ) + + def test_enum_resolver_invalid(): from enum import Enum as PyEnum @@ -345,10 +394,7 @@ def test_enum_resolver_invalid(): results = schema.execute("query { color }") assert results.errors - assert ( - results.errors[0].message - == "Expected a value of type 'Color' but received: 'BLACK'" - ) + assert results.errors[0].message == "Enum 'Color' cannot represent value: 'BLACK'" def test_field_enum_argument(): @@ -460,12 +506,13 @@ def test_mutation_enum_input_type(): schema = Schema(query=Query, mutation=MyMutation) result = schema.execute( - """ mutation MyMutation { - createPaint(colorInput: { color: RED }) { - color + """ + mutation MyMutation { + createPaint(colorInput: { color: RED }) { + color + } } - } - """ + """ ) assert not result.errors assert result.data == {"createPaint": {"color": "RED"}} diff --git a/graphene/types/tests/test_interface.py b/graphene/types/tests/test_interface.py index c30a8a3a..3dd4fc4f 100644 --- a/graphene/types/tests/test_interface.py +++ b/graphene/types/tests/test_interface.py @@ -25,13 +25,18 @@ def test_generate_interface(): def test_generate_interface_with_meta(): + class MyFirstInterface(Interface): + pass + class MyInterface(Interface): class Meta: name = "MyOtherInterface" description = "Documentation" + interfaces = [MyFirstInterface] assert MyInterface._meta.name == "MyOtherInterface" assert MyInterface._meta.description == "Documentation" + assert MyInterface._meta.interfaces == [MyFirstInterface] def test_generate_interface_with_fields(): diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index b5537180..bb754b3a 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -21,6 +21,10 @@ def test_jsonstring_query(): assert not result.errors assert result.data == {"json": json_value} + result = schema.execute("""{ json(input: "{}") }""") + assert not result.errors + assert result.data == {"json": "{}"} + def test_jsonstring_query_variable(): json_value = '{"key": "value"}' @@ -31,3 +35,51 @@ def test_jsonstring_query_variable(): ) assert not result.errors assert result.data == {"json": json_value} + + +def test_jsonstring_optional_uuid_input(): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ json(input: null) }") + assert not result.errors + assert result.data == {"json": None} + + +def test_jsonstring_invalid_query(): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute("{ json(input: 1) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'JSONString', found 1." + + result = schema.execute("{ json(input: {}) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'JSONString', found {}." + + result = schema.execute('{ json(input: "a") }') + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == ( + "Expected value of type 'JSONString', found \"a\"; " + "Badly formed JSONString: Expecting value: line 1 column 1 (char 0)" + ) + + result = schema.execute("""{ json(input: "{\\'key\\': 0}") }""") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Syntax Error: Invalid character escape sequence: '\\''." + ) + + result = schema.execute("""{ json(input: "{\\"key\\": 0,}") }""") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == ( + 'Expected value of type \'JSONString\', found "{\\"key\\": 0,}"; ' + "Badly formed JSONString: Expecting property name enclosed in double quotes: line 1 column 11 (char 10)" + ) diff --git a/graphene/types/tests/test_objecttype.py b/graphene/types/tests/test_objecttype.py index 1ff8fc8f..dece5e8b 100644 --- a/graphene/types/tests/test_objecttype.py +++ b/graphene/types/tests/test_objecttype.py @@ -191,21 +191,15 @@ def test_objecttype_as_container_all_kwargs(): def test_objecttype_as_container_extra_args(): - with raises(TypeError) as excinfo: - Container("1", "2", "3") - - assert "__init__() takes from 1 to 3 positional arguments but 4 were given" == str( - excinfo.value - ) + msg = r"__init__\(\) takes from 1 to 3 positional arguments but 4 were given" + with raises(TypeError, match=msg): + Container("1", "2", "3") # type: ignore def test_objecttype_as_container_invalid_kwargs(): - with raises(TypeError) as excinfo: - Container(unexisting_field="3") - - assert "__init__() got an unexpected keyword argument 'unexisting_field'" == str( - excinfo.value - ) + msg = r"__init__\(\) got an unexpected keyword argument 'unexisting_field'" + with raises(TypeError, match=msg): + Container(unexisting_field="3") # type: ignore def test_objecttype_container_benchmark(benchmark): diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 2d3e4c73..e117754f 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -229,11 +229,11 @@ def test_query_arguments(): result = test_schema.execute("{ test }", None) assert not result.errors - assert result.data == {"test": '[null,{"a_str":null,"a_int":null}]'} + assert result.data == {"test": "[null,{}]"} result = test_schema.execute('{ test(aStr: "String!") }', "Source!") assert not result.errors - assert result.data == {"test": '["Source!",{"a_str":"String!","a_int":null}]'} + assert result.data == {"test": '["Source!",{"a_str":"String!"}]'} result = test_schema.execute('{ test(aInt: -123, aStr: "String!") }', "Source!") assert not result.errors @@ -258,7 +258,7 @@ def test_query_input_field(): result = test_schema.execute("{ test }", None) assert not result.errors - assert result.data == {"test": '[null,{"a_input":null}]'} + assert result.data == {"test": "[null,{}]"} result = test_schema.execute('{ test(aInput: {aField: "String!"} ) }', "Source!") assert not result.errors diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py index 2ff67208..cbdfd8c5 100644 --- a/graphene/types/tests/test_scalar.py +++ b/graphene/types/tests/test_scalar.py @@ -1,4 +1,7 @@ -from ..scalars import Scalar, Int, BigInt +from ..objecttype import ObjectType, Field +from ..scalars import Scalar, Int, BigInt, Float, String, Boolean +from ..schema import Schema +from graphql import Undefined from graphql.language.ast import IntValueNode @@ -11,19 +14,295 @@ def test_scalar(): def test_ints(): - assert Int.parse_value(2 ** 31 - 1) is not None - assert Int.parse_value("2.0") is not None - assert Int.parse_value(2 ** 31) is None + assert Int.parse_value(2**31 - 1) is not Undefined + assert Int.parse_value("2.0") == 2 + assert Int.parse_value(2**31) is Undefined - assert Int.parse_literal(IntValueNode(value=str(2 ** 31 - 1))) == 2 ** 31 - 1 - assert Int.parse_literal(IntValueNode(value=str(2 ** 31))) is None + assert Int.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1 + assert Int.parse_literal(IntValueNode(value=str(2**31))) is Undefined - assert Int.parse_value(-(2 ** 31)) is not None - assert Int.parse_value(-(2 ** 31) - 1) is None + assert Int.parse_value(-(2**31)) is not Undefined + assert Int.parse_value(-(2**31) - 1) is Undefined - assert BigInt.parse_value(2 ** 31) is not None - assert BigInt.parse_value("2.0") is not None - assert BigInt.parse_value(-(2 ** 31) - 1) is not None + assert BigInt.parse_value(2**31) is not Undefined + assert BigInt.parse_value("2.0") == 2 + assert BigInt.parse_value(-(2**31) - 1) is not Undefined - assert BigInt.parse_literal(IntValueNode(value=str(2 ** 31 - 1))) == 2 ** 31 - 1 - assert BigInt.parse_literal(IntValueNode(value=str(2 ** 31))) == 2 ** 31 + assert BigInt.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1 + assert BigInt.parse_literal(IntValueNode(value=str(2**31))) == 2**31 + + +def return_input(_parent, _info, input): + return input + + +class Optional(ObjectType): + int = Int(input=Int(), resolver=return_input) + big_int = BigInt(input=BigInt(), resolver=return_input) + float = Float(input=Float(), resolver=return_input) + bool = Boolean(input=Boolean(), resolver=return_input) + string = String(input=String(), resolver=return_input) + + +class Query(ObjectType): + optional = Field(Optional) + + def resolve_optional(self, info): + return Optional() + + def resolve_required(self, info, input): + return input + + +schema = Schema(query=Query) + + +class TestInt: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute("{ optional { int(input: 20) } }") + assert not result.errors + assert result.data == {"optional": {"int": 20}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { int(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"int": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { int(input: "20") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == 'Int cannot represent non-integer value: "20"' + ) + + result = schema.execute('{ optional { int(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == 'Int cannot represent non-integer value: "a"' + + result = schema.execute("{ optional { int(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Int cannot represent non-integer value: true" + ) + + +class TestBigInt: + def test_query(self): + """ + Test that a normal query works. + """ + value = 2**31 + result = schema.execute("{ optional { bigInt(input: %s) } }" % value) + assert not result.errors + assert result.data == {"optional": {"bigInt": value}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { bigInt(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"bigInt": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { bigInt(input: "20") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Expected value of type 'BigInt', found \"20\"." + ) + + result = schema.execute('{ optional { bigInt(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Expected value of type 'BigInt', found \"a\"." + ) + + result = schema.execute("{ optional { bigInt(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Expected value of type 'BigInt', found true." + ) + + +class TestFloat: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute("{ optional { float(input: 20) } }") + assert not result.errors + assert result.data == {"optional": {"float": 20.0}} + + result = schema.execute("{ optional { float(input: 20.2) } }") + assert not result.errors + assert result.data == {"optional": {"float": 20.2}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { float(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"float": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { float(input: "20") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == 'Float cannot represent non numeric value: "20"' + ) + + result = schema.execute('{ optional { float(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == 'Float cannot represent non numeric value: "a"' + ) + + result = schema.execute("{ optional { float(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Float cannot represent non numeric value: true" + ) + + +class TestBoolean: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute("{ optional { bool(input: true) } }") + assert not result.errors + assert result.data == {"optional": {"bool": True}} + + result = schema.execute("{ optional { bool(input: false) } }") + assert not result.errors + assert result.data == {"optional": {"bool": False}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { bool(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"bool": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { bool(input: "True") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == 'Boolean cannot represent a non boolean value: "True"' + ) + + result = schema.execute('{ optional { bool(input: "true") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == 'Boolean cannot represent a non boolean value: "true"' + ) + + result = schema.execute('{ optional { bool(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == 'Boolean cannot represent a non boolean value: "a"' + ) + + result = schema.execute("{ optional { bool(input: 1) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Boolean cannot represent a non boolean value: 1" + ) + + result = schema.execute("{ optional { bool(input: 0) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Boolean cannot represent a non boolean value: 0" + ) + + +class TestString: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute('{ optional { string(input: "something something") } }') + assert not result.errors + assert result.data == {"optional": {"string": "something something"}} + + result = schema.execute('{ optional { string(input: "True") } }') + assert not result.errors + assert result.data == {"optional": {"string": "True"}} + + result = schema.execute('{ optional { string(input: "0") } }') + assert not result.errors + assert result.data == {"optional": {"string": "0"}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { string(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"string": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute("{ optional { string(input: 1) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "String cannot represent a non string value: 1" + ) + + result = schema.execute("{ optional { string(input: 3.2) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "String cannot represent a non string value: 3.2" + ) + + result = schema.execute("{ optional { string(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "String cannot represent a non string value: true" + ) diff --git a/graphene/types/tests/test_scalars_serialization.py b/graphene/types/tests/test_scalars_serialization.py index a95e8bd4..a0028c85 100644 --- a/graphene/types/tests/test_scalars_serialization.py +++ b/graphene/types/tests/test_scalars_serialization.py @@ -1,3 +1,4 @@ +from graphql import Undefined from ..scalars import Boolean, Float, Int, String @@ -9,12 +10,12 @@ def test_serializes_output_int(): assert Int.serialize(1.1) == 1 assert Int.serialize(-1.1) == -1 assert Int.serialize(1e5) == 100000 - assert Int.serialize(9876504321) is None - assert Int.serialize(-9876504321) is None - assert Int.serialize(1e100) is None - assert Int.serialize(-1e100) is None + assert Int.serialize(9876504321) is Undefined + assert Int.serialize(-9876504321) is Undefined + assert Int.serialize(1e100) is Undefined + assert Int.serialize(-1e100) is Undefined assert Int.serialize("-1.1") == -1 - assert Int.serialize("one") is None + assert Int.serialize("one") is Undefined assert Int.serialize(False) == 0 assert Int.serialize(True) == 1 @@ -27,7 +28,7 @@ def test_serializes_output_float(): assert Float.serialize(1.1) == 1.1 assert Float.serialize(-1.1) == -1.1 assert Float.serialize("-1.1") == -1.1 - assert Float.serialize("one") is None + assert Float.serialize("one") is Undefined assert Float.serialize(False) == 0 assert Float.serialize(True) == 1 @@ -38,7 +39,7 @@ def test_serializes_output_string(): assert String.serialize(-1.1) == "-1.1" assert String.serialize(True) == "true" assert String.serialize(False) == "false" - assert String.serialize(u"\U0001F601") == u"\U0001F601" + assert String.serialize("\U0001F601") == "\U0001F601" def test_serializes_output_boolean(): diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index fe4739c9..c03c81ba 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -1,7 +1,8 @@ -from graphql.type import GraphQLObjectType, GraphQLSchema +from textwrap import dedent + from pytest import raises -from graphene.tests.utils import dedent +from graphql.type import GraphQLObjectType, GraphQLSchema from ..field import Field from ..objecttype import ObjectType @@ -43,8 +44,10 @@ def test_schema_get_type_error(): def test_schema_str(): schema = Schema(Query) - assert str(schema) == dedent( - """ + assert ( + str(schema).strip() + == dedent( + """ type Query { inner: MyOtherType } @@ -53,6 +56,7 @@ def test_schema_str(): field: String } """ + ).strip() ) diff --git a/graphene/types/tests/test_subscribe_async.py b/graphene/types/tests/test_subscribe_async.py index 9b7a1f13..50e5ba68 100644 --- a/graphene/types/tests/test_subscribe_async.py +++ b/graphene/types/tests/test_subscribe_async.py @@ -14,9 +14,7 @@ class Subscription(ObjectType): count_to_ten = Field(Int) async def subscribe_count_to_ten(root, info): - count = 0 - while count < 10: - count += 1 + for count in range(1, 11): yield count diff --git a/graphene/types/tests/test_type_map.py b/graphene/types/tests/test_type_map.py index 12e7a1f4..55b1706e 100644 --- a/graphene/types/tests/test_type_map.py +++ b/graphene/types/tests/test_type_map.py @@ -1,3 +1,4 @@ +from graphql import Undefined from graphql.type import ( GraphQLArgument, GraphQLEnumType, @@ -244,7 +245,9 @@ def test_objecttype_camelcase(): foo_field = fields["fooBar"] assert isinstance(foo_field, GraphQLField) assert foo_field.args == { - "barFoo": GraphQLArgument(GraphQLString, default_value=None, out_name="bar_foo") + "barFoo": GraphQLArgument( + GraphQLString, default_value=Undefined, out_name="bar_foo" + ) } @@ -267,7 +270,7 @@ def test_objecttype_camelcase_disabled(): assert isinstance(foo_field, GraphQLField) assert foo_field.args == { "bar_foo": GraphQLArgument( - GraphQLString, default_value=None, out_name="bar_foo" + GraphQLString, default_value=Undefined, out_name="bar_foo" ) } @@ -286,3 +289,33 @@ def test_objecttype_with_possible_types(): assert graphql_type.is_type_of assert graphql_type.is_type_of({}, None) is True assert graphql_type.is_type_of(MyObjectType(), None) is False + + +def test_interface_with_interfaces(): + class FooInterface(Interface): + foo = String() + + class BarInterface(Interface): + class Meta: + interfaces = [FooInterface] + + foo = String() + bar = String() + + type_map = create_type_map([FooInterface, BarInterface]) + assert "FooInterface" in type_map + foo_graphql_type = type_map["FooInterface"] + assert isinstance(foo_graphql_type, GraphQLInterfaceType) + assert foo_graphql_type.name == "FooInterface" + + assert "BarInterface" in type_map + bar_graphql_type = type_map["BarInterface"] + assert isinstance(bar_graphql_type, GraphQLInterfaceType) + assert bar_graphql_type.name == "BarInterface" + + fields = bar_graphql_type.fields + assert list(fields) == ["foo", "bar"] + assert isinstance(fields["foo"], GraphQLField) + assert isinstance(fields["bar"], GraphQLField) + + assert list(bar_graphql_type.interfaces) == list([foo_graphql_type]) diff --git a/graphene/types/tests/test_uuid.py b/graphene/types/tests/test_uuid.py index 2280b41f..d34f1664 100644 --- a/graphene/types/tests/test_uuid.py +++ b/graphene/types/tests/test_uuid.py @@ -1,14 +1,19 @@ from ..objecttype import ObjectType from ..schema import Schema from ..uuid import UUID +from ..structures import NonNull class Query(ObjectType): uuid = UUID(input=UUID()) + required_uuid = UUID(input=NonNull(UUID), required=True) def resolve_uuid(self, info, input): return input + def resolve_required_uuid(self, info, input): + return input + schema = Schema(query=Query) @@ -29,3 +34,35 @@ def test_uuidstring_query_variable(): ) assert not result.errors assert result.data == {"uuid": uuid_value} + + +def test_uuidstring_optional_uuid_input(): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ uuid(input: null) }") + assert not result.errors + assert result.data == {"uuid": None} + + +def test_uuidstring_invalid_query(): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute("{ uuid(input: 1) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'UUID', found 1." + + result = schema.execute('{ uuid(input: "a") }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Expected value of type 'UUID', found \"a\"; badly formed hexadecimal UUID string" + ) + + result = schema.execute("{ requiredUuid(input: null) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'UUID!', found null." diff --git a/graphene/types/union.py b/graphene/types/union.py index 928656ae..f77e833a 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -21,7 +21,7 @@ class Union(UnmountedType, BaseType): to determine which type is actually used when the field is resolved. The schema in this example can take a search text and return any of the GraphQL object types - indicated: Human, Droid or Startship. + indicated: Human, Droid or Starship. Ambiguous return types can be resolved on each ObjectType through ``Meta.possible_types`` attribute or ``is_type_of`` method. Or by implementing ``resolve_type`` class method on the diff --git a/graphene/types/uuid.py b/graphene/types/uuid.py index c21eb165..f2ba1fcb 100644 --- a/graphene/types/uuid.py +++ b/graphene/types/uuid.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from uuid import UUID as _UUID from graphql.language.ast import StringValueNode +from graphql import Undefined from .scalars import Scalar @@ -21,9 +22,10 @@ class UUID(Scalar): return str(uuid) @staticmethod - def parse_literal(node): + def parse_literal(node, _variables=None): if isinstance(node, StringValueNode): return _UUID(node.value) + return Undefined @staticmethod def parse_value(value): diff --git a/graphene/utils/module_loading.py b/graphene/utils/module_loading.py index 25dc86ca..d9095d0a 100644 --- a/graphene/utils/module_loading.py +++ b/graphene/utils/module_loading.py @@ -27,19 +27,18 @@ def import_string(dotted_path, dotted_attributes=None): if not dotted_attributes: return result - else: - attributes = dotted_attributes.split(".") - traveled_attributes = [] - try: - for attribute in attributes: - traveled_attributes.append(attribute) - result = getattr(result, attribute) - return result - except AttributeError: - raise ImportError( - 'Module "%s" does not define a "%s" attribute inside attribute/class "%s"' - % (module_path, ".".join(traveled_attributes), class_name) - ) + attributes = dotted_attributes.split(".") + traveled_attributes = [] + try: + for attribute in attributes: + traveled_attributes.append(attribute) + result = getattr(result, attribute) + return result + except AttributeError: + raise ImportError( + 'Module "%s" does not define a "%s" attribute inside attribute/class "%s"' + % (module_path, ".".join(traveled_attributes), class_name) + ) def lazy_import(dotted_path, dotted_attributes=None): diff --git a/graphene/utils/tests/test_deduplicator.py b/graphene/utils/tests/test_deduplicator.py index b845caf1..95a70e74 100644 --- a/graphene/utils/tests/test_deduplicator.py +++ b/graphene/utils/tests/test_deduplicator.py @@ -94,6 +94,7 @@ TEST_DATA = { ], "movies": { "1198359": { + "id": "1198359", "name": "King Arthur: Legend of the Sword", "synopsis": ( "When the child Arthur's father is murdered, Vortigern, " @@ -159,7 +160,7 @@ def test_example_end_to_end(): "date": "2017-05-19", "movie": { "__typename": "Movie", - "id": "TW92aWU6Tm9uZQ==", + "id": "TW92aWU6MTE5ODM1OQ==", "name": "King Arthur: Legend of the Sword", "synopsis": ( "When the child Arthur's father is murdered, Vortigern, " @@ -172,7 +173,7 @@ def test_example_end_to_end(): "__typename": "Event", "id": "RXZlbnQ6MjM0", "date": "2017-05-20", - "movie": {"__typename": "Movie", "id": "TW92aWU6Tm9uZQ=="}, + "movie": {"__typename": "Movie", "id": "TW92aWU6MTE5ODM1OQ=="}, }, ] } diff --git a/graphene/utils/tests/test_orderedtype.py b/graphene/utils/tests/test_orderedtype.py index ea6c7cc0..ad5bd77a 100644 --- a/graphene/utils/tests/test_orderedtype.py +++ b/graphene/utils/tests/test_orderedtype.py @@ -38,4 +38,4 @@ def test_orderedtype_non_orderabletypes(): assert one.__lt__(1) == NotImplemented assert one.__gt__(1) == NotImplemented - assert not one == 1 + assert one != 1 diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index 958a1afa..149ac628 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -18,14 +18,12 @@ schema = Schema(query=Query) def run_query(query: str): document = parse(query) - errors = validate( + return validate( schema=schema.graphql_schema, document_ast=document, rules=(DisableIntrospection,), ) - return errors - def test_disallows_introspection_queries(): errors = run_query("{ __schema { queryType { name } } }") diff --git a/setup.py b/setup.py index ae59a92a..b87f56cc 100644 --- a/setup.py +++ b/setup.py @@ -45,20 +45,21 @@ class PyTest(TestCommand): tests_require = [ - "pytest>=5.3,<6", - "pytest-benchmark>=3.2,<4", - "pytest-cov>=2.8,<3", - "pytest-mock>=2,<3", - "pytest-asyncio>=0.10,<2", - "snapshottest>=0.5,<1", - "coveralls>=1.11,<2", + "pytest>=6,<7", + "pytest-benchmark>=3.4,<4", + "pytest-cov>=3,<4", + "pytest-mock>=3,<4", + "pytest-asyncio>=0.16,<2", + "snapshottest>=0.6,<1", + "coveralls>=3.3,<4", "promise>=2.3,<3", - "mock>=4.0,<5", - "pytz==2021.1", - "iso8601>=0.1,<2", + "aiodataloader<1", + "mock>=4,<5", + "pytz==2022.1", + "iso8601>=1,<2", ] -dev_requires = ["black==19.10b0", "flake8>=3.7,<4"] + tests_require +dev_requires = ["black==22.3.0", "flake8>=4,<5"] + tests_require setup( name="graphene", @@ -78,12 +79,14 @@ setup( "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["examples*"]), install_requires=[ - "graphql-core~=3.1.2", - "graphql-relay>=3.0,<4", + "graphql-core>=3.1,<3.3", + "graphql-relay>=3.1,<3.3", "aniso8601>=8,<10", ], tests_require=tests_require, diff --git a/tests_asyncio/test_dataloader.py b/tests_asyncio/test_dataloader.py new file mode 100644 index 00000000..fb8d1630 --- /dev/null +++ b/tests_asyncio/test_dataloader.py @@ -0,0 +1,79 @@ +from collections import namedtuple +from unittest.mock import Mock +from pytest import mark +from aiodataloader import DataLoader + +from graphene import ObjectType, String, Schema, Field, List + + +CHARACTERS = { + "1": {"name": "Luke Skywalker", "sibling": "3"}, + "2": {"name": "Darth Vader", "sibling": None}, + "3": {"name": "Leia Organa", "sibling": "1"}, +} + + +get_character = Mock(side_effect=lambda character_id: CHARACTERS[character_id]) + + +class CharacterType(ObjectType): + name = String() + sibling = Field(lambda: CharacterType) + + async def resolve_sibling(character, info): + if character["sibling"]: + return await info.context.character_loader.load(character["sibling"]) + return None + + +class Query(ObjectType): + skywalker_family = List(CharacterType) + + async def resolve_skywalker_family(_, info): + return await info.context.character_loader.load_many(["1", "2", "3"]) + + +mock_batch_load_fn = Mock( + side_effect=lambda character_ids: [get_character(id) for id in character_ids] +) + + +class CharacterLoader(DataLoader): + async def batch_load_fn(self, character_ids): + return mock_batch_load_fn(character_ids) + + +Context = namedtuple("Context", "character_loader") + + +@mark.asyncio +async def test_basic_dataloader(): + schema = Schema(query=Query) + + character_loader = CharacterLoader() + context = Context(character_loader=character_loader) + + query = """ + { + skywalkerFamily { + name + sibling { + name + } + } + } + """ + + result = await schema.execute_async(query, context=context) + + assert not result.errors + assert result.data == { + "skywalkerFamily": [ + {"name": "Luke Skywalker", "sibling": {"name": "Leia Organa"}}, + {"name": "Darth Vader", "sibling": None}, + {"name": "Leia Organa", "sibling": {"name": "Luke Skywalker"}}, + ] + } + + assert mock_batch_load_fn.call_count == 1 + assert get_character.call_count == 3 diff --git a/tox.ini b/tox.ini index c4bf6ad0..07ddc767 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py36,py37,py38,pre-commit,mypy +envlist = py3{6,7,8,9,10}, flake8, mypy, pre-commit skipsdist = true [testenv] @@ -8,28 +8,28 @@ deps = setenv = PYTHONPATH = .:{envdir} commands = - py{36,37,38}: pytest --cov=graphene graphene examples {posargs} + py{36,37,38,39,310}: pytest --cov=graphene graphene --cov-report=term --cov-report=xml examples {posargs} [testenv:pre-commit] -basepython=python3.8 +basepython = python3.9 deps = - pre-commit>=2,<3 + pre-commit>=2.16,<3 setenv = LC_CTYPE=en_US.UTF-8 commands = pre-commit run --all-files --show-diff-on-failure [testenv:mypy] -basepython=python3.8 +basepython = python3.9 deps = - mypy>=0.761,<1 + mypy>=0.950,<1 commands = mypy graphene [testenv:flake8] -basepython=python3.8 +basepython = python3.9 deps = - flake8>=3.8,<4 + flake8>=4,<5 commands = pip install --pre -e . flake8 graphene