Skip to content

Commit 04a406d

Browse files
refactor: replace global config reads with connection-scoped config
All internal code now reads configuration from self.connection._config instead of the global config singleton. This ensures thread-safe mode works correctly: each Instance's connection carries its own config, and tables/schemas/jobs access it through the connection. Changes across 9 files: - schemas.py: safemode, create_tables default - table.py: safemode in delete/drop, config passed to declare() - expression.py: loglevel in __repr__ - preview.py: display.* settings via query_expression.connection._config - autopopulate.py: jobs.allow_new_pk_fields, jobs.auto_refresh - jobs.py: jobs.default_priority, stale_timeout, keep_completed - declare.py: jobs.add_job_metadata (config param threaded through) - diagram.py: display.diagram_direction (connection stored on instance) - staged_insert.py: get_store_spec() Removed unused `from .settings import config` imports from 7 modules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 845efc0 commit 04a406d

File tree

9 files changed

+42
-41
lines changed

9 files changed

+42
-41
lines changed

src/datajoint/autopopulate.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,8 @@ def _declare_check(self, primary_key: list[str], fk_attribute_map: dict[str, tup
146146
If native (non-FK) PK attributes are found, unless bypassed via
147147
``dj.config.jobs.allow_new_pk_fields_in_computed_tables = True``.
148148
"""
149-
from .settings import config
150-
151149
# Check if validation is bypassed
152-
if config.jobs.allow_new_pk_fields_in_computed_tables:
150+
if self.connection._config.jobs.allow_new_pk_fields_in_computed_tables:
153151
return
154152

155153
# Check for native (non-FK) primary key attributes
@@ -477,8 +475,6 @@ def _populate_distributed(
477475
"""
478476
from tqdm import tqdm
479477

480-
from .settings import config
481-
482478
# Define a signal handler for SIGTERM
483479
def handler(signum, frame):
484480
logger.info("Populate terminated by SIGTERM")
@@ -489,7 +485,7 @@ def handler(signum, frame):
489485
try:
490486
# Refresh job queue if configured
491487
if refresh is None:
492-
refresh = config.jobs.auto_refresh
488+
refresh = self.connection._config.jobs.auto_refresh
493489
if refresh:
494490
# Use delay=-1 to ensure jobs are immediately schedulable
495491
# (avoids race condition with scheduled_time <= CURRENT_TIMESTAMP(3) check)
@@ -659,7 +655,7 @@ def _populate1(
659655
key,
660656
start_time=datetime.datetime.fromtimestamp(start_time),
661657
duration=duration,
662-
version=_get_job_version(),
658+
version=_get_job_version(self.connection._config),
663659
)
664660

665661
if jobs is not None:

src/datajoint/declare.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from .codecs import lookup_codec
1616
from .condition import translate_attribute
1717
from .errors import DataJointError
18-
from .settings import config
1918

2019
# Core DataJoint types - scientist-friendly names that are fully supported
2120
# These are recorded in field comments using :type: syntax for reconstruction
@@ -401,7 +400,7 @@ def prepare_declare(
401400

402401

403402
def declare(
404-
full_table_name: str, definition: str, context: dict, adapter
403+
full_table_name: str, definition: str, context: dict, adapter, *, config=None
405404
) -> tuple[str, list[str], list[str], dict[str, tuple[str, str]], list[str], list[str]]:
406405
r"""
407406
Parse a definition and generate SQL CREATE TABLE statement.
@@ -416,6 +415,8 @@ def declare(
416415
Namespace for resolving foreign key references.
417416
adapter : DatabaseAdapter
418417
Database adapter for backend-specific SQL generation.
418+
config : Config, optional
419+
Configuration object. If None, falls back to global config.
419420
420421
Returns
421422
-------
@@ -464,6 +465,10 @@ def declare(
464465
) = prepare_declare(definition, context, adapter)
465466

466467
# Add hidden job metadata for Computed/Imported tables (not parts)
468+
if config is None:
469+
from .settings import config as _config
470+
471+
config = _config
467472
if config.jobs.add_job_metadata:
468473
# Check if this is a Computed (__) or Imported (_) table, but not a Part (contains __ in middle)
469474
is_computed = table_name.startswith("__") and "__" not in table_name[2:]

src/datajoint/diagram.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
from .dependencies import topo_sort
1818
from .errors import DataJointError
19-
from .settings import config
2019
from .table import Table, lookup_class_name
2120
from .user_tables import Computed, Imported, Lookup, Manual, Part, _AliasNode, _get_tier
2221

@@ -105,6 +104,7 @@ def __init__(self, source, context=None) -> None:
105104
self.nodes_to_show = set(source.nodes_to_show)
106105
self._expanded_nodes = set(source._expanded_nodes)
107106
self.context = source.context
107+
self._connection = source._connection
108108
super().__init__(source)
109109
return
110110

@@ -126,6 +126,7 @@ def __init__(self, source, context=None) -> None:
126126
raise DataJointError("Could not find database connection in %s" % repr(source[0]))
127127

128128
# initialize graph from dependencies
129+
self._connection = connection
129130
connection.dependencies.load()
130131
super().__init__(connection.dependencies)
131132

@@ -584,7 +585,7 @@ def make_dot(self):
584585
Tables are grouped by schema, with the Python module name shown as the
585586
group label when available.
586587
"""
587-
direction = config.display.diagram_direction
588+
direction = self._connection._config.display.diagram_direction
588589
graph = self._make_graph()
589590

590591
# Apply collapse logic if needed
@@ -857,7 +858,7 @@ def make_mermaid(self) -> str:
857858
Session --> Neuron
858859
"""
859860
graph = self._make_graph()
860-
direction = config.display.diagram_direction
861+
direction = self._connection._config.display.diagram_direction
861862

862863
# Apply collapse logic if needed
863864
graph, collapsed_counts = self._apply_collapse(graph)

src/datajoint/expression.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from .errors import DataJointError
2121
from .codecs import decode_attribute
2222
from .preview import preview, repr_html
23-
from .settings import config
2423

2524
logger = logging.getLogger(__name__.split(".")[0])
2625

@@ -1247,7 +1246,7 @@ def __repr__(self):
12471246
str
12481247
String representation of the QueryExpression.
12491248
"""
1250-
return super().__repr__() if config["loglevel"].lower() == "debug" else self.preview()
1249+
return super().__repr__() if self.connection._config["loglevel"].lower() == "debug" else self.preview()
12511250

12521251
def preview(self, limit=None, width=None):
12531252
"""

src/datajoint/jobs.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,22 @@
2424
logger = logging.getLogger(__name__.split(".")[0])
2525

2626

27-
def _get_job_version() -> str:
27+
def _get_job_version(config=None) -> str:
2828
"""
2929
Get version string based on config settings.
3030
31+
Parameters
32+
----------
33+
config : Config, optional
34+
Configuration object. If None, falls back to global config.
35+
3136
Returns
3237
-------
3338
str
3439
Version string, or empty string if version tracking disabled.
3540
"""
36-
from .settings import config
41+
if config is None:
42+
from .settings import config
3743

3844
method = config.jobs.version_method
3945
if method is None or method == "none":
@@ -349,17 +355,15 @@ def refresh(
349355
3. Remove stale jobs: jobs older than stale_timeout whose keys not in key_source
350356
4. Remove orphaned jobs: reserved jobs older than orphan_timeout (if specified)
351357
"""
352-
from .settings import config
353-
354358
# Ensure jobs table exists
355359
if not self.is_declared:
356360
self.declare()
357361

358362
# Get defaults from config
359363
if priority is None:
360-
priority = config.jobs.default_priority
364+
priority = self.connection._config.jobs.default_priority
361365
if stale_timeout is None:
362-
stale_timeout = config.jobs.stale_timeout
366+
stale_timeout = self.connection._config.jobs.stale_timeout
363367

364368
result = {"added": 0, "removed": 0, "orphaned": 0, "re_pended": 0}
365369

@@ -392,7 +396,7 @@ def refresh(
392396
pass # Job already exists
393397

394398
# 2. Re-pend success jobs if keep_completed=True
395-
if config.jobs.keep_completed:
399+
if self.connection._config.jobs.keep_completed:
396400
# Success jobs whose keys are in key_source but not in target
397401
# Disable semantic_check for Job table operations (job table PK has different lineage than target)
398402
success_to_repend = self.completed.restrict(key_source, semantic_check=False).restrict(
@@ -463,7 +467,7 @@ def reserve(self, key: dict) -> bool:
463467
"pid": os.getpid(),
464468
"connection_id": self.connection.connection_id,
465469
"user": self.connection.get_user(),
466-
"version": _get_job_version(),
470+
"version": _get_job_version(self.connection._config),
467471
}
468472

469473
try:
@@ -490,9 +494,7 @@ def complete(self, key: dict, duration: float | None = None) -> None:
490494
- If True: updates status to ``'success'`` with completion time and duration
491495
- If False: deletes the job entry
492496
"""
493-
from .settings import config
494-
495-
if config.jobs.keep_completed:
497+
if self.connection._config.jobs.keep_completed:
496498
# Use server time for completed_time
497499
server_now = self.connection.query("SELECT CURRENT_TIMESTAMP").fetchone()[0]
498500
pk = self._get_pk(key)
@@ -550,13 +552,11 @@ def ignore(self, key: dict) -> None:
550552
key : dict
551553
Primary key dict of the job.
552554
"""
553-
from .settings import config
554-
555555
pk = self._get_pk(key)
556556
if pk in self:
557557
self.update1({**pk, "status": "ignore"})
558558
else:
559-
priority = config.jobs.default_priority
559+
priority = self.connection._config.jobs.default_priority
560560
self.insert1({**pk, "status": "ignore", "priority": priority})
561561

562562
def progress(self) -> dict:

src/datajoint/preview.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import json
44

5-
from .settings import config
6-
75

86
def _format_object_display(json_data):
97
"""Format object metadata for display in query results."""
@@ -44,6 +42,7 @@ def _get_blob_placeholder(heading, field_name, html_escape=False):
4442
def preview(query_expression, limit, width):
4543
heading = query_expression.heading
4644
rel = query_expression.proj(*heading.non_blobs)
45+
config = query_expression.connection._config
4746
# Object fields use codecs - not specially handled in simplified model
4847
object_fields = []
4948
if limit is None:
@@ -105,6 +104,7 @@ def get_display_value(tup, f, idx):
105104
def repr_html(query_expression):
106105
heading = query_expression.heading
107106
rel = query_expression.proj(*heading.non_blobs)
107+
config = query_expression.connection._config
108108
# Object fields use codecs - not specially handled in simplified model
109109
object_fields = []
110110
tuples = rel.to_arrays(limit=config["display.limit"] + 1)

src/datajoint/schemas.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from .connection import Connection
2424
from .heading import Heading
2525
from .jobs import Job
26-
from .settings import config
2726
from .table import FreeTable, lookup_class_name
2827
from .user_tables import Computed, Imported, Lookup, Manual, Part, _get_tier
2928
from .utils import to_camel_case, user_choice
@@ -120,7 +119,7 @@ def __init__(
120119
self.database = None
121120
self.context = context
122121
self.create_schema = create_schema
123-
self.create_tables = create_tables if create_tables is not None else config.database.create_tables
122+
self.create_tables = create_tables # None means "use connection config default"
124123
self.add_objects = add_objects
125124
self.declare_list = []
126125
if schema_name:
@@ -293,7 +292,10 @@ def _decorate_table(self, table_class: type, context: dict[str, Any], assert_dec
293292
# instantiate the class, declare the table if not already
294293
instance = table_class()
295294
is_declared = instance.is_declared
296-
if not is_declared and not assert_declared and self.create_tables:
295+
create_tables = (
296+
self.create_tables if self.create_tables is not None else self.connection._config.database.create_tables
297+
)
298+
if not is_declared and not assert_declared and create_tables:
297299
instance.declare(context)
298300
self.connection.dependencies.clear()
299301
is_declared = is_declared or instance.is_declared
@@ -409,7 +411,7 @@ def drop(self, prompt: bool | None = None) -> None:
409411
AccessError
410412
If insufficient permissions to drop the schema.
411413
"""
412-
prompt = config["safemode"] if prompt is None else prompt
414+
prompt = self.connection._config["safemode"] if prompt is None else prompt
413415

414416
if not self.exists:
415417
logger.info("Schema named `{database}` does not exist. Doing nothing.".format(database=self.database))

src/datajoint/staged_insert.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import fsspec
1515

1616
from .errors import DataJointError
17-
from .settings import config
1817
from .storage import StorageBackend, build_object_path
1918

2019

@@ -69,7 +68,7 @@ def _ensure_backend(self):
6968
"""Ensure storage backend is initialized."""
7069
if self._backend is None:
7170
try:
72-
spec = config.get_store_spec() # Uses stores.default
71+
spec = self._table.connection._config.get_store_spec() # Uses stores.default
7372
self._backend = StorageBackend(spec)
7473
except DataJointError:
7574
raise DataJointError(
@@ -110,7 +109,7 @@ def _get_storage_path(self, field: str, ext: str = "") -> str:
110109
)
111110

112111
# Get storage spec (uses stores.default)
113-
spec = config.get_store_spec()
112+
spec = self._table.connection._config.get_store_spec()
114113
partition_pattern = spec.get("partition_pattern")
115114
token_length = spec.get("token_length", 8)
116115

src/datajoint/table.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
)
2424
from .expression import QueryExpression
2525
from .heading import Heading
26-
from .settings import config
2726
from .staged_insert import staged_insert1 as _staged_insert1
2827
from .utils import get_master, is_camel_case, user_choice
2928

@@ -153,7 +152,7 @@ def declare(self, context=None):
153152
"Class names must be in CamelCase, starting with a capital letter."
154153
)
155154
sql, _external_stores, primary_key, fk_attribute_map, pre_ddl, post_ddl = declare(
156-
self.full_table_name, self.definition, context, self.connection.adapter
155+
self.full_table_name, self.definition, context, self.connection.adapter, config=self.connection._config
157156
)
158157

159158
# Call declaration hook for validation (subclasses like AutoPopulate can override)
@@ -1119,7 +1118,7 @@ def strip_quotes(s):
11191118
raise DataJointError("Exceeded maximum number of delete attempts.")
11201119
return delete_count
11211120

1122-
prompt = config["safemode"] if prompt is None else prompt
1121+
prompt = self.connection._config["safemode"] if prompt is None else prompt
11231122

11241123
# Start transaction
11251124
if transaction:
@@ -1227,7 +1226,7 @@ def drop(self, prompt: bool | None = None):
12271226
raise DataJointError(
12281227
"A table with an applied restriction cannot be dropped. Call drop() on the unrestricted Table."
12291228
)
1230-
prompt = config["safemode"] if prompt is None else prompt
1229+
prompt = self.connection._config["safemode"] if prompt is None else prompt
12311230

12321231
self.connection.dependencies.load()
12331232
do_drop = True

0 commit comments

Comments
 (0)