Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
6.3 (unreleased)
----------------

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


6.2 (2026-01-23)
================
Expand Down
12 changes: 11 additions & 1 deletion src/ZODB/DB.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ def __init__(self,
databases=None,
xrefs=True,
large_record_size=1 << 24,
class_factory=None,
**storage_args):
"""Create an object database.

Expand Down Expand Up @@ -407,6 +408,13 @@ def __init__(self,
:param int large_record_size: When object records are saved
that are larger than this, a warning is issued,
suggesting that blobs should be used instead.
:param callable class_factory: A callable
``class_factory(connection, module_name, global_name)``
used to resolve persistent object classes during
deserialization. If not provided, the default
``DB.classFactory`` method is used; it wraps
:func:`ZODB.broken.find_global` to provide this
three-argument interface.
:param storage_args: Extra keywork arguments passed to a
storage constructor if a path name or None is passed as
the storage argument.
Expand Down Expand Up @@ -465,6 +473,9 @@ def __init__(self,

self.large_record_size = large_record_size

if class_factory is not None:
self.classFactory = class_factory

# Make sure we have a root:
with self.transaction('initial database creation') as conn:
try:
Expand Down Expand Up @@ -847,7 +858,6 @@ def setActivityMonitor(self, am):
self._activity_monitor = am

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

def setCacheSize(self, size):
Expand Down
10 changes: 10 additions & 0 deletions src/ZODB/component.xml
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,16 @@
currently possible) are disallowed.
</description>
</key>
<key name="class-factory" datatype=".importable_name">
<description>
A callable used to resolve persistent object classes during
deserialization. The database-level class factory is called as
``class_factory(connection, module_name, global_name)``.
Specify a Python dotted-path name.
If not provided, the database uses its default class factory,
which delegates to ``ZODB.broken.find_global``.
</description>
</key>

</sectiontype>

Expand Down
26 changes: 26 additions & 0 deletions src/ZODB/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,38 @@
##############################################################################
"""Open database and storage from a configuration."""
import os
import traceback
from io import StringIO

import ZConfig

import ZODB


def importable_name(name):
# A datatype that converts a Python dotted-path-name to an object
try:
components = name.split('.')
start = components[0]
g = globals()
package = __import__(start, g, g)
modulenames = [start]
for component in components[1:]:
modulenames.append(component)
try:
package = getattr(package, component)
except AttributeError:
n = '.'.join(modulenames)
package = __import__(n, g, g, component)
return package
except ImportError:
IO = StringIO()
traceback.print_exc(file=IO)
raise ValueError(
f'The object named by {name!r} could not be imported\n'
f'{IO.getvalue()}')


db_schema_path = os.path.join(ZODB.__path__[0], "config.xml")
_db_schema = None

Expand Down Expand Up @@ -150,6 +175,7 @@ def _option(name, oname=None):
_option('pool_timeout')
_option('allow_implicit_cross_references', 'xrefs')
_option('large_record_size')
_option('class_factory')

try:
return ZODB.DB(
Expand Down
50 changes: 50 additions & 0 deletions src/ZODB/tests/testConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,56 @@ def database_xrefs_config():
"""


def dummy_class_factory(connection, module_name, global_name):
"""Helper function for database_class_factory_config
"""


def database_class_factory_config():
r"""The class-factory option sets the class factory used for
deserializing persistent objects.

Without it, the default DB.classFactory method is used:

>>> db = ZODB.config.databaseFromString(
... "<zodb>\n<mappingstorage>\n</mappingstorage>\n</zodb>\n")
>>> import types
>>> isinstance(db.classFactory, types.MethodType)
True
>>> db.close()

With a dotted name, the specified callable is used:

>>> db = ZODB.config.databaseFromString(
... "<zodb>\nclass-factory ZODB.tests.testConfig.dummy_class_factory\n"
... "<mappingstorage>\n</mappingstorage>\n</zodb>\n")
>>> db.classFactory is dummy_class_factory
True

The factory is available to connections, including the one
pooled during __init__:

>>> conn = db.open()
>>> conn._reader._factory is dummy_class_factory
True
Comment thread
dataflake marked this conversation as resolved.
>>> conn.close()
>>> db.close()

When the class factory is set to a non-existent callable, a detailed
error is raised:
>>> db = ZODB.config.databaseFromString(
... "<zodb>\n"
... "class-factory ZODB.tests.testConfig.non_existent_class_factory\n"
... "<mappingstorage>\n</mappingstorage>\n</zodb>\n"
... ) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
Traceback (most recent call last):
...
ZConfig.DataConversionError: The object named by 'ZODB.tests.testConfig.non_existent_class_factory' could not be imported
Traceback (most recent call last):
...
""" # noqa: E501


def multi_atabases():
r"""If there are multiple codb sections -> multidatabase

Expand Down
32 changes: 32 additions & 0 deletions src/ZODB/tests/testDB.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,38 @@ def passing_None_to_DB():
"""


def class_factory_parameter():
"""The class_factory parameter lets you set a custom class resolver
at construction time, before any connection is created.

>>> from ZODB.broken import find_global
>>> calls = []
>>> def my_factory(conn, module, name):
... calls.append((module, name))
... return find_global(module, name)

>>> db = ZODB.DB(None, class_factory=my_factory)
>>> db.classFactory is my_factory
True

The connection pooled during __init__ has the custom factory:

>>> conn = db.open()
>>> conn._reader._factory is my_factory
True
>>> conn.close()

Reused connections also have the custom factory:

>>> conn2 = db.open()
>>> conn2._reader._factory is my_factory
True
>>> conn2.close()

>>> db.close()
"""


def open_convenience():
"""Often, we just want to open a single connection.

Expand Down
Loading