1+ import asyncio
2+ import contextvars
13import copy
24import threading
35from contextlib import contextmanager
3840# Global lock for settings configuration
3941global_lock = threading .Lock ()
4042
41-
42- class ThreadLocalOverrides (threading .local ):
43- def __init__ (self ):
44- self .overrides = dotdict ()
45-
46-
47- thread_local_overrides = ThreadLocalOverrides ()
43+ thread_local_overrides = contextvars .ContextVar ("context_overrides" , default = dotdict ())
4844
4945
5046class Settings :
@@ -75,7 +71,7 @@ def lock(self):
7571 return global_lock
7672
7773 def __getattr__ (self , name ):
78- overrides = getattr ( thread_local_overrides , "overrides" , dotdict () )
74+ overrides = thread_local_overrides . get ( )
7975 if name in overrides :
8076 return overrides [name ]
8177 elif name in main_thread_config :
@@ -96,7 +92,7 @@ def __setitem__(self, key, value):
9692 self .__setattr__ (key , value )
9793
9894 def __contains__ (self , key ):
99- overrides = getattr ( thread_local_overrides , "overrides" , dotdict () )
95+ overrides = thread_local_overrides . get ( )
10096 return key in overrides or key in main_thread_config
10197
10298 def get (self , key , default = None ):
@@ -106,23 +102,60 @@ def get(self, key, default=None):
106102 return default
107103
108104 def copy (self ):
109- overrides = getattr ( thread_local_overrides , "overrides" , dotdict () )
105+ overrides = thread_local_overrides . get ( )
110106 return dotdict ({** main_thread_config , ** overrides })
111107
112108 @property
113109 def config (self ):
114110 return self .copy ()
115111
116- def configure (self , ** kwargs ):
112+ def _ensure_configure_allowed (self ):
117113 global main_thread_config , config_owner_thread_id
118114 current_thread_id = threading .get_ident ()
119115
120- with self .lock :
121- # First configuration: establish ownership. If ownership established, only that thread can configure.
122- if config_owner_thread_id in [None , current_thread_id ]:
123- config_owner_thread_id = current_thread_id
124- else :
125- raise RuntimeError ("dspy.settings can only be changed by the thread that initially configured it." )
116+ if config_owner_thread_id is None :
117+ # First `configure` call is always allowed.
118+ config_owner_thread_id = current_thread_id
119+ return
120+
121+ if config_owner_thread_id != current_thread_id :
122+ # Disallow a second `configure` calls from other threads.
123+ raise RuntimeError ("dspy.settings can only be changed by the thread that initially configured it." )
124+
125+ # Async task doesn't allow a second `configure` call, must use dspy.context(...) instead.
126+ is_async_task = False
127+ try :
128+ if asyncio .current_task () is not None :
129+ is_async_task = True
130+ except RuntimeError :
131+ # This exception (e.g., "no current task") means we are not in an async loop/task,
132+ # or asyncio module itself is not fully functional in this specific sub-thread context.
133+ is_async_task = False
134+
135+ if not is_async_task :
136+ return
137+
138+ # We are in an async task. Now check for IPython and allow calling `configure` from IPython.
139+ in_ipython = False
140+ try :
141+ from IPython import get_ipython
142+
143+ # get_ipython is a global injected by IPython environments.
144+ # We check its existence and type to be more robust.
145+ in_ipython = get_ipython () is not None
146+ except Exception :
147+ # If `IPython` is not installed or `get_ipython` failed, we are not in an IPython environment.
148+ in_ipython = False
149+
150+ if not in_ipython :
151+ raise RuntimeError (
152+ "dspy.settings.configure(...) cannot be called a second time from an async task. Use "
153+ "`dspy.context(...)` instead."
154+ )
155+
156+ def configure (self , ** kwargs ):
157+ # If no exception is raised, the `configure` call is allowed.
158+ self ._ensure_configure_allowed ()
126159
127160 # Update global config
128161 for k , v in kwargs .items ():
@@ -136,17 +169,17 @@ def context(self, **kwargs):
136169 If threads are spawned inside this block using ParallelExecutor, they will inherit these overrides.
137170 """
138171
139- original_overrides = getattr ( thread_local_overrides , "overrides" , dotdict () ).copy ()
172+ original_overrides = thread_local_overrides . get ( ).copy ()
140173 new_overrides = dotdict ({** main_thread_config , ** original_overrides , ** kwargs })
141- thread_local_overrides . overrides = new_overrides
174+ token = thread_local_overrides . set ( new_overrides )
142175
143176 try :
144177 yield
145178 finally :
146- thread_local_overrides .overrides = original_overrides
179+ thread_local_overrides .reset ( token )
147180
148181 def __repr__ (self ):
149- overrides = getattr ( thread_local_overrides , "overrides" , dotdict () )
182+ overrides = thread_local_overrides . get ( )
150183 combined_config = {** main_thread_config , ** overrides }
151184 return repr (combined_config )
152185
0 commit comments