Skip to content

Commit 3eb43f0

Browse files
bugfix 673 1446 (#807)
1 parent 3df9fb4 commit 3eb43f0

4 files changed

Lines changed: 135 additions & 1 deletion

File tree

‎python_tests/test_memo.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55
import dbzero as db0
66
from .memo_test_types import MemoTestClass, TriColor, MemoAnyAttrs
7+
from abc import ABC, abstractmethod
78
from dataclasses import dataclass
89

910

@@ -37,6 +38,33 @@ def value(self, new_value):
3738
@db0.memo
3839
class MemoDerivedClassNoInit(MemoTestClass):
3940
pass
41+
42+
43+
@db0.memo
44+
class MemoAbstractBase(ABC):
45+
@abstractmethod
46+
def describe(self):
47+
pass
48+
49+
50+
@db0.memo
51+
class MemoConcreteDerived(MemoAbstractBase):
52+
def __init__(self, value):
53+
self.value = value
54+
55+
def describe(self):
56+
return f"value={self.value}"
57+
58+
59+
@db0.memo
60+
class MemoManualAbstractMethods:
61+
__abstractmethods__ = frozenset({"describe"})
62+
63+
def __init__(self, value):
64+
self.value = value
65+
66+
def describe(self):
67+
return f"value={self.value}"
4068

4169

4270
def test_memo_rejects_removed_option():
@@ -55,6 +83,21 @@ class RemovedCacheMemoOption:
5583
pass
5684

5785

86+
def test_memo_respects_abc_abstract_instantiation_check(db0_fixture):
87+
with pytest.raises(TypeError, match="abstract class"):
88+
MemoAbstractBase()
89+
90+
91+
def test_memo_allows_concrete_abc_derived_class(db0_fixture):
92+
obj = MemoConcreteDerived(123)
93+
assert obj.describe() == "value=123"
94+
95+
96+
def test_memo_manual_abstractmethods_without_abc_does_not_block_instantiation(db0_fixture):
97+
obj = MemoManualAbstractMethods(123)
98+
assert obj.describe() == "value=123"
99+
100+
58101
def test_memo_is_instance_operator(db0_fixture):
59102
obj_1 = MemoTestClass(999)
60103
obj_2 = db0.fetch(db0.uuid(obj_1))

‎python_tests/test_memo_singleton.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import pytest
55
import dbzero as db0
6+
import subprocess
7+
import sys
68
from .memo_test_types import MemoTestClass, MemoTestSingleton, MemoScopedSingleton, MemoDataPxSingleton
79
from .memo_test_types import MemoSingletonWithMigrations
810

@@ -56,6 +58,23 @@ def test_create_then_open_dynamically_scoped_singleton(db0_fixture):
5658
assert obj.value == 123
5759

5860

61+
def test_missing_singleton_in_read_only_prefix_reports_type_and_cause(db0_fixture):
62+
prefix = db0.get_current_prefix().name
63+
MemoScopedSingleton("existing-singleton")
64+
db0.commit()
65+
db0.close()
66+
67+
db0.init(DB0_DIR)
68+
db0.open(prefix, "r")
69+
with pytest.raises(Exception) as exc_info:
70+
MemoTestSingleton(123)
71+
72+
message = str(exc_info.value)
73+
assert "MemoTestSingleton" in message
74+
assert "read-only" in message
75+
assert "singleton instance does not exist" in message
76+
77+
5978
def test_find_singleton_static_scope(db0_fixture):
6079
# find singleton with a static-scope
6180
assert db0.find_singleton(MemoDataPxSingleton) is None
@@ -74,6 +93,43 @@ def test_find_singleton(db0_fixture):
7493
assert db0.find_singleton(MemoScopedSingleton, prefix = "my-test-prefix-1") is obj_2
7594

7695

96+
def test_find_singleton_from_read_only_prefix_does_not_acquire_write_lock(db0_fixture):
97+
prefix = "read-only-singleton-prefix"
98+
obj = MemoScopedSingleton(123, prefix=prefix)
99+
uuid = db0.uuid(obj)
100+
db0.commit(prefix)
101+
del obj
102+
db0.close(prefix)
103+
104+
child_code = """
105+
import sys
106+
import time
107+
import dbzero as db0
108+
109+
db0.init(sys.argv[1])
110+
db0.open(sys.argv[2], "rw")
111+
print("ready", flush=True)
112+
time.sleep(10)
113+
db0.close()
114+
"""
115+
process = subprocess.Popen(
116+
[sys.executable, "-c", child_code, DB0_DIR, prefix],
117+
stdout=subprocess.PIPE,
118+
stderr=subprocess.PIPE,
119+
text=True,
120+
)
121+
try:
122+
assert process.stdout.readline().strip() == "ready"
123+
obj = db0.find_singleton(MemoScopedSingleton, prefix=prefix)
124+
125+
assert obj is not None
126+
assert db0.uuid(obj) == uuid
127+
assert obj.value == 123
128+
finally:
129+
process.terminate()
130+
process.communicate(timeout=5)
131+
132+
77133
def test_singleton_with_migrations(db0_fixture):
78134
obj_1 = MemoSingletonWithMigrations(123)
79135

‎src/dbzero/bindings/python/Memo.cpp‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
#define Py_TPFLAGS_MANAGED_DICT 0
3636
#endif
3737

38+
#ifndef Py_TPFLAGS_IS_ABSTRACT
39+
#define Py_TPFLAGS_IS_ABSTRACT (1UL << 20)
40+
#endif
41+
3842
namespace db0::python
3943

4044
{
@@ -82,10 +86,23 @@ namespace db0::python
8286
}
8387
return result;
8488
}
89+
90+
bool checkMemoTypeAbstractInstantiation(PyTypeObject *py_type)
91+
{
92+
if (!(py_type->tp_flags & Py_TPFLAGS_IS_ABSTRACT)) {
93+
return true;
94+
}
95+
PyErr_Format(PyExc_TypeError, "Can't instantiate abstract class %s", py_type->tp_name);
96+
return false;
97+
}
8598

