Skip to content

Commit 461388e

Browse files
committed
Add class_factory parameter to DB and ZConfig class-factory option
DB.__init__() opens a connection to verify the root object exists, and that connection is returned to the pool for future reuse. Connections capture the classFactory reference at creation time (in Connection.__init__ via ObjectReader), so setting db.classFactory after DB.__init__() does not apply to connections already in the pool. This is a problem for Zope, which overrides DB.classFactory after construction to provide graceful handling of broken/uninstalled objects. The first connection handed out after startup may silently use the wrong class factory. Add a class_factory parameter to DB.__init__() that is set before any connection is opened, and a corresponding class-factory option to the ZConfig database configuration schema. This allows frameworks like Zope to pass the class factory at construction time.
1 parent 21ae6a7 commit 461388e

6 files changed

Lines changed: 110 additions & 1 deletion

File tree

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
6.3 (unreleased)
66
----------------
77

8+
- Add ``class_factory`` parameter to ``DB.__init__()`` and
9+
``class-factory`` option to ZConfig database configuration.
10+
See `issue #420 <https://github.com/zopefoundation/ZODB/issues/420>`_.
11+
812

913
6.2 (2026-01-23)
1014
================

src/ZODB/DB.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ def __init__(self,
360360
databases=None,
361361
xrefs=True,
362362
large_record_size=1 << 24,
363+
class_factory=None,
363364
**storage_args):
364365
"""Create an object database.
365366
@@ -407,6 +408,13 @@ def __init__(self,
407408
:param int large_record_size: When object records are saved
408409
that are larger than this, a warning is issued,
409410
suggesting that blobs should be used instead.
411+
:param callable class_factory: A callable
412+
``class_factory(connection, module_name, global_name)``
413+
used to resolve persistent object classes during
414+
deserialization. If not provided, the default
415+
``DB.classFactory`` method is used; it wraps
416+
:func:`ZODB.broken.find_global` to provide this
417+
three-argument interface.
410418
:param storage_args: Extra keywork arguments passed to a
411419
storage constructor if a path name or None is passed as
412420
the storage argument.
@@ -465,6 +473,9 @@ def __init__(self,
465473

466474
self.large_record_size = large_record_size
467475

476+
if class_factory is not None:
477+
self.classFactory = class_factory
478+
468479
# Make sure we have a root:
469480
with self.transaction('initial database creation') as conn:
470481
try:
@@ -847,7 +858,6 @@ def setActivityMonitor(self, am):
847858
self._activity_monitor = am
848859

849860
def classFactory(self, connection, modulename, globalname):
850-
# Zope will rebind this method to arbitrary user code at runtime.
851861
return find_global(modulename, globalname)
852862

853863
def setCacheSize(self, size):

src/ZODB/component.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@
324324
currently possible) are disallowed.
325325
</description>
326326
</key>
327+
<key name="class-factory" datatype=".importable_name">
328+
<description>
329+
A callable used to resolve persistent object classes during
330+
deserialization. The database-level class factory is called as
331+
``class_factory(connection, module_name, global_name)``.
332+
Specify a Python dotted-path name.
333+
If not provided, the database uses its default class factory,
334+
which delegates to ``ZODB.broken.find_global``.
335+
</description>
336+
</key>
327337

328338
</sectiontype>
329339

src/ZODB/config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@
2020
import ZODB
2121

2222

23+
def importable_name(name):
24+
"""ZConfig datatype that imports a dotted Python name."""
25+
components = name.split('.')
26+
used = [components[0]]
27+
obj = __import__(components[0])
28+
for component in components[1:]:
29+
used.append(component)
30+
try:
31+
obj = getattr(obj, component)
32+
except AttributeError:
33+
__import__('.'.join(used))
34+
obj = getattr(obj, component)
35+
return obj
36+
37+
2338
db_schema_path = os.path.join(ZODB.__path__[0], "config.xml")
2439
_db_schema = None
2540

@@ -150,6 +165,7 @@ def _option(name, oname=None):
150165
_option('pool_timeout')
151166
_option('allow_implicit_cross_references', 'xrefs')
152167
_option('large_record_size')
168+
_option('class_factory')
153169

154170
try:
155171
return ZODB.DB(

src/ZODB/tests/testConfig.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,43 @@ def database_xrefs_config():
128128
"""
129129

130130

131+
def dummy_class_factory(connection, module_name, global_name):
132+
"""Helper function for database_class_factory_config
133+
"""
134+
135+
136+
def database_class_factory_config():
137+
r"""The class-factory option sets the class factory used for
138+
deserializing persistent objects.
139+
140+
Without it, the default DB.classFactory method is used:
141+
142+
>>> db = ZODB.config.databaseFromString(
143+
... "<zodb>\n<mappingstorage>\n</mappingstorage>\n</zodb>\n")
144+
>>> import types
145+
>>> isinstance(db.classFactory, types.MethodType)
146+
True
147+
>>> db.close()
148+
149+
With a dotted name, the specified callable is used:
150+
151+
>>> db = ZODB.config.databaseFromString(
152+
... "<zodb>\nclass-factory ZODB.tests.testConfig.dummy_class_factory\n"
153+
... "<mappingstorage>\n</mappingstorage>\n</zodb>\n")
154+
>>> db.classFactory is dummy_class_factory
155+
True
156+
157+
The factory is available to connections, including the one
158+
pooled during __init__:
159+
160+
>>> conn = db.open()
161+
>>> conn._reader._factory is dummy_class_factory
162+
True
163+
>>> conn.close()
164+
>>> db.close()
165+
"""
166+
167+
131168
def multi_atabases():
132169
r"""If there are multiple codb sections -> multidatabase
133170

src/ZODB/tests/testDB.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,38 @@ def passing_None_to_DB():
324324
"""
325325

326326

327+
def class_factory_parameter():
328+
"""The class_factory parameter lets you set a custom class resolver
329+
at construction time, before any connection is created.
330+
331+
>>> from ZODB.broken import find_global
332+
>>> calls = []
333+
>>> def my_factory(conn, module, name):
334+
... calls.append((module, name))
335+
... return find_global(module, name)
336+
337+
>>> db = ZODB.DB(None, class_factory=my_factory)
338+
>>> db.classFactory is my_factory
339+
True
340+
341+
The connection pooled during __init__ has the custom factory:
342+
343+
>>> conn = db.open()
344+
>>> conn._reader._factory is my_factory
345+
True
346+
>>> conn.close()
347+
348+
Reused connections also have the custom factory:
349+
350+
>>> conn2 = db.open()
351+
>>> conn2._reader._factory is my_factory
352+
True
353+
>>> conn2.close()
354+
355+
>>> db.close()
356+
"""
357+
358+
327359
def open_convenience():
328360
"""Often, we just want to open a single connection.
329361

0 commit comments

Comments
 (0)