diff --git a/dbzero/dbzero/interfaces.py b/dbzero/dbzero/interfaces.py index 67dceb60..8b7104ea 100644 --- a/dbzero/dbzero/interfaces.py +++ b/dbzero/dbzero/interfaces.py @@ -201,7 +201,7 @@ def fetch(self, id: Union[str, type], type: Optional[type] = None, prefix: Optio """ ... - def find(self, *query_criteria: Union[Tag, List[Tag], Tuple[Tag], QueryObject, TagSet]) -> QueryObject: + def find(self, *query_criteria: Union[Tag, List[Tag], Tuple[Tag], QueryObject, TagSet], prefix: Optional[str] = None) -> QueryObject: """Query for memo objects based on search criteria such as tags, types, or subqueries. Parameters @@ -216,6 +216,9 @@ def find(self, *query_criteria: Union[Tag, List[Tag], Tuple[Tag], QueryObject, T * Tuple of tags (AND): Objects with all of the specified tags * QueryObject: Result of another query * TagSet: Logical set operation. + prefix : str, optional + Optional data prefix to run the query on. + If omitted, the prefix to run the query is resolved based on query criteria. Returns ------- diff --git a/dbzero/dbzero/reflection_api.py b/dbzero/dbzero/reflection_api.py index 7c785324..7179b51d 100644 --- a/dbzero/dbzero/reflection_api.py +++ b/dbzero/dbzero/reflection_api.py @@ -155,17 +155,23 @@ def get_schema(self): def all(self, snapshot=None, as_memo_base=False): """Find all instances of this Memo class.""" - if not as_memo_base and self.get_class().is_known_type(): + + cls = self.get_class() + prefix_name = db0.get_prefix_of(cls).name + + if not cls.type_exists(): + as_memo_base = True + + if as_memo_base: if snapshot is not None: - return snapshot.find(self.get_class().type()) + return snapshot.find(db0.MemoBase, cls, prefix=prefix_name) else: - return db0.find(self.get_class().type()) - - # fall back to the base class if the actual model class is not imported + return db0.find(db0.MemoBase, cls, prefix=prefix_name) + if snapshot is not None: - return snapshot.find(db0.MemoBase, self.get_class()) + return snapshot.find(cls, prefix=prefix_name) else: - return db0.find(db0.MemoBase, self.get_class()) + return db0.find(cls, prefix=prefix_name) def get_instance_count(self): """Get number of instances of this Memo class.""" diff --git a/python_tests/test_reflection_api.py b/python_tests/test_reflection_api.py index 818614d8..443af205 100644 --- a/python_tests/test_reflection_api.py +++ b/python_tests/test_reflection_api.py @@ -356,3 +356,4 @@ def test_get_memo_class_of_instance(db0_fixture): obj = MemoTestClass(123) memo_class = db0.get_memo_class(obj) assert memo_class is not None + assert db0.get_prefix_of(obj) == db0.get_prefix_of(memo_class.get_class()) diff --git a/src/dbzero/bindings/python/ArgParse.cpp b/src/dbzero/bindings/python/ArgParse.cpp new file mode 100644 index 00000000..447d9d5a --- /dev/null +++ b/src/dbzero/bindings/python/ArgParse.cpp @@ -0,0 +1,31 @@ +#include "ArgParse.hpp" + +namespace db0::python +{ + +const char* parseStringLikeArgument(PyObject *arg, const char *func_name, const char *arg_name) { + const char* result = nullptr; + + if (PyUnicode_Check(arg)) { + result = PyUnicode_AsUTF8(arg); + if (!result) { + // Exception already set by PyUnicode_AsUTF8 + return nullptr; + } + } else if (PyBytes_Check(arg)) { + result = PyBytes_AsString(arg); + if (!result) { + // Exception already set by PyBytes_AsString + return nullptr; + } + } else { + PyErr_Format(PyExc_TypeError, + "%s() argument '%s' must be str or bytes, not %s", + func_name, arg_name, Py_TYPE(arg)->tp_name); + return nullptr; + } + + return result; +} + +} diff --git a/src/dbzero/bindings/python/ArgParse.hpp b/src/dbzero/bindings/python/ArgParse.hpp new file mode 100644 index 00000000..0a4d93a9 --- /dev/null +++ b/src/dbzero/bindings/python/ArgParse.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace db0::python +{ + +const char* parseStringLikeArgument(PyObject *arg, const char *func_name, const char *arg_name); + +} diff --git a/src/dbzero/bindings/python/PyAPI.cpp b/src/dbzero/bindings/python/PyAPI.cpp index a6b419fa..f0b4df80 100644 --- a/src/dbzero/bindings/python/PyAPI.cpp +++ b/src/dbzero/bindings/python/PyAPI.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -392,7 +393,7 @@ namespace db0::python if (prefix_name) { PyToolkit::getPyWorkspace().getWorkspace().close(db0::PrefixName(prefix_name)); } else { - PyToolkit::getPyWorkspace().close(); + PyToolkit::getPyWorkspace().close(); } Py_RETURN_NONE; } @@ -458,6 +459,8 @@ namespace db0::python fixture = reinterpret_cast(py_object)->ext().getFixture(); } else if (PyObjectIterator_Check(py_object)) { fixture = reinterpret_cast(py_object)->ext().getFixture(); + } else if (PyClassObject_Check(py_object)) { + fixture = reinterpret_cast(py_object)->ext().getFixture(); } else { fixture = getFixtureOf(py_object); } diff --git a/src/dbzero/bindings/python/PySnapshot.cpp b/src/dbzero/bindings/python/PySnapshot.cpp index 4b499a64..4451907c 100644 --- a/src/dbzero/bindings/python/PySnapshot.cpp +++ b/src/dbzero/bindings/python/PySnapshot.cpp @@ -4,6 +4,7 @@ #include "PyTagsAPI.hpp" #include "Memo.hpp" #include +#include namespace db0::python @@ -120,16 +121,34 @@ namespace db0::python return tryFetchFrom(snapshot, py_id, type, prefix_name).steal(); } - PyObject *tryPySnapshot_find(PyObject *self, PyObject *const *args, Py_ssize_t nargs) + PyObject *tryPySnapshot_find(PyObject *self, PyObject *args, PyObject *kwargs) { if (!PySnapshot_Check(self)) { PyErr_SetString(PyExc_TypeError, "Invalid argument type"); return NULL; } + Py_ssize_t num_args = PyTuple_Size(args); + std::vector args_data(num_args); + for (Py_ssize_t i = 0; i < num_args; ++i) { + args_data[i] = PyTuple_GetItem(args, i); + } + + const char prefix_arg[] = "prefix"; + const char *prefix_name = nullptr; + if (kwargs) { + PyObject *py_prefix_name = PyDict_GetItemString(kwargs, prefix_arg); + if (py_prefix_name) { + prefix_name = parseStringLikeArgument(py_prefix_name, "find", prefix_arg); + if (!prefix_name) { + return nullptr; + } + } + } + auto &snapshot = reinterpret_cast(self)->modifyExt(); // NOTE: self attached as context - return findIn(snapshot, args, nargs, self); + return findIn(snapshot, (PyObject* const*)args_data.data(), num_args, self, prefix_name); } PyObject *tryGetStateNum(db0::Snapshot &snapshot, PyObject *args, PyObject *kwargs) @@ -176,10 +195,10 @@ namespace db0::python return runSafe(tryPySnapshot_fetch, self, py_id, reinterpret_cast(py_type), prefix_name); } - PyObject *PyAPI_PySnapshot_find(PyObject *self, PyObject *const *args, Py_ssize_t nargs) + PyObject *PyAPI_PySnapshot_find(PyObject *self, PyObject *args, PyObject *kwargs) { PY_API_FUNC - return runSafe(tryPySnapshot_find, self, args, nargs); + return runSafe(tryPySnapshot_find, self, args, kwargs); } PyObject *PyAPI_PySnapshot_enter(PyObject *self, PyObject *) @@ -276,7 +295,7 @@ namespace db0::python static PyMethodDef PySnapshot_methods[] = { {"fetch", (PyCFunction)&PyAPI_PySnapshot_fetch, METH_VARARGS | METH_KEYWORDS, "Fetch dbzero object instance by its ID or type (in case of a singleton)"}, - {"find", (PyCFunction)&PyAPI_PySnapshot_find, METH_FASTCALL, ""}, + {"find", (PyCFunction)&PyAPI_PySnapshot_find, METH_VARARGS | METH_KEYWORDS, ""}, {"deserialize", (PyCFunction)&PyAPI_PySnapshot_deserialize, METH_FASTCALL, "Deserialize from bytes within the snapshot's context"}, {"close", &PyAPI_PySnapshot_close, METH_NOARGS, "Close dbzero snapshot"}, {"get_state_num", (PyCFunction)&PyAPI_PySnapshot_GetStateNum, METH_VARARGS | METH_KEYWORDS, "Get state number of the snapshot"}, diff --git a/src/dbzero/bindings/python/iter/PyJoinIterable.cpp b/src/dbzero/bindings/python/iter/PyJoinIterable.cpp index 21c533e6..d029a157 100644 --- a/src/dbzero/bindings/python/iter/PyJoinIterable.cpp +++ b/src/dbzero/bindings/python/iter/PyJoinIterable.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace db0::python @@ -96,7 +97,7 @@ namespace db0::python for (Py_ssize_t i = 0; i < num_args; ++i) { args_data[i] = PyTuple_GetItem(args, i); } - const char *prefix_name = nullptr; + if (!kwargs) { // The "join" function requires "on" as keyword argument PyErr_SetString(PyExc_TypeError, "join() missing 1 required keyword argument: 'on'"); @@ -109,19 +110,13 @@ namespace db0::python return NULL; } + const char prefix_arg[] = "prefix"; + const char *prefix_name = nullptr; PyObject *py_prefix_name = PyDict_GetItemString(kwargs, "prefix"); if (py_prefix_name) { - if (!PyUnicode_Check(py_prefix_name) && !PyBytes_Check(py_prefix_name)) { - std::stringstream err_msg; - err_msg << "Expected 'prefix' argument to be a string or bytes, got: " << (py_prefix_name ? Py_TYPE(py_prefix_name)->tp_name : "None"); - PyErr_SetString(PyExc_TypeError, err_msg.str().c_str()); - return NULL; - } - if (PyUnicode_Check(py_prefix_name)) { - prefix_name = PyUnicode_AsUTF8(py_prefix_name); - } else { - assert(PyBytes_Check(py_prefix_name)); - prefix_name = PyBytes_AsString(py_prefix_name); + prefix_name = parseStringLikeArgument(py_prefix_name, "join", prefix_arg); + if (!prefix_name) { + return nullptr; } } diff --git a/src/dbzero/bindings/python/iter/PyObjectIterable.cpp b/src/dbzero/bindings/python/iter/PyObjectIterable.cpp index 91a2e88f..047cc261 100644 --- a/src/dbzero/bindings/python/iter/PyObjectIterable.cpp +++ b/src/dbzero/bindings/python/iter/PyObjectIterable.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace db0::python @@ -251,21 +252,14 @@ namespace db0::python args_data[i] = PyTuple_GetItem(args, i); } + const char prefix_arg[] = "prefix"; const char *prefix_name = nullptr; if (kwargs) { - PyObject *py_prefix_name = PyDict_GetItemString(kwargs, "prefix"); + PyObject *py_prefix_name = PyDict_GetItemString(kwargs, prefix_arg); if (py_prefix_name) { - if (!PyUnicode_Check(py_prefix_name) && !PyBytes_Check(py_prefix_name)) { - std::stringstream err_msg; - err_msg << "Expected 'prefix' argument to be a string or bytes, got: " << (py_prefix_name ? Py_TYPE(py_prefix_name)->tp_name : "None"); - PyErr_SetString(PyExc_TypeError, err_msg.str().c_str()); - return NULL; - } - if (PyUnicode_Check(py_prefix_name)) { - prefix_name = PyUnicode_AsUTF8(py_prefix_name); - } else { - assert(PyBytes_Check(py_prefix_name)); - prefix_name = PyBytes_AsString(py_prefix_name); + prefix_name = parseStringLikeArgument(py_prefix_name, "find", prefix_arg); + if (!prefix_name) { + return nullptr; } } } diff --git a/src/dbzero/bindings/python/types/PyClass.cpp b/src/dbzero/bindings/python/types/PyClass.cpp index 44ba540c..42e2595f 100644 --- a/src/dbzero/bindings/python/types/PyClass.cpp +++ b/src/dbzero/bindings/python/types/PyClass.cpp @@ -77,14 +77,14 @@ namespace db0::python return py_list.steal(); } - PyObject *tryGetAttributes(PyObject *self) { + PyObject *tryGetPyClassAttributes(PyObject *self) { return tryGetClassAttributes(reinterpret_cast(self)->ext()); } PyObject *PyAPI_PyClass_get_attributes(PyObject *self, PyObject *) { PY_API_FUNC - return runSafe(tryGetAttributes, self); + return runSafe(tryGetPyClassAttributes, self); } PyObject *PyAPI_PyClass_type_info(PyObject *self, PyObject *) diff --git a/src/dbzero/bindings/python/types/PyClass.hpp b/src/dbzero/bindings/python/types/PyClass.hpp index f71503f5..8407002c 100644 --- a/src/dbzero/bindings/python/types/PyClass.hpp +++ b/src/dbzero/bindings/python/types/PyClass.hpp @@ -25,7 +25,7 @@ namespace db0::python ClassObject *makeClass(std::shared_ptr); bool PyClassObject_Check(PyObject *); - PyObject *tryGetAttributes(PyObject *); + PyObject *tryGetPyClassAttributes(PyObject *); PyObject *tryGetClassAttributes(const db0::object_model::Class &); PyObject *tryGetTypeInfo(const db0::object_model::Class &); diff --git a/src/dbzero/object_model/tags/TagIndex.cpp b/src/dbzero/object_model/tags/TagIndex.cpp index 6d1f1d5e..be8a6191 100644 --- a/src/dbzero/object_model/tags/TagIndex.cpp +++ b/src/dbzero/object_model/tags/TagIndex.cpp @@ -1014,7 +1014,7 @@ namespace db0::object_model std::size_t nargs, const char *prefix_name) { if (prefix_name) { - return workspace.getFixture(prefix_name); + return workspace.getFixture(prefix_name, std::nullopt); } std::uint64_t fixture_uuid = 0;