From 342fdcac28cb8b3ede1d52c316d8aebfebf1e01f Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 19 Jun 2025 23:56:58 +0200 Subject: [PATCH 1/6] CAPI --- .mypy.ini | 3 + Makefile | 3 +- multidict/_multidict.c | 38 +++--------- multidict/_multilib/capsule.h | 95 +++++++++++++++++++++++++++++ multidict/_multilib/state.h | 28 +++++++++ multidict/multidict_api.h | 85 ++++++++++++++++++++++++++ pyproject.toml | 2 +- pytest.ini | 1 + testcapi/pyproject.toml | 3 + testcapi/setup.cfg | 17 ++++++ testcapi/setup.py | 24 ++++++++ testcapi/testcapi/__init__.py | 4 ++ testcapi/testcapi/_api.c | 111 ++++++++++++++++++++++++++++++++++ tests/test_capi.py | 26 ++++++++ 14 files changed, 410 insertions(+), 30 deletions(-) create mode 100644 multidict/_multilib/capsule.h create mode 100644 multidict/multidict_api.h create mode 100644 testcapi/pyproject.toml create mode 100644 testcapi/setup.cfg create mode 100644 testcapi/setup.py create mode 100644 testcapi/testcapi/__init__.py create mode 100644 testcapi/testcapi/_api.c create mode 100644 tests/test_capi.py diff --git a/.mypy.ini b/.mypy.ini index bfac03128..30e2fc91f 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -29,3 +29,6 @@ warn_unused_ignores = True [mypy-test_incorrect_args] disable_error_code = arg-type, call-overload + +[mypy-test_capi] +disable_error_code = attr-defined diff --git a/Makefile b/Makefile index 29606109a..cd7028ab7 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,9 @@ all: test lint: python -Im pre_commit run --all-files --show-diff-on-failure -.develop: .install-deps $(shell find multidict -type f) +.develop: .install-deps $(shell find multidict -type f) $(shell find testcapi -type f) pip install -e . + cd testcapi; pip install -e . @touch .develop test: .develop diff --git a/multidict/_multidict.c b/multidict/_multidict.c index 7171cccc4..64f0e4788 100644 --- a/multidict/_multidict.c +++ b/multidict/_multidict.c @@ -1,6 +1,7 @@ #include #include +#include "_multilib/capsule.h" #include "_multilib/dict.h" #include "_multilib/hashtable.h" #include "_multilib/istr.h" @@ -10,34 +11,6 @@ #include "_multilib/state.h" #include "_multilib/views.h" -#define MultiDict_CheckExact(state, obj) Py_IS_TYPE(obj, state->MultiDictType) -#define MultiDict_Check(state, obj) \ - (MultiDict_CheckExact(state, obj) || \ - PyObject_TypeCheck(obj, state->MultiDictType)) -#define CIMultiDict_CheckExact(state, obj) \ - Py_IS_TYPE(obj, state->CIMultiDictType) -#define CIMultiDict_Check(state, obj) \ - (CIMultiDict_CheckExact(state, obj) || \ - PyObject_TypeCheck(obj, state->CIMultiDictType)) -#define AnyMultiDict_Check(state, obj) \ - (MultiDict_CheckExact(state, obj) || \ - CIMultiDict_CheckExact(state, obj) || \ - PyObject_TypeCheck(obj, state->MultiDictType)) -#define MultiDictProxy_CheckExact(state, obj) \ - Py_IS_TYPE(obj, state->MultiDictProxyType) -#define MultiDictProxy_Check(state, obj) \ - (MultiDictProxy_CheckExact(state, obj) || \ - PyObject_TypeCheck(obj, state->MultiDictProxyType)) -#define CIMultiDictProxy_CheckExact(state, obj) \ - Py_IS_TYPE(obj, state->CIMultiDictProxyType) -#define CIMultiDictProxy_Check(state, obj) \ - (CIMultiDictProxy_CheckExact(state, obj) || \ - PyObject_TypeCheck(obj, state->CIMultiDictProxyType)) -#define AnyMultiDictProxy_Check(state, obj) \ - (MultiDictProxy_CheckExact(state, obj) || \ - CIMultiDictProxy_CheckExact(state, obj) || \ - PyObject_TypeCheck(obj, state->MultiDictProxyType)) - /******************** Internal Methods ********************/ static inline PyObject * @@ -1520,6 +1493,15 @@ module_exec(PyObject *mod) goto fail; } + PyObject *capsule = new_capsule(state); + if (capsule == NULL) { + goto fail; + } + + if (PyModule_Add(mod, MultiDict_CAPI_NAME, capsule) < 0) { + goto fail; + } + return 0; fail: Py_CLEAR(tpl); diff --git a/multidict/_multilib/capsule.h b/multidict/_multilib/capsule.h new file mode 100644 index 000000000..a5a3f7e1e --- /dev/null +++ b/multidict/_multilib/capsule.h @@ -0,0 +1,95 @@ +#ifndef _MULTIDICT_CAPSULE_H +#define _MULTIDICT_CAPSULE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define MULTIDICT_IMPL + +#include "../multidict_api.h" +#include "dict.h" +#include "hashtable.h" +#include "state.h" + +inline static void +_invalid_type() +{ + PyErr_SetString(PyExc_TypeError, "self should be a MultiDict instance"); +} + +static PyTypeObject * +MultiDict_GetType(void *state_) +{ + mod_state *state = (mod_state *)state_; + return (PyTypeObject *)Py_NewRef(state->MultiDictType); +} + +static PyObject * +MultiDict_New(void *state_, int prealloc_size) +{ + mod_state *state = (mod_state *)state_; + MultiDictObject *md = + PyObject_GC_New(MultiDictObject, state->MultiDictType); + if (md == NULL) { + return NULL; + } + if (md_init(md, state, false, prealloc_size) < 0) { + Py_CLEAR(md); + return NULL; + } + PyObject_GC_Track(md); + return (PyObject *)md; +} + +static int +MultiDict_Add(void *state_, PyObject *self, PyObject *key, PyObject *value) +{ + mod_state *state = (mod_state *)state_; + if (MultiDict_Check(state, self) <= 0) { + _invalid_type(); + return -1; + } + return md_add((MultiDictObject *)self, key, value); +} + +static void +capsule_free(MultiDict_CAPI *capi) +{ + PyMem_Free(capi); +} + +static void +capsule_destructor(PyObject *o) +{ + MultiDict_CAPI *capi = PyCapsule_GetPointer(o, MultiDict_CAPSULE_NAME); + capsule_free(capi); +} + +static PyObject * +new_capsule(mod_state *state) +{ + MultiDict_CAPI *capi = + (MultiDict_CAPI *)PyMem_Malloc(sizeof(MultiDict_CAPI)); + if (capi == NULL) { + PyErr_NoMemory(); + return NULL; + } + capi->state = state; + capi->MultiDict_GetType = MultiDict_GetType; + capi->MultiDict_New = MultiDict_New; + capi->MultiDict_Add = MultiDict_Add; + + PyObject *ret = + PyCapsule_New(capi, MultiDict_CAPSULE_NAME, capsule_destructor); + if (ret == NULL) { + capsule_free(capi); + } + return ret; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/multidict/_multilib/state.h b/multidict/_multilib/state.h index 4e2610b6c..1441c1133 100644 --- a/multidict/_multilib/state.h +++ b/multidict/_multilib/state.h @@ -131,6 +131,34 @@ NEXT_VERSION(mod_state *state) return ++state->global_version; } +#define MultiDict_CheckExact(state, obj) Py_IS_TYPE(obj, state->MultiDictType) +#define MultiDict_Check(state, obj) \ + (MultiDict_CheckExact(state, obj) || \ + PyObject_TypeCheck(obj, state->MultiDictType)) +#define CIMultiDict_CheckExact(state, obj) \ + Py_IS_TYPE(obj, state->CIMultiDictType) +#define CIMultiDict_Check(state, obj) \ + (CIMultiDict_CheckExact(state, obj) || \ + PyObject_TypeCheck(obj, state->CIMultiDictType)) +#define AnyMultiDict_Check(state, obj) \ + (MultiDict_CheckExact(state, obj) || \ + CIMultiDict_CheckExact(state, obj) || \ + PyObject_TypeCheck(obj, state->MultiDictType)) +#define MultiDictProxy_CheckExact(state, obj) \ + Py_IS_TYPE(obj, state->MultiDictProxyType) +#define MultiDictProxy_Check(state, obj) \ + (MultiDictProxy_CheckExact(state, obj) || \ + PyObject_TypeCheck(obj, state->MultiDictProxyType)) +#define CIMultiDictProxy_CheckExact(state, obj) \ + Py_IS_TYPE(obj, state->CIMultiDictProxyType) +#define CIMultiDictProxy_Check(state, obj) \ + (CIMultiDictProxy_CheckExact(state, obj) || \ + PyObject_TypeCheck(obj, state->CIMultiDictProxyType)) +#define AnyMultiDictProxy_Check(state, obj) \ + (MultiDictProxy_CheckExact(state, obj) || \ + CIMultiDictProxy_CheckExact(state, obj) || \ + PyObject_TypeCheck(obj, state->MultiDictProxyType)) + #ifdef __cplusplus } #endif diff --git a/multidict/multidict_api.h b/multidict/multidict_api.h new file mode 100644 index 000000000..91e2beb53 --- /dev/null +++ b/multidict/multidict_api.h @@ -0,0 +1,85 @@ +#ifndef _MULTIDICT_API_H +#define _MULTIDICT_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define MultiDict_MODULE_NAME "multidict._multidict" +#define MultiDict_CAPI_NAME "CAPI" +#define MultiDict_CAPSULE_NAME MultiDict_MODULE_NAME "." MultiDict_CAPI_NAME + +typedef struct { + /* N.B. + + For the sake of backward and future compatibility, + new fields should be added at the end of the structure, + unused fields should be never removed. + + Otherwise, it could lead to crashes with memory corruptions + if the client is compiled with older multidict_api.h header + */ + + void *state; + + PyTypeObject *(*MultiDict_GetType)(void *state); + + PyObject *(*MultiDict_New)(void *state, int prealloc_size); + int (*MultiDict_Add)(void *state, PyObject *self, PyObject *key, + PyObject *value); +} MultiDict_CAPI; + +#ifndef MULTIDICT_IMPL + +static inline MultiDict_CAPI * +MultiDict_Import() +{ + return (MultiDict_CAPI *)PyCapsule_Import(MultiDict_CAPSULE_NAME, 0); +} + +static inline PyTypeObject * +MultiDict_GetType(MultiDict_CAPI *api) +{ + return api->MultiDict_GetType(api->state); +} + +static inline int +MultiDict_CheckExact(MultiDict_CAPI *api, PyObject *op) +{ + PyTypeObject *type = api->MultiDict_GetType(api->state); + int ret = Py_IS_TYPE(op, type); + Py_DECREF(type); + return ret; +} + +static inline int +MultiDict_Check(MultiDict_CAPI *api, PyObject *op) +{ + PyTypeObject *type = api->MultiDict_GetType(api->state); + int ret = Py_IS_TYPE(op, type) || PyObject_TypeCheck(op, type); + Py_DECREF(type); + return ret; +} + +static inline PyObject * +MultiDict_New(MultiDict_CAPI *api, int prealloc_size) +{ + return api->MultiDict_New(api->state, prealloc_size); +} + +static inline int +MultiDict_Add(MultiDict_CAPI *api, PyObject *self, PyObject *key, + PyObject *value) +{ + return api->MultiDict_Add(api->state, self, key, value); +} + +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/pyproject.toml b/pyproject.toml index f4c4d6b36..bd42427e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [tool.cibuildwheel] test-requires = "-r requirements/pytest.txt" -test-command = 'pytest -m "not leaks" --no-cov {project}/tests' +test-command = 'pytest -m "not leaks and not capi" --no-cov {project}/tests' # don't build PyPy wheels, install from source instead skip = "pp*" enable = ["cpython-freethreading"] diff --git a/pytest.ini b/pytest.ini index f5c094b96..b59495bab 100644 --- a/pytest.ini +++ b/pytest.ini @@ -57,6 +57,7 @@ junit_suite_name = multidict_test_suite # A mapping of markers to their descriptions allowed in strict mode: markers = leaks: memory leak tests + capi: C API tests minversion = 3.8.2 diff --git a/testcapi/pyproject.toml b/testcapi/pyproject.toml new file mode 100644 index 000000000..f3791e5b3 --- /dev/null +++ b/testcapi/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 40"] +build-backend = "setuptools.build_meta" diff --git a/testcapi/setup.cfg b/testcapi/setup.cfg new file mode 100644 index 000000000..79faef80e --- /dev/null +++ b/testcapi/setup.cfg @@ -0,0 +1,17 @@ +[bdist_wheel] +# wheels should be OS-specific: +# their names must contain macOS/manulinux1/2010/2014/Windows identifiers +universal = 0 + +[metadata] +name = testcapi +# the version doesn't matter, the helper library is never uploaded to pypi +version = 0.0.0.dev0 +description = multidict capi test utils +author = Andrew Svetlov +author_email = andrew.svetlov@gmail.com +license = Apache 2 +[options] +python_requires = >= 3.9 +packages = + testcapi diff --git a/testcapi/setup.py b/testcapi/setup.py new file mode 100644 index 000000000..b1577a244 --- /dev/null +++ b/testcapi/setup.py @@ -0,0 +1,24 @@ +from setuptools import Extension, setup +import multidict +import os + +NO_EXTENSIONS = bool(os.environ.get("MULTIDICT_NO_EXTENSIONS")) + +extensions = [ + Extension( + "testcapi._api", + ["testcapi/_api.c"], + include_dirs=multidict.__path__, + ), +] + +if not NO_EXTENSIONS: + print("*********************") + print("* Accelerated build *") + print("*********************") + setup(ext_modules=extensions) +else: + print("*********************") + print("* Pure Python build *") + print("*********************") + setup() diff --git a/testcapi/testcapi/__init__.py b/testcapi/testcapi/__init__.py new file mode 100644 index 000000000..45dc165b4 --- /dev/null +++ b/testcapi/testcapi/__init__.py @@ -0,0 +1,4 @@ +try: + from testcapi._api import * # noqa +except ImportError: + pass diff --git a/testcapi/testcapi/_api.c b/testcapi/testcapi/_api.c new file mode 100644 index 000000000..bb1a0220f --- /dev/null +++ b/testcapi/testcapi/_api.c @@ -0,0 +1,111 @@ +#include +#include + +typedef struct { + MultiDict_CAPI *capi; +} mod_state; + +static inline mod_state * +get_mod_state(PyObject *mod) +{ + mod_state *state = (mod_state *)PyModule_GetState(mod); + assert(state != NULL); + return state; +} + +/* module functions */ + +static PyObject * +md_type(PyObject *self, PyObject *unused) +{ + mod_state *state = get_mod_state(self); + return Py_NewRef(MultiDict_GetType(state->capi)); +} + +static PyObject * +md_new(PyObject *self, PyObject *arg) +{ + mod_state *state = get_mod_state(self); + return MultiDict_New(state->capi, 0); +} + +static PyObject * +md_add(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (nargs != 3) { + PyErr_SetString(PyExc_TypeError, + "md_add should be called with md, key and value"); + return NULL; + } + mod_state *state = get_mod_state(self); + if (MultiDict_Add(state->capi, args[0], args[1], args[2]) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + +/* module slots */ + +static int +module_traverse(PyObject *mod, visitproc visit, void *arg) +{ + return 0; +} + +static int +module_clear(PyObject *mod) +{ + return 0; +} + +static void +module_free(void *mod) +{ + (void)module_clear((PyObject *)mod); +} + +static PyMethodDef module_methods[] = { + {"md_type", (PyCFunction)md_type, METH_NOARGS}, + {"md_new", (PyCFunction)md_new, METH_O}, + {"md_add", (PyCFunction)md_add, METH_FASTCALL}, + {NULL, NULL} /* sentinel */ +}; + +static int +module_exec(PyObject *mod) +{ + mod_state *state = get_mod_state(mod); + state->capi = MultiDict_Import(); + if (state->capi == NULL) { + return -1; + } + return 0; +} + +static struct PyModuleDef_Slot module_slots[] = { + {Py_mod_exec, module_exec}, +#if PY_VERSION_HEX >= 0x030c00f0 + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, +#endif +#if PY_VERSION_HEX >= 0x030d00f0 + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL}, +}; + +static PyModuleDef api_module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "_api", + .m_size = sizeof(mod_state), + .m_methods = module_methods, + .m_slots = module_slots, + .m_traverse = module_traverse, + .m_clear = module_clear, + .m_free = (freefunc)module_free, +}; + +PyMODINIT_FUNC +PyInit__api(void) +{ + return PyModuleDef_Init(&api_module); +} diff --git a/tests/test_capi.py b/tests/test_capi.py new file mode 100644 index 000000000..72939b1ed --- /dev/null +++ b/tests/test_capi.py @@ -0,0 +1,26 @@ +import multidict +import pytest +import testcapi + +pytest.importorskip("multidict._multidict") + +pytestmark = pytest.mark.capi + +MultiDictStr = multidict.MultiDict[str] + + +def test_md_new() -> None: + md = testcapi.md_new(0) + assert isinstance(md, multidict.MultiDict) + assert len(md) == 0 + + +def test_md_type() -> None: + assert testcapi.md_type() is multidict.MultiDict + + +def test_md_add() -> None: + md: MultiDictStr = multidict.MultiDict() + testcapi.md_add(md, "key", "value") + assert len(md) == 1 + assert list(md.items()) == [("key", "value")] From a1bfcdbc9992b5063d679cfa6a4f2a8477c38cd3 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 20 Jun 2025 00:07:16 +0200 Subject: [PATCH 2/6] Fix wheels builder --- .mypy.ini | 3 --- tests/test_capi.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.mypy.ini b/.mypy.ini index 30e2fc91f..bfac03128 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -29,6 +29,3 @@ warn_unused_ignores = True [mypy-test_incorrect_args] disable_error_code = arg-type, call-overload - -[mypy-test_capi] -disable_error_code = attr-defined diff --git a/tests/test_capi.py b/tests/test_capi.py index 72939b1ed..c9e80ff79 100644 --- a/tests/test_capi.py +++ b/tests/test_capi.py @@ -1,8 +1,8 @@ import multidict import pytest -import testcapi pytest.importorskip("multidict._multidict") +testcapi = pytest.importorskip("testcapi") pytestmark = pytest.mark.capi From b4a292594c60f0de56ef37aa9d7f1a1108d595bf Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 22 Jun 2025 18:10:34 +0200 Subject: [PATCH 3/6] Add CHANGELOG --- CHANGES/1183.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 CHANGES/1183.feature.rst diff --git a/CHANGES/1183.feature.rst b/CHANGES/1183.feature.rst new file mode 100644 index 000000000..65d4535f9 --- /dev/null +++ b/CHANGES/1183.feature.rst @@ -0,0 +1 @@ +Added public C API for the library -- by :user:`Vizonex` and :user:`asvetlov`. From ea64d9c036c75ea43b0633a0f87ed2dce3d63fe4 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 22 Jun 2025 18:32:58 +0200 Subject: [PATCH 4/6] Implement MultiDict_Clear() --- multidict/_multilib/capsule.h | 12 ++++++++ multidict/multidict_api.h | 7 +++++ testcapi/testcapi/_api.c | 58 +++++++++++++++++++++++++++++------ tests/test_capi.py | 6 ++++ 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/multidict/_multilib/capsule.h b/multidict/_multilib/capsule.h index a5a3f7e1e..91cbc9636 100644 --- a/multidict/_multilib/capsule.h +++ b/multidict/_multilib/capsule.h @@ -53,6 +53,17 @@ MultiDict_Add(void *state_, PyObject *self, PyObject *key, PyObject *value) return md_add((MultiDictObject *)self, key, value); } +static int +MultiDict_Clear(void *state_, PyObject *self) +{ + mod_state *state = (mod_state *)state_; + if (MultiDict_Check(state, self) <= 0) { + _invalid_type(); + return -1; + } + return md_clear((MultiDictObject *)self); +} + static void capsule_free(MultiDict_CAPI *capi) { @@ -79,6 +90,7 @@ new_capsule(mod_state *state) capi->MultiDict_GetType = MultiDict_GetType; capi->MultiDict_New = MultiDict_New; capi->MultiDict_Add = MultiDict_Add; + capi->MultiDict_Clear = MultiDict_Clear; PyObject *ret = PyCapsule_New(capi, MultiDict_CAPSULE_NAME, capsule_destructor); diff --git a/multidict/multidict_api.h b/multidict/multidict_api.h index 91e2beb53..a086e626b 100644 --- a/multidict/multidict_api.h +++ b/multidict/multidict_api.h @@ -29,6 +29,7 @@ typedef struct { PyObject *(*MultiDict_New)(void *state, int prealloc_size); int (*MultiDict_Add)(void *state, PyObject *self, PyObject *key, PyObject *value); + int (*MultiDict_Clear)(void *state, PyObject *self); } MultiDict_CAPI; #ifndef MULTIDICT_IMPL @@ -76,6 +77,12 @@ MultiDict_Add(MultiDict_CAPI *api, PyObject *self, PyObject *key, return api->MultiDict_Add(api->state, self, key, value); } +static inline int +MultiDict_Clear(MultiDict_CAPI *api, PyObject *self) +{ + return api->MultiDict_Clear(api->state, self); +} + #endif #ifdef __cplusplus diff --git a/testcapi/testcapi/_api.c b/testcapi/testcapi/_api.c index bb1a0220f..c7b1e37ea 100644 --- a/testcapi/testcapi/_api.c +++ b/testcapi/testcapi/_api.c @@ -13,37 +13,76 @@ get_mod_state(PyObject *mod) return state; } +static int +check_nargs(const char *name, Py_ssize_t nargs, Py_ssize_t required) +{ + if (nargs != required) { + PyErr_Format(PyExc_TypeError, + "%s should be called with %d arguments, got %d", + name, + required, + nargs); + return -1; + } + return 0; +} + /* module functions */ static PyObject * -md_type(PyObject *self, PyObject *unused) +md_type(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { mod_state *state = get_mod_state(self); + if (check_nargs("md_type", nargs, 0) < 0) { + return NULL; + } return Py_NewRef(MultiDict_GetType(state->capi)); } static PyObject * -md_new(PyObject *self, PyObject *arg) +md_new(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { mod_state *state = get_mod_state(self); - return MultiDict_New(state->capi, 0); + if (check_nargs("md_new", nargs, 1) < 0) { + return NULL; + } + long prealloc_size = PyLong_AsLong(args[0]); + if (prealloc_size < 0) { + if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_ValueError, + "Negative prealloc_size is not allowed"); + } + return NULL; + } + return MultiDict_New(state->capi, prealloc_size); } static PyObject * md_add(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { - if (nargs != 3) { - PyErr_SetString(PyExc_TypeError, - "md_add should be called with md, key and value"); + mod_state *state = get_mod_state(self); + if (check_nargs("md_add", nargs, 3) < 0) { return NULL; } - mod_state *state = get_mod_state(self); if (MultiDict_Add(state->capi, args[0], args[1], args[2]) < 0) { return NULL; } Py_RETURN_NONE; } +static PyObject * +md_clear(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + mod_state *state = get_mod_state(self); + if (check_nargs("md_clear", nargs, 1) < 0) { + return NULL; + } + if (MultiDict_Clear(state->capi, args[0]) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + /* module slots */ static int @@ -65,9 +104,10 @@ module_free(void *mod) } static PyMethodDef module_methods[] = { - {"md_type", (PyCFunction)md_type, METH_NOARGS}, - {"md_new", (PyCFunction)md_new, METH_O}, + {"md_type", (PyCFunction)md_type, METH_FASTCALL}, + {"md_new", (PyCFunction)md_new, METH_FASTCALL}, {"md_add", (PyCFunction)md_add, METH_FASTCALL}, + {"md_clear", (PyCFunction)md_clear, METH_FASTCALL}, {NULL, NULL} /* sentinel */ }; diff --git a/tests/test_capi.py b/tests/test_capi.py index c9e80ff79..55e3bf04d 100644 --- a/tests/test_capi.py +++ b/tests/test_capi.py @@ -24,3 +24,9 @@ def test_md_add() -> None: testcapi.md_add(md, "key", "value") assert len(md) == 1 assert list(md.items()) == [("key", "value")] + + +def test_md_clear() -> None: + md: MultiDictStr = multidict.MultiDict(key="val") + testcapi.md_clear(md) + assert len(md) == 0 From 2b653ea7de88070b5a1f89f3e45828b36d5275c7 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 22 Jun 2025 19:23:02 +0200 Subject: [PATCH 5/6] Implement MultiDict_SetDefault() --- multidict/_multilib/capsule.h | 13 +++++++++++++ multidict/multidict_api.h | 9 +++++++++ setup.py | 1 - testcapi/setup.py | 1 + testcapi/testcapi/_api.c | 24 ++++++++++++++++++++++++ tests/test_capi.py | 13 +++++++++++++ 6 files changed, 60 insertions(+), 1 deletion(-) diff --git a/multidict/_multilib/capsule.h b/multidict/_multilib/capsule.h index 91cbc9636..f127799f3 100644 --- a/multidict/_multilib/capsule.h +++ b/multidict/_multilib/capsule.h @@ -64,6 +64,18 @@ MultiDict_Clear(void *state_, PyObject *self) return md_clear((MultiDictObject *)self); } +static inline int +MultiDict_SetDefault(void *state_, PyObject *self, PyObject *key, + PyObject *default_, PyObject **result) +{ + mod_state *state = (mod_state *)state_; + if (MultiDict_Check(state, self) <= 0) { + _invalid_type(); + return -1; + } + return md_set_default((MultiDictObject *)self, key, default_, result); +} + static void capsule_free(MultiDict_CAPI *capi) { @@ -91,6 +103,7 @@ new_capsule(mod_state *state) capi->MultiDict_New = MultiDict_New; capi->MultiDict_Add = MultiDict_Add; capi->MultiDict_Clear = MultiDict_Clear; + capi->MultiDict_SetDefault = MultiDict_SetDefault; PyObject *ret = PyCapsule_New(capi, MultiDict_CAPSULE_NAME, capsule_destructor); diff --git a/multidict/multidict_api.h b/multidict/multidict_api.h index a086e626b..36ec33c71 100644 --- a/multidict/multidict_api.h +++ b/multidict/multidict_api.h @@ -30,6 +30,8 @@ typedef struct { int (*MultiDict_Add)(void *state, PyObject *self, PyObject *key, PyObject *value); int (*MultiDict_Clear)(void *state, PyObject *self); + int (*MultiDict_SetDefault)(void *state, PyObject *self, PyObject *key, + PyObject *default_, PyObject **result); } MultiDict_CAPI; #ifndef MULTIDICT_IMPL @@ -83,6 +85,13 @@ MultiDict_Clear(MultiDict_CAPI *api, PyObject *self) return api->MultiDict_Clear(api->state, self); } +static inline int +MultiDict_SetDefault(MultiDict_CAPI *api, PyObject *self, PyObject *key, + PyObject *default_, PyObject **result) +{ + return api->MultiDict_SetDefault(api->state, self, key, default_, result); +} + #endif #ifdef __cplusplus diff --git a/setup.py b/setup.py index 33dd98afd..5de4ccce3 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,6 @@ NO_EXTENSIONS = True CFLAGS = ["-O0", "-g3", "-UNDEBUG"] if DEBUG_BUILD else ["-O3"] - if platform.system() != "Windows": CFLAGS.extend( [ diff --git a/testcapi/setup.py b/testcapi/setup.py index b1577a244..58a14e111 100644 --- a/testcapi/setup.py +++ b/testcapi/setup.py @@ -9,6 +9,7 @@ "testcapi._api", ["testcapi/_api.c"], include_dirs=multidict.__path__, + extra_compile_args=["-O0", "-g3", "-UNDEBUG"], ), ] diff --git a/testcapi/testcapi/_api.c b/testcapi/testcapi/_api.c index c7b1e37ea..da8465e64 100644 --- a/testcapi/testcapi/_api.c +++ b/testcapi/testcapi/_api.c @@ -83,6 +83,29 @@ md_clear(PyObject *self, PyObject *const *args, Py_ssize_t nargs) Py_RETURN_NONE; } +static PyObject * +md_setdefault(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + printf("000000\n"); + mod_state *state = get_mod_state(self); + if (check_nargs("md_setdefault", nargs, 3) < 0) { + return NULL; + } + PyObject *result = NULL; + int ret = + MultiDict_SetDefault(state->capi, args[0], args[1], args[2], &result); + if (ret < 0) { + return NULL; + } + assert(result != NULL); + PyObject *val = PyBool_FromLong(ret); + if (val == NULL) { + Py_CLEAR(result); + return NULL; + } + return PyTuple_Pack(2, result, val); +} + /* module slots */ static int @@ -108,6 +131,7 @@ static PyMethodDef module_methods[] = { {"md_new", (PyCFunction)md_new, METH_FASTCALL}, {"md_add", (PyCFunction)md_add, METH_FASTCALL}, {"md_clear", (PyCFunction)md_clear, METH_FASTCALL}, + {"md_setdefault", (PyCFunction)md_setdefault, METH_FASTCALL}, {NULL, NULL} /* sentinel */ }; diff --git a/tests/test_capi.py b/tests/test_capi.py index 55e3bf04d..d3c863fa8 100644 --- a/tests/test_capi.py +++ b/tests/test_capi.py @@ -30,3 +30,16 @@ def test_md_clear() -> None: md: MultiDictStr = multidict.MultiDict(key="val") testcapi.md_clear(md) assert len(md) == 0 + + +@pytest.mark.parametrize( + "key, expected", + [ + pytest.param("key", ("val", True), id="found"), + pytest.param("key2", ("default", False), id="notfound"), + ], +) +def test_md_setdefault(key: str, expected: tuple[str, bool]) -> None: + md: MultiDictStr = multidict.MultiDict(key="val") + ret = testcapi.md_setdefault(md, key, "default") + assert ret == expected From c48cffd6fe01fec3f245f316ec07da89809f602a Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 27 Jun 2025 20:45:30 +0200 Subject: [PATCH 6/6] Cherry-pick from #1190 --- .../{1183.feature.rst => 1170.feature.rst} | 0 docs/multidict.rst | 225 ++++++++++++++ multidict/_multilib/capsule.h | 211 ++++++++++--- multidict/_multilib/hashtable.h | 7 +- multidict/multidict_api.h | 289 ++++++++++++++++-- testcapi/testcapi/_api.c | 228 ++++++++++++-- tests/test_capi.py | 199 ++++++++++-- 7 files changed, 1032 insertions(+), 127 deletions(-) rename CHANGES/{1183.feature.rst => 1170.feature.rst} (100%) diff --git a/CHANGES/1183.feature.rst b/CHANGES/1170.feature.rst similarity index 100% rename from CHANGES/1183.feature.rst rename to CHANGES/1170.feature.rst diff --git a/docs/multidict.rst b/docs/multidict.rst index 1d2bffcda..8326dafa4 100644 --- a/docs/multidict.rst +++ b/docs/multidict.rst @@ -482,3 +482,228 @@ Environment variables The debug build is not intended for production use and should only be used for development and debugging purposes. + +C-API +===== + +The library is also shipped with a C-API, the header files can be compiled using +**multidict.__path__[0]**. + +.. versionadded:: 6.7 + +.. c:enum:: UpdateOp + + Specifies the operation kind for :c:func:`MultiDict_UpdateFromMultiDict`, + :c:func:`MultiDict_UpdateFromDict`, and :c:func:`MultiDict_UpdateFromSequence` + calls. + + The followings members are recognized: + + * ``Extend`` = 0, for :class:`~multidict.MultiDict.extend`. + + * ``Update`` = 1, for :class:`~multidict.MultiDict.update`. + + * ``Merge`` = 0, for :class:`~multidict.MultiDict.merge`. + +.. c:type:: uint64_t + + An unsigned long long 64 bit integer that can be found in **stdint.h** + +.. c:struct:: MultiDict_CAPI + + Internal structure to keep C API structures. + + .. note:: + + It is similar to the way `Numpy + `_ and `Datetime + `_ work. multidict utilizes a + `Python Capsule `_ to expose the + CAPI to other low level projects. + + .. c:var:: void* state; + + Carries the module state to help lookup types and other objects. + + +.. c:function:: MultiDict_CAPI* MultiDict_Import() + + Imports Multidict CAPI as a capsule object. + + :returns: A Capsule Containing the Multidict CAPI + :retval NULL: if object fails to import + + +.. c:function:: PyTypeObject* MultiDict_GetType(MultiDict_CAPI* api) + + Obtains the :class:`MultiDict` TypeObject. + + :param api: Python Capsule Pointer + :returns: return A CPython `PyTypeObject` is returned as a pointer + :retval NULL: if an exception was raised + + +.. c:function:: int MultiDict_CheckExact(MultiDict_CAPI* api, PyObject* op) + + Checks if :class:`MultiDict` object type matches exactly. + + :param api: Python Capsule Pointer + :param op: The Object to check + :retval 1: if true + :retval 0: if false + :retval -1: if exception was raised + +.. c:function:: int MultiDict_Add(MultiDict_CAPI* api, PyObject* self, PyObject* key, PyObject* value) + + Adds a new entry to the :class:`MultiDict` object. + + :param api: Python Capsule Pointer + :param self: the Multidict object + :param key: The key of the entry to add + :param value: The value of the entry to add + :retval 0: on success + :reval -1: on failure + +.. c:function:: int MultiDict_Clear(MultiDict_CAPI* api, PyObject* self) + + Clears a :class:`MultiDict` object and removes all it's entries. + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :retval 0: if success + :retval -1: on failure and will raise :exc:`TypeError` if :class:`MultiDict` Type is incorrect + + +.. c:function:: PyObject* Multidict_SetDefault(MultiDict_CAPI* api, PyObject* self, PyObject* key, PyObject* _default) + + If key is in the dictionary its the first value. + If not, insert key with a value of default and return default. + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param key: the key to insert + :param _default: the default value to have inserted + :returns: the default object on success + :retval NULL: on failure + +.. c:function:: int MutliDict_Del(MultiDict_CAPI* api, PyObject* self, PyObject* key) + + Remove all items where key is equal to key from d. + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param key: the key to be removed + :retval 0: on success, + :retval -1: on failure followed by raising either :exc:`TypeError` or :exc:`KeyError` if key is not in the map. + +.. c:function:: uint64_t MultiDict_Version(MultiDict_CAPI* api, PyObject* self) + + Return a version of given :class:`MultiDict` object + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :returns: the version flag of the object, otherwise 0 on failure + +.. c:function:: int MultiDict_Contains(MultiDict_CAPI* api, PyObject* self, PyObject* key) + + Determines if a certain key exists a multidict object + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param key: the key to look for + :retval 1: if true + :retval 0: if false, + :retval -1: if failure had occurred + +.. c:function:: PyObject* MultiDict_GetOne(MultiDict_CAPI* api, PyObject* self, PyObject* key, PyObject** result) + + Return the **first** value for *key* if *key* is in the + dictionary, else *NULL*. + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param key: the key to get one item from + :param result: the object to attached the obtained object to. + :retval 1: on success + :retval 0: on failure (No exceptions are raised) + :retval -1: on :exc:`TypeError` raised + +.. c:function:: PyObject* MultiDict_PopOne(MultiDict_CAPI* api, PyObject* self, PyObject* key) + + Remove and return a value from the dictionary. + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param key: the key to remove + :returns: corresponding value on success or None + :retval NULL: on failure and raises :exc:`TypeError` + +.. c:function:: PyObject* MultiDict_PopAll(MultiDict_CAPI* api, PyObject* self, PyObject* key) + + Pops all related objects corresponding to `key` + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param key: the key to pop all of + :returns: :class:`list` object on success + :retval NULL: on error and raises either :exc:`KeyError` or :exc:`TypeError` + + +.. c:function:: PyObject* MultiDict_PopItem(MultiDict_CAPI* api, PyObject* self) + + Remove and return an arbitrary ``(key, value)`` pair from the + dictionary. + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :returns: an arbitrary tuple on success + :retval NULL: on error along with :exc:`TypeError` or :exc:`KeyError` raised + + +.. c:function:: int MultiDict_Replace(MultiDict_CAPI* api, PyObject* self, PyObject* key, PyObject* value) + + Replaces a set object with another object + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param key: the key to lookup for replacement + :param value: the value to replace with + :retval 0: on success + :retval -1: on failure and raises :exc:`TypeError` + + +.. c:function:: int MultiDict_UpdateFromMultiDict(MultiDict_CAPI* api, PyObject* self, PyObject* other, UpdateOp op) + + Updates Multidict object using another MultiDict Object + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param other: a multidict object to update corresponding object with + :param op: :c:enum:`UpdateOp` operation for extending, updating, or merging values. + :retval 0: on success + :retval -1: on failure + + + +.. c:function:: int MultiDict_UpdateFromDict(MultiDict_CAPI* api, PyObject* self, PyObject* kwds, UpdateOp op) + + Updates Multidict object using another Dictionary Object + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param kwds: the keywords or Dictionary object to merge + :param op: :c:enum:`UpdateOp` operation for extending, updating, or merging values. + :retval 0: on success + :retval -1: on failure + + +.. c:function:: int MultiDict_UpdateFromSequence(MultiDict_CAPI* api, PyObject* self, PyObject* seq, UpdateOp op) + + Updates Multidict object using a sequence object + + :param api: Python Capsule Pointer + :param self: the :class:`MultiDict` object + :param seq: the sequence to merge with. + :param op: :c:enum:`UpdateOp` operation for extending, updating, or merging values. + :retval 0: on success + :retval -1: on failure diff --git a/multidict/_multilib/capsule.h b/multidict/_multilib/capsule.h index f127799f3..16c29e136 100644 --- a/multidict/_multilib/capsule.h +++ b/multidict/_multilib/capsule.h @@ -12,25 +12,31 @@ extern "C" { #include "hashtable.h" #include "state.h" -inline static void -_invalid_type() -{ - PyErr_SetString(PyExc_TypeError, "self should be a MultiDict instance"); -} +// NOTE: MACROS WITH '__' ARE INTERNAL METHODS, +// PLEASE DON'T USE IN OTHER PROJECTS!!! + +#define __MULTIDICT_VALIDATION_CHECK(SELF, STATE, ON_FAIL) \ + if (MultiDict_Check(((mod_state*)STATE), (SELF)) <= 0) { \ + PyErr_Format(PyExc_TypeError, \ + #SELF " should be a MultiDict instance not %s", \ + Py_TYPE(SELF)->tp_name); \ + return ON_FAIL; \ + } -static PyTypeObject * -MultiDict_GetType(void *state_) +static PyTypeObject* +MultiDict_GetType(void* state_) { - mod_state *state = (mod_state *)state_; - return (PyTypeObject *)Py_NewRef(state->MultiDictType); + mod_state* state = (mod_state*)state_; + return (PyTypeObject*)Py_NewRef(state->MultiDictType); } -static PyObject * -MultiDict_New(void *state_, int prealloc_size) +static PyObject* +MultiDict_New(void* state_, int prealloc_size) { - mod_state *state = (mod_state *)state_; - MultiDictObject *md = - PyObject_GC_New(MultiDictObject, state->MultiDictType); + mod_state* state = (mod_state*)state_; + MultiDictObject* md = (MultiDictObject*)state->MultiDictType->tp_alloc( + state->MultiDictType, 0); + if (md == NULL) { return NULL; } @@ -39,61 +45,170 @@ MultiDict_New(void *state_, int prealloc_size) return NULL; } PyObject_GC_Track(md); - return (PyObject *)md; + return (PyObject*)md; } static int -MultiDict_Add(void *state_, PyObject *self, PyObject *key, PyObject *value) +MultiDict_Add(void* state_, PyObject* self, PyObject* key, PyObject* value) { - mod_state *state = (mod_state *)state_; - if (MultiDict_Check(state, self) <= 0) { - _invalid_type(); - return -1; + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + return md_add((MultiDictObject*)self, key, value); +} + +static int +MultiDict_Clear(void* state_, PyObject* self) +{ + // TODO: Macro for repeated steps being done? + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + return md_clear((MultiDictObject*)self); +} + +static int +MultiDict_SetDefault(void* state_, PyObject* self, PyObject* key, + PyObject* value, PyObject** result) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + return md_set_default((MultiDictObject*)self, key, value, result); +} + +static int +MultiDict_Del(void* state_, PyObject* self, PyObject* key) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + return md_del((MultiDictObject*)self, key); +} + +static uint64_t +MultiDict_Version(void* state_, PyObject* self) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, 0); + return md_version((MultiDictObject*)self); +} + +static int +MultiDict_Contains(void* state_, PyObject* self, PyObject* key) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + return md_contains((MultiDictObject*)self, key, NULL); +} + +// Suggestion: Would be smart in to do what python does and provide +// a version of GetOne, GetAll, PopOne & PopAll simillar +// to an unsafe call. The validation check could then be +// replaced with an assertion check such as _PyList_CAST for example +// a concept of this idea can be found for PyList_GetItem -> PyList_GET_ITEM + +static int +MultiDict_GetOne(void* state_, PyObject* self, PyObject* key, + PyObject** result) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + + // TODO: edit md_get_one to return 0 if not found, 1 if found. + // For now the macro made will suffice... + return md_get_one((MultiDictObject*)self, key, result); +} + +static int +MultiDict_GetAll(void* state_, PyObject* self, PyObject* key, + PyObject** result) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + return md_get_all((MultiDictObject*)self, key, result); +} + +static int +MultiDict_PopOne(void* state_, PyObject* self, PyObject* key, + PyObject** result) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + return md_pop_one((MultiDictObject*)self, key, result); +} + +static int +MultiDict_PopAll(void* state_, PyObject* self, PyObject* key, + PyObject** result) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + return md_pop_all((MultiDictObject*)self, key, result); +} + +static PyObject* +MultiDict_PopItem(void* state_, PyObject* self) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, NULL); + return md_pop_item((MultiDictObject*)self); +} + +static int +MultiDict_Replace(void* state_, PyObject* self, PyObject* key, PyObject* value) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + return md_replace((MultiDictObject*)self, key, value); +} + +static int +MultiDict_UpdateFromMultiDict(void* state_, PyObject* self, PyObject* other, + UpdateOp op) +{ + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + __MULTIDICT_VALIDATION_CHECK(other, state_, -1); + int ret = + md_update_from_ht((MultiDictObject*)self, (MultiDictObject*)other, op); + if (op != Extend) { + md_post_update((MultiDictObject*)self); } - return md_add((MultiDictObject *)self, key, value); + return ret; } static int -MultiDict_Clear(void *state_, PyObject *self) +MultiDict_UpdateFromDict(void* state_, PyObject* self, PyObject* other, + UpdateOp op) { - mod_state *state = (mod_state *)state_; - if (MultiDict_Check(state, self) <= 0) { - _invalid_type(); + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + if (PyDict_CheckExact(other) <= 0) { + PyErr_Format(PyExc_TypeError, + "other should be a MultiDict instance not %s", + Py_TYPE(other)->tp_name); return -1; } - return md_clear((MultiDictObject *)self); + int ret = md_update_from_dict((MultiDictObject*)self, other, op); + if (op != Extend) { + md_post_update((MultiDictObject*)self); + } + return ret; } -static inline int -MultiDict_SetDefault(void *state_, PyObject *self, PyObject *key, - PyObject *default_, PyObject **result) +static int +MultiDict_UpdateFromSequence(void* state_, PyObject* self, PyObject* seq, + UpdateOp op) { - mod_state *state = (mod_state *)state_; - if (MultiDict_Check(state, self) <= 0) { - _invalid_type(); - return -1; + __MULTIDICT_VALIDATION_CHECK(self, state_, -1); + int ret = md_update_from_seq((MultiDictObject*)self, seq, op); + if (op != Extend) { + md_post_update((MultiDictObject*)self); } - return md_set_default((MultiDictObject *)self, key, default_, result); + return ret; } static void -capsule_free(MultiDict_CAPI *capi) +capsule_free(MultiDict_CAPI* capi) { PyMem_Free(capi); } static void -capsule_destructor(PyObject *o) +capsule_destructor(PyObject* o) { - MultiDict_CAPI *capi = PyCapsule_GetPointer(o, MultiDict_CAPSULE_NAME); + MultiDict_CAPI* capi = PyCapsule_GetPointer(o, MultiDict_CAPSULE_NAME); capsule_free(capi); } -static PyObject * -new_capsule(mod_state *state) +static PyObject* +new_capsule(mod_state* state) { - MultiDict_CAPI *capi = - (MultiDict_CAPI *)PyMem_Malloc(sizeof(MultiDict_CAPI)); + MultiDict_CAPI* capi = + (MultiDict_CAPI*)PyMem_Malloc(sizeof(MultiDict_CAPI)); if (capi == NULL) { PyErr_NoMemory(); return NULL; @@ -104,8 +219,20 @@ new_capsule(mod_state *state) capi->MultiDict_Add = MultiDict_Add; capi->MultiDict_Clear = MultiDict_Clear; capi->MultiDict_SetDefault = MultiDict_SetDefault; + capi->MultiDict_Del = MultiDict_Del; + capi->MultiDict_Version = MultiDict_Version; + capi->MultiDict_Contains = MultiDict_Contains; + capi->MultiDict_GetOne = MultiDict_GetOne; + capi->MultiDict_GetAll = MultiDict_GetAll; + capi->MultiDict_PopOne = MultiDict_PopOne; + capi->MultiDict_PopAll = MultiDict_PopAll; + capi->MultiDict_PopItem = MultiDict_PopItem; + capi->MultiDict_Replace = MultiDict_Replace; + capi->MultiDict_UpdateFromMultiDict = MultiDict_UpdateFromMultiDict; + capi->MultiDict_UpdateFromDict = MultiDict_UpdateFromDict; + capi->MultiDict_UpdateFromSequence = MultiDict_UpdateFromSequence; - PyObject *ret = + PyObject* ret = PyCapsule_New(capi, MultiDict_CAPSULE_NAME, capsule_destructor); if (ret == NULL) { capsule_free(capi); diff --git a/multidict/_multilib/hashtable.h b/multidict/_multilib/hashtable.h index ce85ec8d7..c10168807 100644 --- a/multidict/_multilib/hashtable.h +++ b/multidict/_multilib/hashtable.h @@ -12,6 +12,7 @@ extern "C" { #include #include +#include "../multidict_api.h" #include "dict.h" #include "htkeys.h" #include "istr.h" @@ -30,12 +31,6 @@ typedef struct _md_finder { PyObject *identity; // borrowed ref } md_finder_t; -typedef enum _UpdateOp { - Extend, - Update, - Merge, -} UpdateOp; - /* The multidict's implementation is close to Python's dict except for multiple keys. diff --git a/multidict/multidict_api.h b/multidict/multidict_api.h index 36ec33c71..88a707b6c 100644 --- a/multidict/multidict_api.h +++ b/multidict/multidict_api.h @@ -6,11 +6,19 @@ extern "C" { #endif #include +#include +#include #define MultiDict_MODULE_NAME "multidict._multidict" #define MultiDict_CAPI_NAME "CAPI" #define MultiDict_CAPSULE_NAME MultiDict_MODULE_NAME "." MultiDict_CAPI_NAME +typedef enum _UpdateOp { + Extend = 0, + Update = 1, + Merge = 2, +} UpdateOp; + typedef struct { /* N.B. @@ -22,76 +30,309 @@ typedef struct { if the client is compiled with older multidict_api.h header */ - void *state; + void* state; + + PyTypeObject* (*MultiDict_GetType)(void* state); + + PyObject* (*MultiDict_New)(void* state, int prealloc_size); + int (*MultiDict_Add)(void* state, PyObject* self, PyObject* key, + PyObject* value); + int (*MultiDict_Clear)(void* state, PyObject* self); + + int (*MultiDict_SetDefault)(void* state, PyObject* self, PyObject* key, + PyObject* default_, PyObject** result); - PyTypeObject *(*MultiDict_GetType)(void *state); + int (*MultiDict_Del)(void* state, PyObject* self, PyObject* key); + uint64_t (*MultiDict_Version)(void* state, PyObject* self); + + int (*MultiDict_Contains)(void* state, PyObject* self, PyObject* key); + int (*MultiDict_GetOne)(void* state_, PyObject* self, PyObject* key, + PyObject** result); + int (*MultiDict_GetAll)(void* state_, PyObject* self, PyObject* key, + PyObject** result); + int (*MultiDict_PopOne)(void* state_, PyObject* self, PyObject* key, + PyObject** result); + int (*MultiDict_PopAll)(void* state_, PyObject* self, PyObject* key, + PyObject** result); + PyObject* (*MultiDict_PopItem)(void* state, PyObject* self); + int (*MultiDict_Replace)(void* state, PyObject* self, PyObject* key, + PyObject* value); + int (*MultiDict_UpdateFromMultiDict)(void* state, PyObject* self, + PyObject* other, UpdateOp op); + int (*MultiDict_UpdateFromDict)(void* state, PyObject* self, + PyObject* kwds, UpdateOp op); + int (*MultiDict_UpdateFromSequence)(void* state, PyObject* self, + PyObject* kwds, UpdateOp op); - PyObject *(*MultiDict_New)(void *state, int prealloc_size); - int (*MultiDict_Add)(void *state, PyObject *self, PyObject *key, - PyObject *value); - int (*MultiDict_Clear)(void *state, PyObject *self); - int (*MultiDict_SetDefault)(void *state, PyObject *self, PyObject *key, - PyObject *default_, PyObject **result); } MultiDict_CAPI; #ifndef MULTIDICT_IMPL -static inline MultiDict_CAPI * +/// @brief Imports Multidict CAPI +/// @return A Capsule Containing the Multidict CAPI Otherwise NULL +static inline MultiDict_CAPI* MultiDict_Import() { - return (MultiDict_CAPI *)PyCapsule_Import(MultiDict_CAPSULE_NAME, 0); + return (MultiDict_CAPI*)PyCapsule_Import(MultiDict_CAPSULE_NAME, 0); } -static inline PyTypeObject * -MultiDict_GetType(MultiDict_CAPI *api) +/// @brief Obtains the Multidict TypeObject +/// @param api Python Capsule Pointer to the API +/// @return A CPython `PyTypeObject` is returned as a pointer, +/// `NULL` on failure +static inline PyTypeObject* +MultiDict_GetType(MultiDict_CAPI* api) { return api->MultiDict_GetType(api->state); } - +/// @brief Checks if Multidict Object Type Matches Exactly +/// @param api Python Capsule Pointer to the API +/// @param op The Object to check +/// @return 1 if `true`, 0 if `false` static inline int -MultiDict_CheckExact(MultiDict_CAPI *api, PyObject *op) +MultiDict_CheckExact(MultiDict_CAPI* api, PyObject* op) { - PyTypeObject *type = api->MultiDict_GetType(api->state); + PyTypeObject* type = api->MultiDict_GetType(api->state); int ret = Py_IS_TYPE(op, type); Py_DECREF(type); return ret; } +/// @brief Checks if Multidict Object Type Matches or is a subclass of itself +/// @param api Python Capsule Pointer to the API +/// @param op The Object to check +/// @return 1 if `true`, 0 if `false` static inline int -MultiDict_Check(MultiDict_CAPI *api, PyObject *op) +MultiDict_Check(MultiDict_CAPI* api, PyObject* op) { - PyTypeObject *type = api->MultiDict_GetType(api->state); + PyTypeObject* type = api->MultiDict_GetType(api->state); int ret = Py_IS_TYPE(op, type) || PyObject_TypeCheck(op, type); Py_DECREF(type); return ret; } -static inline PyObject * -MultiDict_New(MultiDict_CAPI *api, int prealloc_size) +/// @brief Creates a New Multidict Type Object with a number entries wanted +/// preallocated +/// @param api Python Capsule Pointer to the API +/// @param prealloc_size The Number of entires to preallocate for +/// @return `MultiDict` object if successful, otherwise `NULL` +static inline PyObject* +MultiDict_New(MultiDict_CAPI* api, int prealloc_size) { return api->MultiDict_New(api->state, prealloc_size); } +/// @brief Adds a new entry to the `multidict` object +/// @param api Python Capsule Pointer to the API +/// @param self the Multidict object +/// @param key The key of the entry to add +/// @param value The value of the entry to add +/// @return 0 on success, -1 on failure static inline int -MultiDict_Add(MultiDict_CAPI *api, PyObject *self, PyObject *key, - PyObject *value) +MultiDict_Add(MultiDict_CAPI* api, PyObject* self, PyObject* key, + PyObject* value) { return api->MultiDict_Add(api->state, self, key, value); } +/// @brief Clears a multidict object and removes all it's entries +/// @param api Python Capsule Pointer to the API +/// @param self the multidict object +/// @return 0 if success otherwise -1 , will raise TypeError if MultiDict's +/// Type is incorrect static inline int -MultiDict_Clear(MultiDict_CAPI *api, PyObject *self) +MultiDict_Clear(MultiDict_CAPI* api, PyObject* self) { return api->MultiDict_Clear(api->state, self); } +/// XXX: Documentation is incorrect I will need to edit in a bit - Vizonex +/// @brief If key is in the dictionary its the first value. +/// If not, insert key with a value of default and return default. +/// @param api Python Capsule Pointer +/// @param self the MultiDict object +/// @param key the key to insert +/// @param _default the default value to have inserted +/// @return default on success, NULL on failure static inline int -MultiDict_SetDefault(MultiDict_CAPI *api, PyObject *self, PyObject *key, - PyObject *default_, PyObject **result) +MultiDict_SetDefault(MultiDict_CAPI* api, PyObject* self, PyObject* key, + PyObject* default_, PyObject** result) { return api->MultiDict_SetDefault(api->state, self, key, default_, result); } +/// @brief Remove all items where key is equal to key from d. +/// @param api Python Capsule Pointer +/// @param self the MultiDict +/// @param key the key to be removed +/// @return 0 on success, -1 on failure followed by rasing either +/// `TypeError` or `KeyError` if key is not in the map. +static inline int +MutliDict_Del(MultiDict_CAPI* api, PyObject* self, PyObject* key) +{ + return api->MultiDict_Del(api->state, self, key); +} + +/// @brief Return a version of given mdict object +/// @param api Python Capsule Pointer +/// @param self the mdict object +/// @return the version flag of the object, otherwise 0 on failure +static uint64_t +MultiDict_Version(MultiDict_CAPI* api, PyObject* self) +{ + return api->MultiDict_Version(api->state, self); +} + +// Under debate as concept + +// /// @brief Creates a new positional marker for a multidict to iterate +// /// with when being utlizied with `MultiDict_Next` +// /// @param api Python Capsule Pointer +// /// @param self the multidict to create a positional marker for +// /// @param pos the positional marker to be created +// /// @return 0 on success, -1 on failure along with `TypeError` exception +// being thrown static inline int MultiDict_CreatePosMarker(MultiDict_CAPI* +// api, PyObject* self, md_pos_t* pos){ +// return api->MultiDict_CreatePosMarker(api->state, self, pos); +// } +// static inline int MultiDict_Next(MultiDict_CAPI* api, PyObject* self, +// md_pos_t* pos, PyObject** identity, PyObject**key, PyObject **value){ +// return api->MultiDict_Next(api->state, self, pos, identity, key, value); +// }; + +/// @brief Determines if a certain key exists a multidict object +/// @param api Python Capsule Pointer +/// @param self the multidict object +/// @param key the key to look for +/// @return 1 if true, 0 if false, -1 if failure had occured +static inline int +MultiDict_Contains(MultiDict_CAPI* api, PyObject* self, PyObject* key) +{ + return api->MultiDict_Contains(api->state, self, key); +} + +/// @brief Return the **first** value for *key* if *key* is in the +/// dictionary, else *default*. +/// @param api Python Capsule Pointer +/// @param self the multidict object +/// @param key the key to get one item from +/// @return returns a default value on success, -1 with `KeyError` or +/// `TypeError` on failure +static inline int +MultiDict_GetOne(MultiDict_CAPI* api, PyObject* self, PyObject* key, + PyObject** result) +{ + return api->MultiDict_GetOne(api->state, self, key, result); +} + +/// @brief Return a list of all values for *key* if *key* is in the +/// dictionary, else *default*. +/// @param api Python Capsule Pointer +/// @param self the multidict obeject +/// @param key the key to obtain all the items from +/// @return a list of all the values, otherwise NULL on error +/// raises either `KeyError` or `TypeError` +static inline int +MultiDict_GetAll(MultiDict_CAPI* api, PyObject* self, PyObject* key, + PyObject** result) +{ + return api->MultiDict_GetAll(api->state, self, key, result); +} + +/// @brief If `key` is in the dictionary, remove it and return its the +/// `first` value, else return `default`. +/// @param api Python Capsule Pointer +/// @param self the multidict object +/// @param key the key to pop +/// @return object on success, otherwise NULL on error along +/// with `KeyError` or `TypeError` being raised +static inline int +MultiDict_PopOne(MultiDict_CAPI* api, PyObject* self, PyObject* key, + PyObject** result) +{ + return api->MultiDict_PopOne(api->state, self, key, result); +} + +/// @brief Pops all related objects corresponding to `key` +/// @param api Python Capsule Pointer +/// @param self the multidict object +/// @param key the key to pop all of +/// @return list object on success, otherwise NULL, on error and raises either +/// `KeyError` or `TyperError` +static inline int +MultiDict_PopAll(MultiDict_CAPI* api, PyObject* self, PyObject* key, + PyObject** result) +{ + return api->MultiDict_PopAll(api->state, self, key, result); +} + +/// @brief Remove and return an arbitrary `(key, value)` pair from the +/// dictionary. +/// @param api Python Capsule Pointer +/// @param self the multidict object +/// @return an arbitray tuple on success, otherwise NULL on error along +/// with `TypeError` or `KeyError` raised +static inline PyObject* +MultiDict_PopItem(MultiDict_CAPI* api, PyObject* self) +{ + return api->MultiDict_PopItem(api->state, self); +} + +/// @brief Replaces a set object with another object +/// @param api Python Capsule Pointer +/// @param self the multidict object +/// @param key the key to lookup for replacement +/// @param value the value to replace with +/// @return 0 on sucess, -1 on Failure and raises TypeError +static inline int +MultiDict_Replace(MultiDict_CAPI* api, PyObject* self, PyObject* key, + PyObject* value) +{ + return api->MultiDict_Replace(api->state, self, key, value); +}; + +/// @brief Updates Multidict object using another MultiDict Object +/// @param api Python Capsule Pointer +/// @param self the multidict object +/// @param other a multidict object to update corresponding object with +/// @param update if true append references and stack them, otherwise steal all +/// references. +/// @return 0 on sucess, -1 on failure +static inline int +MultiDict_UpdateFromMultiDict(MultiDict_CAPI* api, PyObject* self, + PyObject* other, UpdateOp op) +{ + return api->MultiDict_UpdateFromMultiDict(api->state, self, other, op); +}; + +/// @brief Updates Multidict object using another Dictionary Object +/// @param api Python Capsule Pointer +/// @param self the multidict object +/// @param kwds the keywords or Dictionary object to merge +/// @param update if true append references and stack them, otherwise steal all +/// references. +/// @return 0 on sucess, -1 on failure +static inline int +MultiDict_UpdateFromDict(MultiDict_CAPI* api, PyObject* self, PyObject* other, + UpdateOp op) +{ + return api->MultiDict_UpdateFromDict(api->state, self, other, op); +}; + +/// @brief Updates Multidict object using a sequence object +/// @param api Python Capsule Pointer +/// @param self the multidict object +/// @param seq the sequence to merge with. +/// @param update if true append references and stack them, otherwise steal all +/// references. +/// @return 0 on sucess, -1 on failure +static inline int +MultiDict_UpdateFromSequence(MultiDict_CAPI* api, PyObject* self, + PyObject* seq, UpdateOp op) +{ + return api->MultiDict_UpdateFromSequence(api->state, self, seq, op); +}; + #endif #ifdef __cplusplus diff --git a/testcapi/testcapi/_api.c b/testcapi/testcapi/_api.c index da8465e64..9ff2a3f7c 100644 --- a/testcapi/testcapi/_api.c +++ b/testcapi/testcapi/_api.c @@ -1,5 +1,6 @@ #include #include +#include typedef struct { MultiDict_CAPI *capi; @@ -13,6 +14,12 @@ get_mod_state(PyObject *mod) return state; } +static inline MultiDict_CAPI * +get_capi(PyObject *mod) +{ + return get_mod_state(mod)->capi; +} + static int check_nargs(const char *name, Py_ssize_t nargs, Py_ssize_t required) { @@ -27,26 +34,41 @@ check_nargs(const char *name, Py_ssize_t nargs, Py_ssize_t required) return 0; } -/* module functions */ +// Took the most repetative part and put it right here to help +// you can get rid of this comment before the pr is finished, +// Using a function was less confusing here than a macro - Vizonex static PyObject * -md_type(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +handle_result(int ret, PyObject *result) { - mod_state *state = get_mod_state(self); - if (check_nargs("md_type", nargs, 0) < 0) { + if (ret < 0) { + return NULL; + } + // Test if we missed + if (ret == 0) { + return PyTuple_Pack(2, Py_None, Py_False); + } + assert(result != NULL); + PyObject *val = PyBool_FromLong(ret); + if (val == NULL) { + Py_CLEAR(result); return NULL; } - return Py_NewRef(MultiDict_GetType(state->capi)); + return PyTuple_Pack(2, result, val); +} + +/* module functions */ + +static PyObject * +md_type(PyObject *self, PyObject *unused) +{ + return Py_NewRef(MultiDict_GetType(get_capi(self))); } static PyObject * -md_new(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +md_new(PyObject *self, PyObject *arg) { - mod_state *state = get_mod_state(self); - if (check_nargs("md_new", nargs, 1) < 0) { - return NULL; - } - long prealloc_size = PyLong_AsLong(args[0]); + long prealloc_size = PyLong_AsLong(arg); if (prealloc_size < 0) { if (!PyErr_Occurred()) { PyErr_SetString(PyExc_ValueError, @@ -54,30 +76,25 @@ md_new(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } return NULL; } - return MultiDict_New(state->capi, prealloc_size); + return MultiDict_New(get_capi(self), prealloc_size); } static PyObject * md_add(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { - mod_state *state = get_mod_state(self); if (check_nargs("md_add", nargs, 3) < 0) { return NULL; } - if (MultiDict_Add(state->capi, args[0], args[1], args[2]) < 0) { + if (MultiDict_Add(get_capi(self), args[0], args[1], args[2]) < 0) { return NULL; } Py_RETURN_NONE; } static PyObject * -md_clear(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +md_clear(PyObject *self, PyObject *arg) { - mod_state *state = get_mod_state(self); - if (check_nargs("md_clear", nargs, 1) < 0) { - return NULL; - } - if (MultiDict_Clear(state->capi, args[0]) < 0) { + if (MultiDict_Clear(get_capi(self), arg) < 0) { return NULL; } Py_RETURN_NONE; @@ -86,24 +103,157 @@ md_clear(PyObject *self, PyObject *const *args, Py_ssize_t nargs) static PyObject * md_setdefault(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { - printf("000000\n"); - mod_state *state = get_mod_state(self); if (check_nargs("md_setdefault", nargs, 3) < 0) { return NULL; } PyObject *result = NULL; - int ret = - MultiDict_SetDefault(state->capi, args[0], args[1], args[2], &result); - if (ret < 0) { + int ret = MultiDict_SetDefault( + get_capi(self), args[0], args[1], args[2], &result); + return handle_result(ret, result); +} + +static PyObject * +md_del(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + // handle this check first so that there's an immediate exit + // rather than waiting for the state to be obtained + if (check_nargs("md_del", nargs, 2) < 0) { return NULL; } - assert(result != NULL); - PyObject *val = PyBool_FromLong(ret); - if (val == NULL) { - Py_CLEAR(result); + if ((MutliDict_Del(get_capi(self), args[0], args[1])) < 0) { return NULL; } - return PyTuple_Pack(2, result, val); + Py_RETURN_NONE; +} + +static PyObject * +md_version(PyObject *self, PyObject *arg) +{ + return PyLong_FromUnsignedLongLong(MultiDict_Version(get_capi(self), arg)); +} + +static PyObject * +md_contains(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (check_nargs("md_contains", nargs, 2) < 0) { + return NULL; + } + int ret = MultiDict_Contains(get_capi(self), args[0], args[1]); + if (ret == -1) { + return NULL; + } + return PyBool_FromLong(ret); +} + +static PyObject * +md_getone(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (check_nargs("md_getone", nargs, 2) < 0) { + return NULL; + } + PyObject *result = NULL; + int ret = MultiDict_GetOne(get_capi(self), args[0], args[1], &result); + return handle_result(ret, result); +} + +static PyObject * +md_getall(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (check_nargs("md_getall", nargs, 2) < 0) { + return NULL; + } + PyObject *result = NULL; + int ret = MultiDict_GetAll(get_capi(self), args[0], args[1], &result); + return handle_result(ret, result); +} + +static PyObject * +md_popone(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (check_nargs("md_popone", nargs, 2) < 0) { + return NULL; + } + PyObject *result = NULL; + int ret = MultiDict_PopOne(get_capi(self), args[0], args[1], &result); + return handle_result(ret, result); +} + +static PyObject * +md_popall(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (check_nargs("md_popall", nargs, 2) < 0) { + return NULL; + } + mod_state *state = get_mod_state(self); + PyObject *result = NULL; + int ret = MultiDict_PopAll(get_capi(self), args[0], args[1], &result); + return handle_result(ret, result); +} + +static PyObject * +md_popitem(PyObject *self, PyObject *arg) +{ + mod_state *state = get_mod_state(self); + PyObject *REF = MultiDict_PopItem(get_capi(self), arg); + if (REF != NULL) { + Py_INCREF(REF); + } + return REF; +} + +static PyObject * +md_replace(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (check_nargs("md_replace", nargs, 3) < 0) { + return NULL; + } + + if (MultiDict_Replace(get_capi(self), args[0], args[1], args[2]) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +md_update_from_md(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (check_nargs("md_update_from_md", nargs, 3) < 0) { + return NULL; + } + + if (MultiDict_UpdateFromMultiDict( + get_capi(self), args[0], args[1], PyLong_AsLong(args[2])) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +md_update_from_dict(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (check_nargs("md_update_from_dict", nargs, 3) < 0) { + return NULL; + } + mod_state *state = get_mod_state(self); + + if (MultiDict_UpdateFromDict( + get_capi(self), args[0], args[1], PyLong_AsLong(args[2])) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +md_update_from_seq(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (check_nargs("md_update_from_seq", nargs, 3) < 0) { + return NULL; + } + if (MultiDict_UpdateFromSequence( + get_capi(self), args[0], args[1], PyLong_AsLong(args[2]))) { + return NULL; + }; + Py_RETURN_NONE; } /* module slots */ @@ -127,11 +277,23 @@ module_free(void *mod) } static PyMethodDef module_methods[] = { - {"md_type", (PyCFunction)md_type, METH_FASTCALL}, - {"md_new", (PyCFunction)md_new, METH_FASTCALL}, + {"md_type", (PyCFunction)md_type, METH_NOARGS}, + {"md_new", (PyCFunction)md_new, METH_O}, {"md_add", (PyCFunction)md_add, METH_FASTCALL}, - {"md_clear", (PyCFunction)md_clear, METH_FASTCALL}, + {"md_clear", (PyCFunction)md_clear, METH_O}, {"md_setdefault", (PyCFunction)md_setdefault, METH_FASTCALL}, + {"md_del", (PyCFunction)md_del, METH_FASTCALL}, + {"md_version", (PyCFunction)md_version, METH_O}, + {"md_contains", (PyCFunction)md_contains, METH_FASTCALL}, + {"md_getone", (PyCFunction)md_getone, METH_FASTCALL}, + {"md_getall", (PyCFunction)md_getall, METH_FASTCALL}, + {"md_popone", (PyCFunction)md_popone, METH_FASTCALL}, + {"md_popall", (PyCFunction)md_popall, METH_FASTCALL}, + {"md_popitem", (PyCFunction)md_popitem, METH_O}, + {"md_replace", (PyCFunction)md_replace, METH_FASTCALL}, + {"md_update_from_md", (PyCFunction)md_update_from_md, METH_FASTCALL}, + {"md_update_from_dict", (PyCFunction)md_update_from_dict, METH_FASTCALL}, + {"md_update_from_seq", (PyCFunction)md_update_from_seq, METH_FASTCALL}, {NULL, NULL} /* sentinel */ }; diff --git a/tests/test_capi.py b/tests/test_capi.py index d3c863fa8..22d0df1f2 100644 --- a/tests/test_capi.py +++ b/tests/test_capi.py @@ -9,16 +9,6 @@ MultiDictStr = multidict.MultiDict[str] -def test_md_new() -> None: - md = testcapi.md_new(0) - assert isinstance(md, multidict.MultiDict) - assert len(md) == 0 - - -def test_md_type() -> None: - assert testcapi.md_type() is multidict.MultiDict - - def test_md_add() -> None: md: MultiDictStr = multidict.MultiDict() testcapi.md_add(md, "key", "value") @@ -27,19 +17,184 @@ def test_md_add() -> None: def test_md_clear() -> None: - md: MultiDictStr = multidict.MultiDict(key="val") + previous = multidict.MultiDict([("key", "value")]) + md: MultiDictStr = previous.copy() testcapi.md_clear(md) - assert len(md) == 0 + assert md != previous + + +def test_md_contains() -> None: + d = multidict.MultiDict([("key", "one")]) + assert testcapi.md_contains(d, "key") + testcapi.md_del(d, "key") + assert testcapi.md_contains(d, "key") is False + + +def test_md_del() -> None: + d = multidict.MultiDict([("key", "one"), ("key", "two")], foo="bar") + assert list(d.keys()) == ["key", "key", "foo"] + + testcapi.md_del(d, "key") + assert d == {"foo": "bar"} + assert list(d.items()) == [("foo", "bar")] + + with pytest.raises(KeyError, match="key"): + testcapi.md_del(d, "key") -@pytest.mark.parametrize( - "key, expected", - [ - pytest.param("key", ("val", True), id="found"), - pytest.param("key2", ("default", False), id="notfound"), - ], +def test_md_get_all() -> None: + d: MultiDictStr = multidict.MultiDict() + d.add("key1", "val1") + d.add("key2", "val2") + d.add("key1", "val3") + ret = testcapi.md_getall(d, "key1") + assert (["val1", "val3"], True) == ret + + +def test_md_get_all_miss() -> None: + d = multidict.MultiDict([("key", "value1")], key="value2") + assert testcapi.md_getall(d, "x")[1] is False + + +def test_md_getone() -> None: + d: MultiDictStr = multidict.MultiDict(key="val1") + d.add("key", "val2") + assert testcapi.md_getone(d, "key") == ("val1", True) + + +def test_md_getone_miss() -> None: + d: MultiDictStr = multidict.MultiDict([("key", "value1")], key="value2") + assert testcapi.md_getone(d, "x")[1] is False + + +@pytest.mark.skip( + "GC/WeakRef Releated Bug: SEE: https://github.com/aio-libs/multidict/pull/1190#discussion_r2162536248" ) -def test_md_setdefault(key: str, expected: tuple[str, bool]) -> None: - md: MultiDictStr = multidict.MultiDict(key="val") - ret = testcapi.md_setdefault(md, key, "default") - assert ret == expected +def test_md_new() -> None: + md = testcapi.md_new(0) + assert isinstance(md, multidict.MultiDict) + assert len(md) == 0 + + +def test_md_popall() -> None: + d: MultiDictStr = multidict.MultiDict() + + d.add("key1", "val1") + d.add("key2", "val2") + d.add("key1", "val3") + ret = testcapi.md_popall(d, "key1") + assert (["val1", "val3"], True) == ret + assert {"key2": "val2"} == d + + +def test_md_popall_key_miss() -> None: + d: MultiDictStr = multidict.MultiDict() + assert testcapi.md_popall(d, "x")[1] is False + + +def test_md_popone() -> None: + d: MultiDictStr = multidict.MultiDict() + d.add("key", "val1") + d.add("key2", "val2") + d.add("key", "val3") + + assert ("val1", True) == testcapi.md_popone(d, "key") + assert [("key2", "val2"), ("key", "val3")] == list(d.items()) + + +def test_md_popone_miss() -> None: + d: MultiDictStr = multidict.MultiDict(other="val") + assert testcapi.md_popone(d, "x")[1] is False + + +def test_md_popitem() -> None: + d: MultiDictStr = multidict.MultiDict() + d.add("key", "val1") + d.add("key", "val2") + + assert ("key", "val2") == testcapi.md_popitem(d) + assert len(d) == 1 + assert [("key", "val1")] == list(d.items()) + + +def test_md_replace() -> None: + d: MultiDictStr = multidict.MultiDict() + d.add("key", "val1") + testcapi.md_replace(d, "key", "val2") + assert "val2" == d["key"] + testcapi.md_replace(d, "key", "val3") + assert "val3" == d["key"] + + +def test_md_setdefault() -> None: + md: MultiDictStr = multidict.MultiDict([("key", "one"), ("key", "two")], foo="bar") + assert ("one", True) == testcapi.md_setdefault(md, "key", "three") + assert (None, False) == testcapi.md_setdefault(md, "otherkey", "three") + assert "otherkey" in md + assert "three" == md["otherkey"] + + +def test_md_update_from_md() -> None: + d1: MultiDictStr = multidict.MultiDict({"key": "val1", "k": "v1"}) + d2: MultiDictStr = multidict.MultiDict({"foo": "bar", "k": "v2"}) + + d = d1.copy() + testcapi.md_update_from_md(d, d2, 0) + assert [("key", "val1"), ("k", "v1"), ("foo", "bar"), ("k", "v2")] == list( + d.items() + ) + + d = d1.copy() + testcapi.md_update_from_md(d, d2, 1) + assert [("key", "val1"), ("k", "v2"), ("foo", "bar")] == list(d.items()) + + d = d1.copy() + testcapi.md_update_from_md(d, d2, 2) + assert [("key", "val1"), ("k", "v1"), ("foo", "bar")] == list(d.items()) + + +def test_md_update_from_dict() -> None: + d1: MultiDictStr = multidict.MultiDict({"key": "val1", "k": "v1"}) + d2 = {"foo": "bar", "k": "v2"} + + d = d1.copy() + testcapi.md_update_from_dict(d, d2, 0) + assert [("key", "val1"), ("k", "v1"), ("foo", "bar"), ("k", "v2")] == list( + d.items() + ) + + d = d1.copy() + testcapi.md_update_from_dict(d, d2, 1) + assert [("key", "val1"), ("k", "v2"), ("foo", "bar")] == list(d.items()) + + d = d1.copy() + testcapi.md_update_from_dict(d, d2, 2) + assert [("key", "val1"), ("k", "v1"), ("foo", "bar")] == list(d.items()) + + +def test_md_update_from_seq() -> None: + d1: MultiDictStr = multidict.MultiDict({"key": "val1", "k": "v1"}) + lst = [("foo", "bar"), ("k", "v2")] + + d = d1.copy() + testcapi.md_update_from_seq(d, lst, 0) + assert [("key", "val1"), ("k", "v1"), ("foo", "bar"), ("k", "v2")] == list( + d.items() + ) + + d = d1.copy() + testcapi.md_update_from_seq(d, lst, 1) + assert [("key", "val1"), ("k", "v2"), ("foo", "bar")] == list(d.items()) + + d = d1.copy() + testcapi.md_update_from_seq(d, lst, 2) + assert [("key", "val1"), ("k", "v1"), ("foo", "bar")] == list(d.items()) + + +def test_md_type() -> None: + assert testcapi.md_type() is multidict.MultiDict + + +def test_md_version() -> None: + d = multidict.MultiDict() # type: ignore[var-annotated] + assert testcapi.md_version(d) != 0