1111 Iterable ,
1212 List ,
1313 Optional ,
14+ Set ,
1415 Tuple ,
1516 Type ,
1617 Union ,
@@ -174,6 +175,42 @@ def __ne__(self, other: Any) -> bool:
174175Middleware = Optional [Union [Tuple , List , MiddlewareManager ]]
175176
176177
178+ class CollectedErrors :
179+ """A list of errors collected during execution, ignoring nulled positions.
180+
181+ For internal use only.
182+ """
183+
184+ _error_positions : Set [Optional [Path ]]
185+ _errors : List [GraphQLError ]
186+
187+ def __init__ (self ) -> None :
188+ self ._error_positions = set ()
189+ self ._errors = []
190+
191+ @property
192+ def errors (self ) -> List [GraphQLError ]:
193+ return self ._errors
194+
195+ def add (self , error : GraphQLError , path : Optional [Path ]) -> None :
196+ # Do not modify errors list if the execution position for this error or
197+ # any of its ancestors has already been nulled via error propagation.
198+ # This check should be unnecessary for implementations able to implement
199+ # actual cancellation.
200+ if self ._has_nulled_position (path ):
201+ return
202+ self ._error_positions .add (path )
203+ self ._errors .append (error )
204+
205+ def _has_nulled_position (self , start_path : Optional [Path ]) -> bool :
206+ path = start_path
207+ while path is not None :
208+ if path in self ._error_positions :
209+ return True
210+ path = path .prev
211+ return None in self ._error_positions
212+
213+
177214class ExecutionContext :
178215 """Data that must be available at all points during query execution.
179216
@@ -190,7 +227,7 @@ class ExecutionContext:
190227 field_resolver : GraphQLFieldResolver
191228 type_resolver : GraphQLTypeResolver
192229 subscribe_field_resolver : GraphQLFieldResolver
193- errors : List [ GraphQLError ]
230+ collected_errors : CollectedErrors
194231 middleware_manager : Optional [MiddlewareManager ]
195232
196233 is_awaitable = staticmethod (default_is_awaitable )
@@ -206,7 +243,7 @@ def __init__(
206243 field_resolver : GraphQLFieldResolver ,
207244 type_resolver : GraphQLTypeResolver ,
208245 subscribe_field_resolver : GraphQLFieldResolver ,
209- errors : List [ GraphQLError ] ,
246+ collected_errors : CollectedErrors ,
210247 middleware_manager : Optional [MiddlewareManager ],
211248 is_awaitable : Optional [Callable [[Any ], bool ]],
212249 ) -> None :
@@ -219,7 +256,7 @@ def __init__(
219256 self .field_resolver = field_resolver
220257 self .type_resolver = type_resolver
221258 self .subscribe_field_resolver = subscribe_field_resolver
222- self .errors = errors
259+ self .collected_errors = collected_errors
223260 self .middleware_manager = middleware_manager
224261 if is_awaitable :
225262 self .is_awaitable = is_awaitable # type: ignore
@@ -306,7 +343,7 @@ def build(
306343 field_resolver or default_field_resolver ,
307344 type_resolver or default_type_resolver ,
308345 subscribe_field_resolver or default_field_resolver ,
309- [] ,
346+ CollectedErrors () ,
310347 middleware_manager ,
311348 is_awaitable ,
312349 )
@@ -536,7 +573,7 @@ async def await_result() -> Any:
536573 return completed
537574 except Exception as raw_error :
538575 error = located_error (raw_error , field_nodes , path .as_list ())
539- self .handle_field_error (error , return_type )
576+ self .handle_field_error (error , return_type , path )
540577 return None
541578
542579 return await_result ()
@@ -551,29 +588,30 @@ async def await_completed() -> Any:
551588 return await completed
552589 except Exception as raw_error :
553590 error = located_error (raw_error , field_nodes , path .as_list ())
554- self .handle_field_error (error , return_type )
591+ self .handle_field_error (error , return_type , path )
555592 return None
556593
557594 return await_completed ()
558595
559596 return completed
560597 except Exception as raw_error :
561598 error = located_error (raw_error , field_nodes , path .as_list ())
562- self .handle_field_error (error , return_type )
599+ self .handle_field_error (error , return_type , path )
563600 return None
564601
565602 def handle_field_error (
566603 self ,
567604 error : GraphQLError ,
568605 return_type : GraphQLOutputType ,
606+ path : Path ,
569607 ) -> None :
570608 # If the field type is non-nullable, then it is resolved without any protection
571609 # from errors, however it still properly locates the error.
572610 if is_non_null_type (return_type ):
573611 raise error
574612 # Otherwise, error protection is applied, logging the error and resolving a
575613 # null value for this field if one is encountered.
576- self .errors . append (error )
614+ self .collected_errors . add (error , path )
577615 return None
578616
579617 def complete_value (
@@ -721,7 +759,7 @@ async def await_completed(item: Any, item_path: Path) -> Any:
721759 error = located_error (
722760 raw_error , field_nodes , item_path .as_list ()
723761 )
724- self .handle_field_error (error , item_type )
762+ self .handle_field_error (error , item_type , item_path )
725763 return None
726764
727765 completed_item = await_completed (item , item_path )
@@ -739,13 +777,13 @@ async def await_completed(item: Any, item_path: Path) -> Any:
739777 error = located_error (
740778 raw_error , field_nodes , item_path .as_list ()
741779 )
742- self .handle_field_error (error , item_type )
780+ self .handle_field_error (error , item_type , item_path )
743781 return None
744782
745783 completed_item = await_completed (completed_item , item_path )
746784 except Exception as raw_error :
747785 error = located_error (raw_error , field_nodes , item_path .as_list ())
748- self .handle_field_error (error , item_type )
786+ self .handle_field_error (error , item_type , item_path )
749787 completed_item = None
750788
751789 if is_awaitable (completed_item ):
@@ -1029,7 +1067,7 @@ def execute(
10291067 # Errors from sub-fields of a NonNull type may propagate to the top level,
10301068 # at which point we still log the error and null the parent field, which
10311069 # in this case is the entire response.
1032- errors = exe_context .errors
1070+ collected_errors = exe_context .collected_errors
10331071 build_response = exe_context .build_response
10341072 try :
10351073 operation = exe_context .operation
@@ -1039,17 +1077,18 @@ def execute(
10391077 # noinspection PyShadowingNames
10401078 async def await_result () -> Any :
10411079 try :
1042- return build_response (await result , errors ) # type: ignore
1080+ data = await result # type: ignore
1081+ return build_response (data , collected_errors .errors )
10431082 except GraphQLError as error :
1044- errors . append (error )
1045- return build_response (None , errors )
1083+ collected_errors . add (error , None )
1084+ return build_response (None , collected_errors . errors )
10461085
10471086 return await_result ()
10481087 except GraphQLError as error :
1049- errors . append (error )
1050- return build_response (None , errors )
1088+ collected_errors . add (error , None )
1089+ return build_response (None , collected_errors . errors )
10511090 else :
1052- return build_response (result , errors ) # type: ignore
1091+ return build_response (result , collected_errors . errors ) # type: ignore
10531092
10541093
10551094def assume_not_awaitable (_value : Any ) -> bool :
0 commit comments