@@ -23,7 +23,7 @@ This model works well for single-user scripts and notebooks but creates problems
2323
2424## Design Principles
2525
26- 1 . ** One-way lock ** — Once enabled, thread -safe mode cannot be disabled
26+ 1 . ** Deployment-time decision ** — Thread -safe mode is set via environment or config file, not programmatically
27272 . ** Explicit over implicit** — All connection parameters must be explicitly provided
28283 . ** No hidden global state** — Connection behavior is fully determined by its configuration
29294 . ** Backward compatible** — Existing code works unchanged when ` thread_safe=False `
@@ -77,29 +77,27 @@ Connection parameters (set at creation, read-only after):
7777
7878### Enabling Thread-Safe Mode
7979
80- ``` python
81- import datajoint as dj
82-
83- # Method 1: Programmatic
84- dj.config.thread_safe = True
80+ Thread-safe mode is read-only after initialization and can only be set via environment variable or config file:
8581
86- # Method 2: Environment variable
87- # DJ_THREAD_SAFE=true python app.py
82+ ``` bash
83+ # Method 1: Environment variable
84+ DJ_THREAD_SAFE=true python app.py
85+ ```
8886
89- # Method 3: Config file (datajoint.json)
90- # { "thread_safe": true }
87+ ``` json
88+ // Method 2: Config file (datajoint.json)
89+ { "thread_safe" : true }
9190```
9291
93- Once enabled, cannot be disabled :
92+ Programmatic setting is not allowed :
9493``` python
95- dj.config.thread_safe = True
96- dj.config.thread_safe = False # Raises ThreadSafetyError
94+ dj.config.thread_safe = True # Raises ThreadSafetyError
9795```
9896
9997### Global Config Access in Thread-Safe Mode
10098
10199``` python
102- dj.config.thread_safe = True
100+ # With DJ_THREAD_SAFE=true set in environment
103101
104102# Only thread_safe is accessible
105103dj.config.thread_safe # OK (returns True)
@@ -259,40 +257,39 @@ schema = dj.Schema("my_schema", connection=conn)
259257
260258### Connection.config Behavior
261259
262- When ` thread_safe=False ` , ` conn.config ` provides access to settings but ** forwards to global config** for settings not explicitly provided:
260+ The behavior of ` conn.config ` depends on ** which API created the connection** , not on the ` thread_safe ` setting:
261+
262+ ** New API** (` Connection.from_config() ` ) — Uses explicit values or defaults. Never accesses global config. Works identically with ` thread_safe ` on or off:
263263
264264``` python
265- dj.config.thread_safe = False
266- dj.config.safemode = True
265+ dj.config.safemode = False # Set in global config
267266dj.config.database_prefix = " dev_"
268267
269268conn = dj.Connection.from_config(
270269 host = " localhost" ,
271270 user = " root" ,
272271 password = " secret" ,
273- # safemode not specified - will forward to global
272+ # safemode not specified - uses default, NOT global config
274273)
275274
276- conn.config.safemode # True (from dj.config )
277- conn.config.database_prefix # "dev_ " (from dj.config )
275+ conn.config.safemode # True (default, not global )
276+ conn.config.database_prefix # "" (default, not global )
278277```
279278
280- When ` thread_safe=True ` , ` conn.config ` uses only explicitly provided values with defaults :
279+ ** Legacy API ** ( ` dj.conn() ` ) — Forwards unset values to global config for backward compatibility :
281280
282281``` python
283- dj.config.thread_safe = True
282+ dj.config.safemode = False
283+ dj.config.database_prefix = " dev_"
284284
285- conn = dj.Connection.from_config(
286- host = " localhost" ,
287- user = " root" ,
288- password = " secret" ,
289- # safemode not specified - uses default
290- )
285+ conn = dj.conn() # Legacy API
291286
292- conn.config.safemode # True (default )
293- conn.config.database_prefix # "" (default )
287+ conn.config.safemode # False (from dj.config )
288+ conn.config.database_prefix # "dev_ " (from dj.config )
294289```
295290
291+ This design ensures that code using ` Connection.from_config() ` is portable and behaves identically whether ` thread_safe ` is enabled or not.
292+
296293## Migration Path
297294
298295Migration is immediate — adopt the new API and your code works in both modes:
@@ -351,18 +348,24 @@ class Config(BaseSettings):
351348 if name.startswith(" _" ):
352349 return object .__setattr__ (self , name, value)
353350
354- # thread_safe: one-way lock (can only go False -> True)
351+ # thread_safe is read- only after initialization
355352 if name == " thread_safe" :
356- current = object .__getattribute__ (self , " thread_safe" )
357- if current and not value:
358- raise ThreadSafetyError(" Cannot disable thread-safe mode once enabled." )
353+ try :
354+ object .__getattribute__ (self , " thread_safe" )
355+ # If we get here, thread_safe already exists - block the set
356+ raise ThreadSafetyError(
357+ " thread_safe cannot be set programmatically. "
358+ " Set DJ_THREAD_SAFE=true in environment or datajoint.json."
359+ )
360+ except AttributeError :
361+ pass # First time setting during __init__ - allow it
359362 return object .__setattr__ (self , name, value)
360363
361364 # Block everything else in thread-safe mode
362365 if object .__getattribute__ (self , " thread_safe" ):
363366 raise ThreadSafetyError(
364- f " Setting ' { name } ' is connection-scoped in thread-safe mode. "
365- " Modify it via connection.config instead ."
367+ " Global config is inaccessible in thread-safe mode. "
368+ " Use Connection.from_config() with explicit configuration ."
366369 )
367370
368371 return object .__setattr__ (self , name, value)
@@ -376,8 +379,9 @@ class Connection:
376379 use_tls = None , backend = None , * , _config = None ):
377380 # ... existing connection setup ...
378381
379- # Store connection-scoped config
380- self .config = _config or ConnectionConfig()
382+ # Connection-scoped configuration
383+ # Legacy API (dj.conn()) uses global fallback for backward compatibility
384+ self .config = _config if _config is not None else ConnectionConfig(_use_global_fallback = True )
381385
382386 @ classmethod
383387 def from_config (cls , cfg = None , * , host = None , user = None , password = None ,
@@ -393,14 +397,9 @@ class Connection:
393397 # ... merge cfg dict with kwargs ...
394398 # ... validate required fields (host, user, password) ...
395399
396- # Determine thread-safe mode (check global config safely)
397- from .settings import config
398- is_thread_safe = config.thread_safe
399-
400- # Build ConnectionConfig with explicit values only
401- # (unset values will forward to global or use defaults)
400+ # Build ConnectionConfig - new API never falls back to global config
402401 conn_config = ConnectionConfig(
403- _thread_safe = is_thread_safe ,
402+ _use_global_fallback = False ,
404403 ** ({" safemode" : safemode} if safemode is not None else {}),
405404 ** ({" stores" : stores} if stores is not None else {}),
406405 ** ({" database_prefix" : database_prefix} if database_prefix is not None else {}),
@@ -428,8 +427,9 @@ class ConnectionConfig:
428427 """
429428 Connection-scoped configuration (read/write).
430429
431- When thread_safe=False, unset values forward to global dj.config.
432- When thread_safe=True, unset values use defaults (no global access).
430+ Behavior depends on how the connection was created:
431+ - New API (from_config): Uses explicit values or defaults. Never accesses global config.
432+ - Legacy API (dj.conn): Forwards unset values to global dj.config.
433433 """
434434
435435 _DEFAULTS = {
@@ -448,7 +448,9 @@ class ConnectionConfig:
448448
449449 def __init__ (self , ** explicit_values ):
450450 self ._values = {} # Mutable storage for this connection
451- self ._thread_safe = explicit_values.pop(" _thread_safe" , False )
451+ # If True, forward unset values to global config (legacy API behavior)
452+ # If False, use defaults only (new API behavior)
453+ self ._use_global_fallback = explicit_values.pop(" _use_global_fallback" , False )
452454 self ._values.update(explicit_values)
453455
454456 def __getattr__ (self , name ):
@@ -459,12 +461,12 @@ class ConnectionConfig:
459461 if name in self ._values:
460462 return self ._values[name]
461463
462- # If thread_safe=False, forward to global config
463- if not self ._thread_safe :
464+ # Legacy API: forward to global config for backward compatibility
465+ if self ._use_global_fallback :
464466 from .settings import config
465467 return getattr (config, name, self ._DEFAULTS .get(name))
466468
467- # If thread_safe=True, return default
469+ # New API: use defaults only (no global config access)
468470 return self ._DEFAULTS .get(name)
469471
470472 def __setattr__ (self , name , value ):
0 commit comments