8699
template <typename MemoImplT>
87100
MemoImplT *tryMemoObject_new(const MemoTypeDecoration &decor, PyTypeObject *py_type, PyObject *, PyObject *)
88101
{
102+
if (!checkMemoTypeAbstractInstantiation(py_type)) {
103+
return nullptr;
104+
}
105+
89106
// NOTE: read-only fixture access is sufficient here since objects are lazy-initialized
90107
// i.e. the actual dbzero instance is created on postInit
91108
// this is also important for dynamically scoped clases (where read/write access may not be possible on default fixture)
@@ -193,6 +210,10 @@ namespace db0::python
193210

194211
PyObject *tryMemoObject_new_singleton(PyTypeObject *py_type, PyObject *args, PyObject *kwargs)
195212
{
213+
if (!checkMemoTypeAbstractInstantiation(py_type)) {
214+
return nullptr;
215+
}
216+
196217
std::string px_name;
197218
std::uint64_t fixture_uuid = 0;
198219
// try resolve type specific scope: static, dynamic or default
@@ -206,6 +227,12 @@ namespace db0::python
206227
if (result) {
207228
return result;
208229
}
230+
if (fixture->getAccessType() == AccessType::READ_ONLY) {
231+
THROWF(db0::InputException)
232+
<< "Cannot create singleton " << PyToolkit::getTypeName(py_type)
233+
<< " in read-only prefix '" << fixture->getPrefix().getName()
234+
<< "': singleton instance does not exist";
235+
}
209236
}
210237

211238
// fixture, does not exist, try creating a new one
@@ -972,6 +999,9 @@ namespace db0::python
972999

9731000
// Enable GC for Python 3.10 compatibility - required for inheritance hierarchies
9741001
std::uint32_t flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
1002+
if (base_class->tp_flags & Py_TPFLAGS_IS_ABSTRACT) {
1003+
flags |= Py_TPFLAGS_IS_ABSTRACT;
1004+
}
9751005
#if PY_VERSION_HEX < 0x030B0000 // Python < 3.11
9761006
flags |= Py_TPFLAGS_HAVE_GC;
9771007
#endif

‎src/dbzero/bindings/python/PyAPI.cpp‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1673,7 +1673,12 @@ namespace db0::python
16731673
}
16741674

16751675
auto &workspace = PyToolkit::getPyWorkspace().getWorkspace();
1676-
db0::swine_ptr<Fixture> fixture = prefix_name ? workspace.getFixture(prefix_name) : workspace.getCurrentFixture();
1676+
db0::swine_ptr<Fixture> fixture = prefix_name
1677+
? workspace.tryGetFixture(prefix_name, AccessType::READ_ONLY)
1678+
: workspace.getCurrentFixture();
1679+
if (!fixture) {
1680+
Py_RETURN_NONE;
1681+
}
16771682
fixture->refreshIfUpdated();
16781683
auto py_singleton = tryMemoObject_open_singleton(reinterpret_cast<PyTypeObject*>(py_type), *fixture);
16791684
if (!py_singleton) {

0 commit comments

Comments
 (0)