66import inspect
77import sys
88import types
9- import weakref
109from asyncio import Future , Task
1110from collections .abc import (
1211 AsyncGenerator ,
@@ -731,14 +730,11 @@ async def sync_coro():
731730
732731 Notes:
733732 - This is a different mechanism from Python 3.12's native eager execution
734- feature. Python 3.12 provides `eager_start=True` parameter for
735- `asyncio.create_task()` and `asyncio.eager_task_factory()`. Our
733+ feature. Python 3.12 provides `asyncio.eager_task_factory`. Our
736734 implementation works on all Python versions but may not always create
737735 a real Task - synchronous coroutines get a TaskLikeFuture instead.
738736 - All kwargs from asyncio.create_task() are properly forwarded to the
739737 inner factory when delegation occurs.
740- - This is experimental functionality that modifies global task creation
741- behavior for the entire event loop.
742738 - If you want to preserve an existing task factory, explicitly pass it
743739 as inner_factory rather than relying on automatic detection.
744740
@@ -770,23 +766,7 @@ def real_task_factory(coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
770766 else :
771767 return asyncio .Task (coro , loop = loop , ** kwargs )
772768
773- ghost_task_getter = get_ghost_task_getter (loop )
774-
775- return coro_eager_task_helper (
776- loop , coro , name , context , ghost_task_getter , real_task_factory
777- )
778-
779- # cache GhostTaskHelper instances per event loop
780- helpers : weakref .WeakKeyDictionary [asyncio .AbstractEventLoop , GhostTaskHelper ] = (
781- weakref .WeakKeyDictionary ()
782- )
783-
784- def get_ghost_task_getter (
785- loop : asyncio .AbstractEventLoop ,
786- ) -> Callable [[], asyncio .Task [Any ]]:
787- if loop not in helpers :
788- helpers [loop ] = GhostTaskHelper (lambda coro : asyncio .Task (coro , loop = loop ))
789- return helpers [loop ].get
769+ return coro_eager_task_helper (loop , coro , name , context , real_task_factory )
790770
791771 return factory
792772
@@ -865,7 +845,6 @@ def real_task_factory(coro_arg: Coroutine[Any, Any, T]) -> asyncio.Task[T]:
865845 coro ,
866846 name ,
867847 context ,
868- default_ghost_task_getter ,
869848 real_task_factory ,
870849 )
871850
@@ -889,36 +868,38 @@ class GhostTaskHelper:
889868 temporary task contexts if creating eager tasks in a non-task context.
890869 """
891870
892- cleanup : set [asyncio .Task [Any ]] = set ()
893-
894- def __init__ (
895- self , raw_create : Callable [[Coroutine [Any , Any , Any ]], asyncio .Task [Any ]]
896- ) -> None :
897- self .raw_create = raw_create
898- self .ghost_task : asyncio .Task [Any ] | None = None
871+ # this could be a WeakKeyDictionary but we want maximum performance here
872+ # and there are unlikely to be many event loops in practice.
873+ tasks : dict [asyncio .AbstractEventLoop , asyncio .Task [Any ]] = {}
899874
900- def get (self ) -> asyncio .Task [Any ]:
901- if self .ghost_task is None :
902-
903- async def ghost_coro () -> None :
904- return None
875+ @classmethod
876+ def get (
877+ cls ,
878+ loop : asyncio .AbstractEventLoop ,
879+ raw_create : Callable [[Coroutine [Any , Any , Any ]], asyncio .Task [Any ]],
880+ ) -> asyncio .Task [Any ]:
881+ """
882+ Get the GhostTaskHelper instance for the given event loop.
883+ """
884+ try :
885+ return cls .tasks [loop ]
886+ except KeyError :
887+ cls .tasks [loop ] = raw_create (cls .task_coro ())
888+ return cls .tasks [loop ]
905889
906- self .ghost_task = self .raw_create (ghost_coro ())
907- GhostTaskHelper .cleanup .add (self .ghost_task )
908- self .ghost_task .add_done_callback (GhostTaskHelper .cleanup .discard )
909- return self .ghost_task
890+ @classmethod
891+ async def task_coro (cls ) -> None :
892+ pass
910893
911894
912- # default instance for the create_task helper
913- default_ghost_task_getter = GhostTaskHelper (_create_task ).get
895+ _get_ghost_task = GhostTaskHelper .get # for easier access
914896
915897
916898def coro_eager_task_helper (
917899 loop : asyncio .AbstractEventLoop ,
918900 coro : Coroutine [Any , Any , T ],
919901 name : str | None ,
920902 context : Context | None ,
921- get_fake_task : Callable [[], asyncio .Task [Any ]],
922903 real_task_factory : Callable [[Coroutine [Any , Any , T ]], asyncio .Task [T ]],
923904) -> asyncio .Task [T ] | TaskLikeFuture [T ]:
924905 """
@@ -936,38 +917,32 @@ def coro_eager_task_helper(
936917 """
937918 # In Python < 3.11, context parameter doesn't exist for create_task()
938919 # so we ignore any provided context and let CoroStart manage its own
939-
940920 if sys .version_info < (3 , 11 ):
941921 context = None
942922
943- # start the coroutine in the task context
944- def start () -> CoroStart [T ]:
945- if context is not None :
946- # Enter the context only for the initial start, then use None for CoroStart
947- # This way the continuation won't try to re-enter the context
948- def start_in_context () -> CoroStart [T ]:
949- return CoroStart (coro , context = None )
950-
951- cs = context .run (start_in_context )
952- else :
953- # No explicit context - use copy_context() as before
954- cs = CoroStart (coro , context = copy_context ())
955- return cs
956-
957- current_task = asyncio .current_task (loop )
958- # if there is no current task, then we need a fake task to run it in
959- # this is so that asyncio.get_current_task() returns a valid task during
960- # eager start. This is not the same task as will be created later. This
961- # is purely to satisfy get_current_task() calls during eager start, such
962- # as for anyio that wants to detect the current async framework.
963-
964923 cs : CoroStart [T ]
924+ current_task = asyncio .current_task (loop )
965925 if current_task is not None :
966- cs = start ()
926+ if context is None :
927+ cs = CoroStart (coro , context = copy_context ())
928+ else :
929+ # Enter the context only for the initial start, then use None for CoroStart
930+ # This way the continuation won't try to re-enter the context
931+ cs = context .run (lambda : CoroStart (coro , context = None ))
967932 else :
968- old = swap_current_task (loop , get_fake_task ())
933+ # if there is no current task, then we need a fake task to run it in
934+ # this is so that asyncio.get_current_task() returns a valid task during
935+ # eager start. This is not the same task as will be created later. This
936+ # is purely to satisfy get_current_task() calls during eager start, such
937+ # as for anyio that wants to detect the current async framework.
938+ old = swap_current_task (loop , _get_ghost_task (loop , real_task_factory ))
969939 try :
970- cs = start ()
940+ if context is None :
941+ cs = CoroStart (coro , context = copy_context ())
942+ else :
943+ # Enter the context only for the initial start, then use None for CoroStart
944+ # This way the continuation won't try to re-enter the context
945+ cs = context .run (lambda : CoroStart (coro , context = None ))
971946 finally :
972947 swap_current_task (loop , old )
973948
@@ -976,7 +951,6 @@ def start_in_context() -> CoroStart[T]:
976951 if not cs .done ():
977952 return real_task_factory (cs .as_coroutine ())
978953 else :
979- # Return a TaskLikeFuture wrapping the result
980954 return TaskLikeFuture (cs , name = name , context = context )
981955
982956
0 commit comments