Repository: aio-libs/multidict

Issue:
Implement __sizeof__ function to correctly calculate all internal structures size.
See also https://docs.python.org/3/library/sys.html#sys.getsizeof

Comments:

Patch:
diff --git a/CHANGES/444.feature b/CHANGES/444.feature
new file mode 100644
index 0000000..009dc8a
--- /dev/null
+++ b/CHANGES/444.feature
@@ -0,0 +1,1 @@
+Implement ``__sizeof__`` function to correctly calculate all internal structures size.
\ No newline at end of file
diff --git a/multidict/_multidict.c b/multidict/_multidict.c
index 0e8fa5d..0f0bf3d 100644
--- a/multidict/_multidict.c
+++ b/multidict/_multidict.c
@@ -416,6 +416,7 @@ fail:
     return NULL;
 }
 
+
 /******************** Base Methods ********************/
 
 static inline PyObject *
@@ -863,6 +864,21 @@ multidict_class_getitem(PyObject *self, PyObject *arg)
     return self;
 }
 
+
+PyDoc_STRVAR(sizeof__doc__,
+"D.__sizeof__() -> size of D in memory, in bytes");
+
+static inline PyObject *
+_multidict_sizeof(MultiDictObject *self)
+{
+    Py_ssize_t size = sizeof(MultiDictObject);
+    if (self->pairs.pairs != self->pairs.buffer) {
+        size += (Py_ssize_t)sizeof(pair_t) * self->pairs.capacity;
+    }
+    return PyLong_FromSsize_t(size);
+}
+
+
 static PySequenceMethods multidict_sequence = {
     .sq_contains = (objobjproc)multidict_sq_contains,
 };
@@ -978,10 +994,16 @@ static PyMethodDef multidict_methods[] = {
     },
     {
         "__class_getitem__",
-        multidict_class_getitem,
+        (PyCFunction)multidict_class_getitem,
         METH_O | METH_CLASS,
         NULL
     },
+    {
+        "__sizeof__",
+        (PyCFunction)_multidict_sizeof,
+        METH_NOARGS,
+        sizeof__doc__,
+    },
     {
         NULL,
         NULL
@@ -1290,7 +1312,7 @@ static PyMethodDef multidict_proxy_methods[] = {
     },
     {
         "__class_getitem__",
-        multidict_class_getitem,
+        (PyCFunction)multidict_class_getitem,
         METH_O | METH_CLASS,
         NULL
     },
diff --git a/multidict/_multidict_py.py b/multidict/_multidict_py.py
index 8ee1850..d96c3a7 100644
--- a/multidict/_multidict_py.py
+++ b/multidict/_multidict_py.py
@@ -1,3 +1,4 @@
+import sys
 from array import array
 from collections import abc
 
@@ -38,6 +39,10 @@ class _Impl:
         v[0] += 1
         self._version = v[0]
 
+    if sys.implementation.name != "pypy":
+        def __sizeof__(self):
+            return object.__sizeof__(self) + sys.getsizeof(self._items)
+
 
 class _Base:
     def _title(self, key):
@@ -173,6 +178,10 @@ class MultiDict(_Base, MutableMultiMapping):
 
         self._extend(args, kwargs, self.__class__.__name__, self._extend_items)
 
+    if sys.implementation.name != "pypy":
+        def __sizeof__(self):
+            return object.__sizeof__(self) + sys.getsizeof(self._impl)
+
     def __reduce__(self):
         return (self.__class__, (list(self.items()),))
 
diff --git a/setup.py b/setup.py
index 4e07115..79e6b20 100644
--- a/setup.py
+++ b/setup.py
@@ -27,11 +27,7 @@ if platform.system() != "Windows":
 
 extensions = [
     Extension(
-        "multidict._multidict",
-        [
-            "multidict/_multidict.c",
-        ],
-        extra_compile_args=CFLAGS,
+        "multidict._multidict", ["multidict/_multidict.c"], extra_compile_args=CFLAGS,
     ),
 ]
 


Test patch:
diff --git a/tests/test_mutable_multidict.py b/tests/test_mutable_multidict.py
index 57661b7..9b9f118 100644
--- a/tests/test_mutable_multidict.py
+++ b/tests/test_mutable_multidict.py
@@ -1,3 +1,6 @@
+import string
+import sys
+
 import pytest
 
 
@@ -459,3 +462,21 @@ class TestCIMutableMultiDict:
         d2 = cls(KEY="val")
 
         assert d1 == d2
+
+    @pytest.mark.skipif(sys.implementation.name == "pypy",
+                        reason="getsizeof() is not implemented on PyPy")
+    def test_sizeof(self, cls):
+        md = cls()
+        s1 = sys.getsizeof(md)
+        for i in string.ascii_lowercase:
+            for j in string.ascii_uppercase:
+                md[i + j] = i + j
+        # multidict should be resized
+        s2 = sys.getsizeof(md)
+        assert s2 > s1
+
+    @pytest.mark.skipif(sys.implementation.name == "pypy",
+                        reason="getsizeof() is not implemented on PyPy")
+    def test_min_sizeof(self, cls):
+        md = cls()
+        assert sys.getsizeof(md) < 1024