From 43d6eeb48bf656b0231abcdf74b8bcbc2ac913e0 Mon Sep 17 00:00:00 2001 From: Samuel Cormier-Iijima Date: Mon, 3 Jun 2024 15:32:22 -0400 Subject: [PATCH] Update packaging + CI (#3) * Update packaging + CI * add packages * add matrix * python 3.7 * back to 3.10 --- .github/workflows/release.yml | 25 ++ .github/workflows/test.yml | 36 +++ .gitignore | 168 +++++++++++-- LICENSE | 21 -- LICENSE.md | 29 +++ readme.md => README.md | 10 +- graphql_core_promise/__init__.py | 2 +- graphql_core_promise/execute/__init__.py | 2 +- graphql_core_promise/execute/promise.py | 232 ++++++++---------- .../py.typed | 0 pyproject.toml | 59 +++-- tests/test_execute.py | 7 +- 12 files changed, 394 insertions(+), 197 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml delete mode 100644 LICENSE create mode 100644 LICENSE.md rename readme.md => README.md (76%) rename tests/__init__.py => graphql_core_promise/py.typed (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1d96a27 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Release + +on: + release: + types: + - created + +jobs: + build: + runs-on: ubuntu-latest + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install uv + - run: uv venv + - run: uv pip install --requirement pyproject.toml + - run: uv pip install setuptools setuptools-scm wheel build + - run: .venv/bin/python -m build --no-isolation + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..aa33c41 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: pip install uv + - run: uv venv + - run: uv pip install --requirement pyproject.toml --all-extras + - run: uv pip install --editable . + - run: .venv/bin/pytest + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - run: pip install uv + - run: uv venv + - run: uv pip install --requirement pyproject.toml --all-extras + - run: .venv/bin/ruff format --check graphql_core_promise tests + - run: .venv/bin/ruff graphql_core_promise tests + - run: .venv/bin/mypy graphql_core_promise diff --git a/.gitignore b/.gitignore index 3ad127b..68bc17f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,160 @@ -.cache/ -.coverage -.env*/ -.idea/ -.mypy_cache/ -.pytest_cache/ -.tox/ -.venv*/ -.vs/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so +# Distribution / packaging +.Python build/ +develop-eggs/ dist/ -docs/_build/ -htmlcov/ -pip-wheel-metadata/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST -play/ +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec -__pycache__/ +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml *.cover -*.egg -*.egg-info +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: *.log -*.py[cod] \ No newline at end of file +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index a95b789..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Fellow - -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. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d071125 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2019, Fellow Insights Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/readme.md b/README.md similarity index 76% rename from readme.md rename to README.md index 86aa878..a8feca7 100644 --- a/readme.md +++ b/README.md @@ -1,6 +1,6 @@ # Graphql core promise -> Add support for promise-based dataloaders and resolvers to graphql-core v3+. This aims to make migrating to graphene 3 and graphql-core 3 easier for existing projects. +Add support for promise-based dataloaders and resolvers to graphql-core v3+. This aims to make migrating to graphene 3 and graphql-core 3 easier for existing projects. ## Usage @@ -14,9 +14,11 @@ execute(schema=..., document=..., execution_context_class=PromiseExecutionContex ``` ### With Django + graphene-django's `GraphqlView` accepts a `execution_context_class` argument in the constructor. Or you can specify it as a class variable when subclassing. For example: + ```python view = GraphQLView.as_view(execution_context_class=PromiseExecutionContext) # OR @@ -24,8 +26,8 @@ class MyGraphQLView(GraphQLView): execution_context_class = PromiseExecutionContext ``` -Note that this project requires graphene-django 3, which is not fully compatible with graphene-django 2. - +Note that this project requires graphene-django 3, which is not fully compatible with graphene-django 2. ### How it works -This packages is done by translating the asyncio code in the default `ExecuteContext` into promise based code. + +This packages is done by translating the asyncio code in the default `ExecuteContext` into promise based code. diff --git a/graphql_core_promise/__init__.py b/graphql_core_promise/__init__.py index d5b1081..1a06aa2 100644 --- a/graphql_core_promise/__init__.py +++ b/graphql_core_promise/__init__.py @@ -1,3 +1,3 @@ from .execute import PromiseExecutionContext -__all__ = ["PromiseExecutionContext"] \ No newline at end of file +__all__ = ["PromiseExecutionContext"] diff --git a/graphql_core_promise/execute/__init__.py b/graphql_core_promise/execute/__init__.py index 66594a1..70f9bfd 100644 --- a/graphql_core_promise/execute/__init__.py +++ b/graphql_core_promise/execute/__init__.py @@ -1,3 +1,3 @@ from .promise import PromiseExecutionContext -__all__ = ["PromiseExecutionContext"] \ No newline at end of file +__all__ = ["PromiseExecutionContext"] diff --git a/graphql_core_promise/execute/promise.py b/graphql_core_promise/execute/promise.py index 842ee7e..c5ae1a9 100644 --- a/graphql_core_promise/execute/promise.py +++ b/graphql_core_promise/execute/promise.py @@ -1,16 +1,13 @@ +from collections.abc import AsyncIterable, Callable, Iterable from functools import partial from typing import ( Any, - AsyncIterable, - Dict, - Iterable, - List, - Optional, + TypeAlias, TypeVar, - Union, cast, ) +import promise from graphql.error import located_error from graphql.error.graphql_error import GraphQLError from graphql.execution.execute import ( @@ -18,31 +15,34 @@ get_field_def, invalid_return_type_error, ) +from graphql.execution.middleware import MiddlewareManager from graphql.execution.values import get_argument_values -from graphql.language.ast import FieldNode -from graphql.pyutils import inspect, is_iterable +from graphql.language.ast import ( + FieldNode, + FragmentDefinitionNode, + OperationDefinitionNode, +) +from graphql.pyutils import AwaitableOrValue, is_iterable from graphql.pyutils.path import Path from graphql.pyutils.undefined import Undefined +from graphql.type import GraphQLSchema from graphql.type.definition import ( GraphQLAbstractType, - GraphQLLeafType, + GraphQLFieldResolver, GraphQLList, - GraphQLNonNull, GraphQLObjectType, GraphQLOutputType, GraphQLResolveInfo, - is_abstract_type, - is_leaf_type, - is_list_type, - is_non_null_type, - is_object_type, + GraphQLTypeResolver, ) - -import promise from promise import Promise T = TypeVar("T") -PromiseOrValue = Union[Promise[T], T] +PromiseOrValue: TypeAlias = Promise[T] | T + + +def is_thenable(value: Any) -> bool: + return promise.is_thenable(value) class PromiseExecutionContext(ExecutionContext): @@ -52,23 +52,53 @@ class PromiseExecutionContext(ExecutionContext): resolvers can continue to function """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.is_awaitable = self.is_promise = promise.is_thenable + def __init__( + self, + schema: GraphQLSchema, + fragments: dict[str, FragmentDefinitionNode], + root_value: Any, + context_value: Any, + operation: OperationDefinitionNode, + variable_values: dict[str, Any], + field_resolver: GraphQLFieldResolver, + type_resolver: GraphQLTypeResolver, + subscribe_field_resolver: GraphQLFieldResolver, + errors: list[GraphQLError], + middleware_manager: MiddlewareManager | None, + is_awaitable: Callable[[Any], bool] | None, + ) -> None: + super().__init__( + schema=schema, + fragments=fragments, + root_value=root_value, + context_value=context_value, + operation=operation, + variable_values=variable_values, + field_resolver=field_resolver, + type_resolver=type_resolver, + subscribe_field_resolver=subscribe_field_resolver, + errors=errors, + middleware_manager=middleware_manager, + is_awaitable=None, + ) + + is_awaitable = is_promise = staticmethod(is_thenable) - def execute_operation(self, *args, **kwargs): + def execute_operation( + self, operation: OperationDefinitionNode, root_value: Any + ) -> AwaitableOrValue[Any] | None: super_exec = super().execute_operation - result = Promise.resolve(None).then(lambda _: super_exec(*args, **kwargs)) + result = Promise.resolve(None).then(lambda _: super_exec(operation, root_value)) return result.get() - def execute_fields_serially( + def execute_fields_serially( # type: ignore[override] self, parent_type: GraphQLObjectType, source_value: Any, - path: Optional[Path], - fields: Dict[str, List[FieldNode]], - ) -> PromiseOrValue[Dict[str, Any]]: - results: PromiseOrValue[Dict[str, Any]] = {} + path: Path | None, + fields: dict[str, list[FieldNode]], + ) -> PromiseOrValue[dict[str, Any]]: + results: PromiseOrValue[dict[str, Any]] = {} is_promise = self.is_promise for response_name, field_nodes in fields.items(): field_path = Path(path, response_name, parent_type.name) @@ -80,10 +110,10 @@ def execute_fields_serially( if is_promise(results): # noinspection PyShadowingNames def await_and_set_result( - results: Promise[Dict[str, Any]], + results: Promise[dict[str, Any]], response_name: str, result: PromiseOrValue[Any], - ) -> Promise[Dict[str, Any]]: + ) -> Promise[dict[str, Any]]: def handle_results(resolved_results): if is_promise(result): @@ -104,10 +134,10 @@ def on_resolve(v): elif is_promise(result): # noinspection PyShadowingNames def set_result( - results: Dict[str, Any], + results: dict[str, Any], response_name: str, result: Promise, - ) -> Promise[Dict[str, Any]]: + ) -> Promise[dict[str, Any]]: def on_resolve(v): results[response_name] = v return results @@ -115,17 +145,17 @@ def on_resolve(v): return result.then(on_resolve) results = set_result( - cast(Dict[str, Any], results), response_name, result + cast(dict[str, Any], results), response_name, result ) else: - cast(Dict[str, Any], results)[response_name] = result + cast(dict[str, Any], results)[response_name] = result return results def execute_field( self, parent_type: GraphQLObjectType, source: Any, - field_nodes: List[FieldNode], + field_nodes: list[FieldNode], path: Path, ) -> PromiseOrValue[Any]: field_def = get_field_def(self.schema, parent_type, field_nodes[0]) @@ -152,7 +182,7 @@ def execute_field( result = resolve_fn(source, info, **args) if self.is_promise(result): - result: Promise = result + assert isinstance(result, Promise) # noinspection PyShadowingNames def await_result() -> Any: @@ -174,6 +204,8 @@ def handle_error(raw_error): return_type, field_nodes, info, path, result ) if self.is_promise(completed): + assert isinstance(completed, Promise) + # noinspection PyShadowingNames def await_completed() -> Any: def handle_error(raw_error): @@ -191,16 +223,16 @@ def handle_error(raw_error): self.handle_field_error(error, return_type) return None - def execute_fields( + def execute_fields( # type: ignore[override] self, parent_type: GraphQLObjectType, source_value: Any, - path: Optional[Path], - fields: Dict[str, List[FieldNode]], - ) -> PromiseOrValue[Dict[str, Any]]: + path: Path | None, + fields: dict[str, list[FieldNode]], + ) -> PromiseOrValue[dict[str, Any]]: results = {} is_promise = self.is_promise - awaitable_fields: List[str] = [] + awaitable_fields: list[str] = [] append_awaitable = awaitable_fields.append for response_name, field_nodes in fields.items(): field_path = Path(path, response_name, parent_type.name) @@ -215,12 +247,14 @@ def execute_fields( if not awaitable_fields: return results - def get_results() -> dict[str, Any]: + def get_results() -> PromiseOrValue[dict[str, Any]]: r = [results[field] for field in awaitable_fields] if len(r) > 1: def on_all_resolve(resolved_results: list[Any]): - for field, result in zip(awaitable_fields, resolved_results): + for field, result in zip( + awaitable_fields, resolved_results, strict=False + ): results[field] = result return results @@ -236,14 +270,14 @@ def on_single_resolve(resolved): return get_results() - def complete_object_value( + def complete_object_value( # type: ignore[override] self, return_type: GraphQLObjectType, - field_nodes: List[FieldNode], + field_nodes: list[FieldNode], info: GraphQLResolveInfo, path: Path, result: Any, - ) -> PromiseOrValue[Dict[str, Any]]: + ) -> PromiseOrValue[dict[str, Any]]: # Collect sub-fields to execute to complete this value. sub_field_nodes = self.collect_subfields(return_type, field_nodes) @@ -254,10 +288,9 @@ def complete_object_value( is_type_of = return_type.is_type_of(result, info) if self.is_promise(is_type_of): + assert isinstance(is_type_of, Promise) - def execute_subfields_async() -> Dict[str, Any]: - is_type_of = cast(Promise, is_type_of) - + def execute_subfields_async() -> PromiseOrValue[dict[str, Any]]: def on_is_type_of_resolve(v): if not v: raise ( @@ -280,73 +313,10 @@ def on_is_type_of_resolve(v): return self.execute_fields(return_type, result, path, sub_field_nodes) - def complete_value( - self, - return_type: GraphQLOutputType, - field_nodes: List[FieldNode], - info: GraphQLResolveInfo, - path: Path, - result: Any, - ) -> PromiseOrValue[Any]: - # If result is an Exception, throw a located error. - if isinstance(result, Exception): - raise result - - # If field type is NonNull, complete for inner type, and throw field error if - # result is null. - if is_non_null_type(return_type): - completed = self.complete_value( - cast(GraphQLNonNull, return_type).of_type, - field_nodes, - info, - path, - result, - ) - if completed is None: - raise TypeError( - "Cannot return null for non-nullable field" - f" {info.parent_type.name}.{info.field_name}." - ) - return completed - - # If result value is null or undefined then return null. - if result is None or result is Undefined: - return None - - # If field type is List, complete each item in the list with inner type - if is_list_type(return_type): - return self.complete_list_value( - cast(GraphQLList, return_type), field_nodes, info, path, result - ) - - # If field type is a leaf type, Scalar or Enum, serialize to a valid value, - # returning null if serialization is not possible. - if is_leaf_type(return_type): - return self.complete_leaf_value(cast(GraphQLLeafType, return_type), result) - - # If field type is an abstract type, Interface or Union, determine the runtime - # Object type and complete for that type. - if is_abstract_type(return_type): - return self.complete_abstract_value( - cast(GraphQLAbstractType, return_type), field_nodes, info, path, result - ) - - # If field type is Object, execute and complete all sub-selections. - if is_object_type(return_type): - return self.complete_object_value( - cast(GraphQLObjectType, return_type), field_nodes, info, path, result - ) - - # Not reachable. All possible output types have been considered. - raise TypeError( # pragma: no cover - "Cannot complete value of unexpected output type:" - f" '{inspect(return_type)}'." - ) - def complete_abstract_value( self, return_type: GraphQLAbstractType, - field_nodes: List[FieldNode], + field_nodes: list[FieldNode], info: GraphQLResolveInfo, path: Path, result: Any, @@ -355,7 +325,7 @@ def complete_abstract_value( runtime_type = resolve_type_fn(result, info, return_type) # type: ignore if self.is_promise(runtime_type): - runtime_type = cast(Promise, runtime_type) + assert isinstance(runtime_type, Promise) def await_complete_object_value() -> Any: def on_runtime_type_resolve(resolved_runtime_type): @@ -374,10 +344,10 @@ def on_runtime_type_resolve(resolved_runtime_type): ) value = runtime_type.then(on_runtime_type_resolve) - return value # pragma: no cover + return value return await_complete_object_value() - runtime_type = cast(Optional[str], runtime_type) + runtime_type = cast(str | None, runtime_type) return self.complete_object_value( self.ensure_valid_runtime_type( @@ -389,14 +359,14 @@ def on_runtime_type_resolve(resolved_runtime_type): result, ) - def complete_list_value( + def complete_list_value( # type: ignore[override] self, return_type: GraphQLList[GraphQLOutputType], - field_nodes: List[FieldNode], + field_nodes: list[FieldNode], info: GraphQLResolveInfo, path: Path, - result: Union[Iterable[Any], Iterable[Promise[Any]]], - ) -> PromiseOrValue[List[Any]]: + result: Iterable[Any] | Iterable[Promise[Any]], + ) -> PromiseOrValue[list[Any]]: """Complete a list value. Complete a list value by completing each item in the list with the inner type. @@ -405,12 +375,14 @@ def complete_list_value( # experimental: allow async iterables if isinstance(result, AsyncIterable): # This should never happen in a promise context - raise Exception("what's going on?") + msg = "what's going on?" + raise Exception(msg) - raise GraphQLError( + msg = ( "Expected Iterable, but did not find one for field" f" '{info.parent_type.name}.{info.field_name}'." ) + raise GraphQLError(msg) result = cast(Iterable[Any], result) # This is specified as a simple map, however we're optimizing the path where @@ -418,9 +390,9 @@ def complete_list_value( # object. item_type = return_type.of_type is_promise = self.is_promise - awaitable_indices: List[int] = [] + awaitable_indices: list[int] = [] append_awaitable = awaitable_indices.append - completed_results: List[Any] = [] + completed_results: list[Any] = [] append_result = completed_results.append for index, item in enumerate(result): # No need to modify the info object containing the path, since from here on @@ -453,6 +425,8 @@ def on_item_resolve(item_value): item_type, field_nodes, info, item_path, item ) if is_promise(completed_item): + assert isinstance(completed_item, Promise) + # noinspection PyShadowingNames def await_completed(item: Promise[Any], item_path: Path) -> Any: def on_error(raw_error): @@ -477,7 +451,7 @@ def on_error(raw_error): return completed_results # noinspection PyShadowingNames - def get_completed_results() -> list[Any]: + def get_completed_results() -> PromiseOrValue[list[Any]]: if len(awaitable_indices) == 1: def on_one_resolved(result): @@ -489,13 +463,13 @@ def on_one_resolved(result): return completed_results[0].then(on_one_resolved) def on_all_resolve(results): - for index, result in zip(awaitable_indices, results): + for index, result in zip(awaitable_indices, results, strict=False): completed_results[index] = result return completed_results - return Promise.all( - [completed_results[index] for index in awaitable_indices] - ).then(on_all_resolve) + return Promise.all([ + completed_results[index] for index in awaitable_indices + ]).then(on_all_resolve) res = get_completed_results() return res diff --git a/tests/__init__.py b/graphql_core_promise/py.typed similarity index 100% rename from tests/__init__.py rename to graphql_core_promise/py.typed diff --git a/pyproject.toml b/pyproject.toml index 942e1de..1eadb60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,47 @@ -[tool.poetry] +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] name = "graphql-core-promise" -version = "3.2.3-2" +dynamic = ["version"] description = "Add support for promise-based dataloaders and resolvers to graphql-core v3+" -authors = ["Shen Li "] -license = "MIT" readme = "README.md" -packages = [{include = "graphql_core_promise"}] +requires-python = ">=3.10" +authors = [ + { name = "Samuel Cormier-Iijima", email = "sam@fellow.co" }, + { name = "Shen Li", email = "dustet@gmail.com" }, +] +license = { text = "BSD-3-Clause" } +dependencies = ["graphql-core>=3.2", "promise>=2.3"] -[tool.poetry.dependencies] -python = "^3.7" +[project.optional-dependencies] +dev = ["mypy~=1.9", "pytest~=8.2", "pytest-benchmark~=4.0", "ruff~=0.4"] -[tool.poetry.group.dev.dependencies] -black = "^22.8.0" -pytest = "^7.1.3" -pytest-describe = "^2.0.1" -pytest-benchmark = "^4.0.0" +[tool.setuptools] +packages = ["graphql_core_promise"] -# These two are peer dependencies -graphql-core = "^3.2" -promise = "^2.3" +[tool.setuptools_scm] -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.ruff.lint] +select = [ + "B", + "COM", + "E", + "EM", + "F", + "I", + "I", + "N", + "PT", + "RSE", + "RUF", + "SIM", + "UP", + "W", +] +ignore = ["COM812"] +preview = true -[tool.isort] -profile = "black" \ No newline at end of file +[tool.ruff.format] +preview = true diff --git a/tests/test_execute.py b/tests/test_execute.py index 38687ad..5aef57a 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -16,7 +16,6 @@ from graphql_core_promise.execute import PromiseExecutionContext - author_load_stub = MagicMock() name_loader_stub = MagicMock() book_loader_stub = MagicMock() @@ -75,9 +74,9 @@ def batch_load_fn(self, keys): ), "books": GraphQLField( GraphQLList(Book), - resolve=lambda obj, _info: books_loader.load_many( - [obj.id + f" book_{i}" for i in range(1, 3)] - ), + resolve=lambda obj, _info: books_loader.load_many([ + obj.id + f" book_{i}" for i in range(1, 3) + ]), ), }, )