From 9f27343f311479cfc3e69d89dcdcf84cf41b436a Mon Sep 17 00:00:00 2001 From: Thibault Derousseaux <6574550+tibdex@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:31:35 -0400 Subject: [PATCH] Make `OperationRequest` public --- .github/dependabot.yml | 2 +- README.md | 2 +- bookshop/graphql/runtime/__init__.py | 2 +- bookshop/graphql/runtime/merge.py | 50 ++++++++++++------------ bookshop/graphql/runtime/operation.py | 27 ++++++++----- pyproject.toml | 4 +- src/graphql_codegen/runtime/__init__.py | 2 +- src/graphql_codegen/runtime/merge.py | 50 ++++++++++++------------ src/graphql_codegen/runtime/operation.py | 27 ++++++++----- tests/test_runtime.py | 27 ++++++++++--- uv.lock | 2 +- 11 files changed, 115 insertions(+), 80 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0b4bf8f..3bb8a1c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,4 +13,4 @@ updates: - package-ecosystem: github-actions directory: / schedule: - interval: weekly + interval: monthly diff --git a/README.md b/README.md index c2887aa..277a94b 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ from .error import ( ) from .injection import OMITTED as OMITTED from .merge import merge as merge -from .operation import Operation as Operation +from .operation import Operation as Operation, OperationRequest as OperationRequest from .request import PreparedRequest as PreparedRequest, Request as Request ``` diff --git a/bookshop/graphql/runtime/__init__.py b/bookshop/graphql/runtime/__init__.py index 6e22d8b..073db91 100644 --- a/bookshop/graphql/runtime/__init__.py +++ b/bookshop/graphql/runtime/__init__.py @@ -12,5 +12,5 @@ ) from .injection import OMITTED as OMITTED from .merge import merge as merge -from .operation import Operation as Operation +from .operation import Operation as Operation, OperationRequest as OperationRequest from .request import PreparedRequest as PreparedRequest, Request as Request diff --git a/bookshop/graphql/runtime/merge.py b/bookshop/graphql/runtime/merge.py index c18fd9e..724ed4e 100644 --- a/bookshop/graphql/runtime/merge.py +++ b/bookshop/graphql/runtime/merge.py @@ -13,7 +13,7 @@ from .error import ExecutionError, GraphqlError, UnexpectedNullError from .injection import _Injectors from .operation import ( - _OperationRequest, + OperationRequest, _origin_note, _parse_response, _resolve_variables, @@ -23,7 +23,7 @@ @final class _MergedRequest[Type: OperationType, Data](Request[Type, Data]): - def __init__(self, requests: Sequence[_OperationRequest[Type, object]], /) -> None: + def __init__(self, requests: Sequence[OperationRequest[Type, object]], /) -> None: if not requests: raise ValueError("Cannot merge no request.") @@ -32,7 +32,7 @@ def __init__(self, requests: Sequence[_OperationRequest[Type, object]], /) -> No @override def __repr__(self) -> str: - return f"merge({', '.join(map(repr, self._requests))})" + return f"{merge.__name__}({', '.join(map(repr, self._requests))})" @override def prepare(self, *, injectors: _Injectors | None = None) -> PreparedRequest[Data]: @@ -47,45 +47,45 @@ def prepare(self, *, injectors: _Injectors | None = None) -> PreparedRequest[Dat # Typed position by position up to 6 requests, as many as typeshed's `asyncio.gather()`: https://github.com/python/typeshed/blob/1ee8b928886a93b060bdafbf1e30dc0bb7c85848/stdlib/asyncio/tasks.pyi#L108-L155 @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2]( - first: _OperationRequest[Type, D1], second: _OperationRequest[Type, D2], / + first: OperationRequest[Type, D1], second: OperationRequest[Type, D2], / ) -> Request[Type, tuple[D1, D2]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2, D3]( - first: _OperationRequest[Type, D1], - second: _OperationRequest[Type, D2], - third: _OperationRequest[Type, D3], + first: OperationRequest[Type, D1], + second: OperationRequest[Type, D2], + third: OperationRequest[Type, D3], /, ) -> Request[Type, tuple[D1, D2, D3]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2, D3, D4]( - first: _OperationRequest[Type, D1], - second: _OperationRequest[Type, D2], - third: _OperationRequest[Type, D3], - fourth: _OperationRequest[Type, D4], + first: OperationRequest[Type, D1], + second: OperationRequest[Type, D2], + third: OperationRequest[Type, D3], + fourth: OperationRequest[Type, D4], /, ) -> Request[Type, tuple[D1, D2, D3, D4]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2, D3, D4, D5]( - first: _OperationRequest[Type, D1], - second: _OperationRequest[Type, D2], - third: _OperationRequest[Type, D3], - fourth: _OperationRequest[Type, D4], - fifth: _OperationRequest[Type, D5], + first: OperationRequest[Type, D1], + second: OperationRequest[Type, D2], + third: OperationRequest[Type, D3], + fourth: OperationRequest[Type, D4], + fifth: OperationRequest[Type, D5], /, ) -> Request[Type, tuple[D1, D2, D3, D4, D5]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2, D3, D4, D5, D6]( - first: _OperationRequest[Type, D1], - second: _OperationRequest[Type, D2], - third: _OperationRequest[Type, D3], - fourth: _OperationRequest[Type, D4], - fifth: _OperationRequest[Type, D5], - sixth: _OperationRequest[Type, D6], + first: OperationRequest[Type, D1], + second: OperationRequest[Type, D2], + third: OperationRequest[Type, D3], + fourth: OperationRequest[Type, D4], + fifth: OperationRequest[Type, D5], + sixth: OperationRequest[Type, D6], /, ) -> Request[Type, tuple[D1, D2, D3, D4, D5, D6]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), Data]( - requests: Sequence[_OperationRequest[Type, Data]], / + requests: Sequence[OperationRequest[Type, Data]], / ) -> Request[Type, tuple[Data, ...]]: ... def merge(*args: Any) -> Request[Any, Any]: """Return one request for several queries, or several mutations, whose data is a tuple of their data. @@ -102,7 +102,7 @@ def merge(*args: Any) -> Request[Any, Any]: def _prepare_merged( operation_type: OperationType, - requests: Sequence[_OperationRequest[OperationType, object]], + requests: Sequence[OperationRequest[OperationType, object]], /, *, injectors: _Injectors | None, @@ -192,7 +192,7 @@ def parse(raw: bytes, /) -> tuple[object, ...]: def _raises_execution_error( - request: _OperationRequest[OperationType, object], / + request: OperationRequest[OperationType, object], / ) -> bool: match request._on_execution_error: case "raise": diff --git a/bookshop/graphql/runtime/operation.py b/bookshop/graphql/runtime/operation.py index 391ac10..ef07bd9 100644 --- a/bookshop/graphql/runtime/operation.py +++ b/bookshop/graphql/runtime/operation.py @@ -55,20 +55,19 @@ def __init__( self._injections: Final = injections self._data_type: Final = data_type - def __call__(self, variables: Variables, /) -> _OperationRequest[Type, Data]: + def __call__(self, variables: Variables, /) -> OperationRequest[Type, Data]: """Return the request of this operation with *variables*. A method rather than a function taking both, so every type checker checks the variables against their exact type. """ - return _OperationRequest( + return OperationRequest( operation=self, variables=variables, on_execution_error="raise" ) @override def __repr__(self) -> str: - return ( - f"Operation(operation_type={self._operation_type!r}, name={self._name!r})" - ) + # As the document spells it, in brackets since it is not code. + return f"<{self._operation_type} {self._name}>" @cached_property def _data_parser(self) -> Convert | None: @@ -97,7 +96,7 @@ def _request_body(self, variables: Mapping[str, object], /) -> bytes: @final -class _OperationRequest[Type: OperationType, Data](Request[Type, Data]): +class OperationRequest[Type: OperationType, Data](Request[Type, Data]): def __init__( self, *, @@ -106,6 +105,7 @@ def __init__( variables: Mapping[str, object], on_execution_error: ExecutionErrorHandling, ) -> None: + """Private.""" super().__init__(operation_type=operation._operation_type) self._operation: Final = operation self._variables: Final = variables @@ -113,14 +113,23 @@ def __init__( @override def __repr__(self) -> str: - return f"Request(operation={self._operation!r}, variables={self._variables!r}, on_execution_error={self._on_execution_error!r})" + # As written: the operation called with its variables. + call = f"{self._operation._name}({self._variables!r})" + + match self._on_execution_error: + case "raise": + return call + case "return": + return f"{call}.{self.returning_error.__name__}()" + case _ as never: + assert_never(never) def returning_error( self, - ) -> _OperationRequest[Type, Data | ExecutionError[Data]]: + ) -> OperationRequest[Type, Data | ExecutionError[Data]]: """Return a copy of this request for which `PreparedRequest.parse()` returns its `ExecutionError` instead of raising it.""" # Sound since `Data` is covariant: a request returning its data is one returning its data or its error. - return _OperationRequest( + return OperationRequest( operation=self._operation, variables=self._variables, on_execution_error="return", diff --git a/pyproject.toml b/pyproject.toml index 2dabbaf..e9ced70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ license-files = ["LICENSE"] name = "graphql-codegen" readme = "README.md" requires-python = ">=3.12" -version = "0.2.0" +version = "0.2.1" [project.optional-dependencies] # Only reading a YAML graphql-config file needs it. @@ -135,7 +135,7 @@ ignore = [ "undocumented-public-module", "undocumented-public-nested-class", "undocumented-public-package", - "useless-import-alias", # `X as X` is how PEP 484 marks a re-export, in any module and not only an `__init__.py`. + "useless-import-alias", # `X as X` is how PEP 484 marks a re-export, in any module and not only an `__init__.py`. ] select = ["ALL"] diff --git a/src/graphql_codegen/runtime/__init__.py b/src/graphql_codegen/runtime/__init__.py index 4b2d3f6..61ad905 100644 --- a/src/graphql_codegen/runtime/__init__.py +++ b/src/graphql_codegen/runtime/__init__.py @@ -12,5 +12,5 @@ ) from .injection import OMITTED as OMITTED from .merge import merge as merge -from .operation import Operation as Operation +from .operation import Operation as Operation, OperationRequest as OperationRequest from .request import PreparedRequest as PreparedRequest, Request as Request diff --git a/src/graphql_codegen/runtime/merge.py b/src/graphql_codegen/runtime/merge.py index ca4cfde..c1f37da 100644 --- a/src/graphql_codegen/runtime/merge.py +++ b/src/graphql_codegen/runtime/merge.py @@ -13,7 +13,7 @@ from .error import ExecutionError, GraphqlError, UnexpectedNullError from .injection import _Injectors from .operation import ( - _OperationRequest, + OperationRequest, _origin_note, _parse_response, _resolve_variables, @@ -23,7 +23,7 @@ @final class _MergedRequest[Type: OperationType, Data](Request[Type, Data]): - def __init__(self, requests: Sequence[_OperationRequest[Type, object]], /) -> None: + def __init__(self, requests: Sequence[OperationRequest[Type, object]], /) -> None: if not requests: raise ValueError("Cannot merge no request.") @@ -32,7 +32,7 @@ def __init__(self, requests: Sequence[_OperationRequest[Type, object]], /) -> No @override def __repr__(self) -> str: - return f"merge({', '.join(map(repr, self._requests))})" + return f"{merge.__name__}({', '.join(map(repr, self._requests))})" @override def prepare(self, *, injectors: _Injectors | None = None) -> PreparedRequest[Data]: @@ -47,45 +47,45 @@ def prepare(self, *, injectors: _Injectors | None = None) -> PreparedRequest[Dat # Typed position by position up to 6 requests, as many as typeshed's `asyncio.gather()`: https://github.com/python/typeshed/blob/1ee8b928886a93b060bdafbf1e30dc0bb7c85848/stdlib/asyncio/tasks.pyi#L108-L155 @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2]( - first: _OperationRequest[Type, D1], second: _OperationRequest[Type, D2], / + first: OperationRequest[Type, D1], second: OperationRequest[Type, D2], / ) -> Request[Type, tuple[D1, D2]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2, D3]( - first: _OperationRequest[Type, D1], - second: _OperationRequest[Type, D2], - third: _OperationRequest[Type, D3], + first: OperationRequest[Type, D1], + second: OperationRequest[Type, D2], + third: OperationRequest[Type, D3], /, ) -> Request[Type, tuple[D1, D2, D3]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2, D3, D4]( - first: _OperationRequest[Type, D1], - second: _OperationRequest[Type, D2], - third: _OperationRequest[Type, D3], - fourth: _OperationRequest[Type, D4], + first: OperationRequest[Type, D1], + second: OperationRequest[Type, D2], + third: OperationRequest[Type, D3], + fourth: OperationRequest[Type, D4], /, ) -> Request[Type, tuple[D1, D2, D3, D4]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2, D3, D4, D5]( - first: _OperationRequest[Type, D1], - second: _OperationRequest[Type, D2], - third: _OperationRequest[Type, D3], - fourth: _OperationRequest[Type, D4], - fifth: _OperationRequest[Type, D5], + first: OperationRequest[Type, D1], + second: OperationRequest[Type, D2], + third: OperationRequest[Type, D3], + fourth: OperationRequest[Type, D4], + fifth: OperationRequest[Type, D5], /, ) -> Request[Type, tuple[D1, D2, D3, D4, D5]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), D1, D2, D3, D4, D5, D6]( - first: _OperationRequest[Type, D1], - second: _OperationRequest[Type, D2], - third: _OperationRequest[Type, D3], - fourth: _OperationRequest[Type, D4], - fifth: _OperationRequest[Type, D5], - sixth: _OperationRequest[Type, D6], + first: OperationRequest[Type, D1], + second: OperationRequest[Type, D2], + third: OperationRequest[Type, D3], + fourth: OperationRequest[Type, D4], + fifth: OperationRequest[Type, D5], + sixth: OperationRequest[Type, D6], /, ) -> Request[Type, tuple[D1, D2, D3, D4, D5, D6]]: ... @overload def merge[Type: (Literal["query"], Literal["mutation"]), Data]( - requests: Sequence[_OperationRequest[Type, Data]], / + requests: Sequence[OperationRequest[Type, Data]], / ) -> Request[Type, tuple[Data, ...]]: ... def merge(*args: Any) -> Request[Any, Any]: """Return one request for several queries, or several mutations, whose data is a tuple of their data. @@ -102,7 +102,7 @@ def merge(*args: Any) -> Request[Any, Any]: def _prepare_merged( operation_type: OperationType, - requests: Sequence[_OperationRequest[OperationType, object]], + requests: Sequence[OperationRequest[OperationType, object]], /, *, injectors: _Injectors | None, @@ -192,7 +192,7 @@ def parse(raw: bytes, /) -> tuple[object, ...]: def _raises_execution_error( - request: _OperationRequest[OperationType, object], / + request: OperationRequest[OperationType, object], / ) -> bool: match request._on_execution_error: case "raise": diff --git a/src/graphql_codegen/runtime/operation.py b/src/graphql_codegen/runtime/operation.py index 7e1896f..ff105bb 100644 --- a/src/graphql_codegen/runtime/operation.py +++ b/src/graphql_codegen/runtime/operation.py @@ -55,20 +55,19 @@ def __init__( self._injections: Final = injections self._data_type: Final = data_type - def __call__(self, variables: Variables, /) -> _OperationRequest[Type, Data]: + def __call__(self, variables: Variables, /) -> OperationRequest[Type, Data]: """Return the request of this operation with *variables*. A method rather than a function taking both, so every type checker checks the variables against their exact type. """ - return _OperationRequest( + return OperationRequest( operation=self, variables=variables, on_execution_error="raise" ) @override def __repr__(self) -> str: - return ( - f"Operation(operation_type={self._operation_type!r}, name={self._name!r})" - ) + # As the document spells it, in brackets since it is not code. + return f"<{self._operation_type} {self._name}>" @cached_property def _data_parser(self) -> Convert | None: @@ -97,7 +96,7 @@ def _request_body(self, variables: Mapping[str, object], /) -> bytes: @final -class _OperationRequest[Type: OperationType, Data](Request[Type, Data]): +class OperationRequest[Type: OperationType, Data](Request[Type, Data]): def __init__( self, *, @@ -106,6 +105,7 @@ def __init__( variables: Mapping[str, object], on_execution_error: ExecutionErrorHandling, ) -> None: + """Private.""" super().__init__(operation_type=operation._operation_type) self._operation: Final = operation self._variables: Final = variables @@ -113,14 +113,23 @@ def __init__( @override def __repr__(self) -> str: - return f"Request(operation={self._operation!r}, variables={self._variables!r}, on_execution_error={self._on_execution_error!r})" + # As written: the operation called with its variables. + call = f"{self._operation._name}({self._variables!r})" + + match self._on_execution_error: + case "raise": + return call + case "return": + return f"{call}.{self.returning_error.__name__}()" + case _ as never: + assert_never(never) def returning_error( self, - ) -> _OperationRequest[Type, Data | ExecutionError[Data]]: + ) -> OperationRequest[Type, Data | ExecutionError[Data]]: """Return a copy of this request for which `PreparedRequest.parse()` returns its `ExecutionError` instead of raising it.""" # Sound since `Data` is covariant: a request returning its data is one returning its data or its error. - return _OperationRequest( + return OperationRequest( operation=self._operation, variables=self._variables, on_execution_error="return", diff --git a/tests/test_runtime.py b/tests/test_runtime.py index b25658f..58e3064 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -8,6 +8,7 @@ OMITTED, ExecutionError, Operation, + OperationRequest, ProtocolError, RequestError, ) @@ -26,11 +27,27 @@ ) -def test_a_repr_names_the_operation_rather_than_dumping_its_document() -> None: - assert ( - repr(_LIST_BOOKS({})) - == "Request(operation=Operation(operation_type='query', name='ListBooks'), variables={}, on_execution_error='raise')" - ) +def test_an_operation_s_repr_is_how_its_document_spells_it() -> None: + assert repr(_LIST_BOOKS) == "" + + +@pytest.mark.parametrize( + ("request_", "expected"), + [ + pytest.param( + _LIST_BOOKS({"first": 2}), "ListBooks({'first': 2})", id="raising" + ), + pytest.param( + _LIST_BOOKS({"first": 2}).returning_error(), + "ListBooks({'first': 2}).returning_error()", + id="returning its error", + ), + ], +) +def test_a_request_s_repr_is_how_it_is_written( + request_: OperationRequest[Literal["query"], object], expected: str +) -> None: + assert repr(request_) == expected def test_a_request_is_the_spec_s_json_envelope() -> None: diff --git a/uv.lock b/uv.lock index d77f9b8..27045d1 100644 --- a/uv.lock +++ b/uv.lock @@ -125,7 +125,7 @@ wheels = [ [[package]] name = "graphql-codegen" -version = "0.2.0" +version = "0.2.1" source = { editable = "." } dependencies = [ { name = "graphql-core" },