]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-108191: Add support of positional argument in SimpleNamespace constructor (GH...
authorSerhiy Storchaka <storchaka@gmail.com>
Wed, 24 Apr 2024 21:39:54 +0000 (00:39 +0300)
committerGitHub <noreply@github.com>
Wed, 24 Apr 2024 21:39:54 +0000 (00:39 +0300)
SimpleNamespace({'a': 1, 'b': 2}) and SimpleNamespace([('a', 1), ('b', 2)])
are now the same as SimpleNamespace(a=1, b=2).

Doc/library/types.rst
Doc/whatsnew/3.13.rst
Lib/test/test_types.py
Misc/NEWS.d/next/Library/2023-08-21-10-34-43.gh-issue-108191.GZM3mv.rst [new file with mode: 0644]
Objects/namespaceobject.c

index b856544e44207c567647b200cdc6c03b3c7f0df7..89bc0a600c0af8f48252b4318e58ef2204410eab 100644 (file)
@@ -481,14 +481,25 @@ Additional Utility Classes and Functions
    A simple :class:`object` subclass that provides attribute access to its
    namespace, as well as a meaningful repr.
 
-   Unlike :class:`object`, with ``SimpleNamespace`` you can add and remove
-   attributes.  If a ``SimpleNamespace`` object is initialized with keyword
-   arguments, those are directly added to the underlying namespace.
+   Unlike :class:`object`, with :class:`!SimpleNamespace` you can add and remove
+   attributes.
+
+   :py:class:`SimpleNamespace` objects may be initialized
+   in the same way as :class:`dict`: either with keyword arguments,
+   with a single positional argument, or with both.
+   When initialized with keyword arguments,
+   those are directly added to the underlying namespace.
+   Alternatively, when initialized with a positional argument,
+   the underlying namespace will be updated with key-value pairs
+   from that argument (either a mapping object or
+   an :term:`iterable` object producing key-value pairs).
+   All such keys must be strings.
 
    The type is roughly equivalent to the following code::
 
        class SimpleNamespace:
-           def __init__(self, /, **kwargs):
+           def __init__(self, mapping_or_iterable=(), /, **kwargs):
+               self.__dict__.update(mapping_or_iterable)
                self.__dict__.update(kwargs)
 
            def __repr__(self):
@@ -512,6 +523,9 @@ Additional Utility Classes and Functions
       Attribute order in the repr changed from alphabetical to insertion (like
       ``dict``).
 
+   .. versionchanged:: 3.13
+      Added support for an optional positional argument.
+
 .. function:: DynamicClassAttribute(fget=None, fset=None, fdel=None, doc=None)
 
    Route attribute access on a class to __getattr__.
index 89694afdfa3fece7cd45effb4ba9c3695f57dbcc..ad107aad5db3bd7191f5d23669dec22ad294b9a9 100644 (file)
@@ -804,6 +804,14 @@ traceback
   ``True``) to indicate whether ``exc_type`` should be saved.
   (Contributed by Irit Katriel in :gh:`112332`.)
 
+types
+-----
+
+* :class:`~types.SimpleNamespace` constructor now allows specifying initial
+  values of attributes as a positional argument which must be a mapping or
+  an iterable of key-value pairs.
+  (Contributed by Serhiy Storchaka in :gh:`108191`.)
+
 typing
 ------
 
index 16985122bc0219d617900c57ed7d36444c8723f1..fbca198aab5180ff766ffe3fd88292cd20949961 100644 (file)
@@ -2,7 +2,7 @@
 
 from test.support import run_with_locale, cpython_only, MISSING_C_DOCSTRINGS
 import collections.abc
-from collections import namedtuple
+from collections import namedtuple, UserDict
 import copy
 import _datetime
 import gc
@@ -1755,21 +1755,50 @@ class ClassCreationTests(unittest.TestCase):
 class SimpleNamespaceTests(unittest.TestCase):
 
     def test_constructor(self):
-        ns1 = types.SimpleNamespace()
-        ns2 = types.SimpleNamespace(x=1, y=2)
-        ns3 = types.SimpleNamespace(**dict(x=1, y=2))
+        def check(ns, expected):
+            self.assertEqual(len(ns.__dict__), len(expected))
+            self.assertEqual(vars(ns), expected)
+            # check order
+            self.assertEqual(list(vars(ns).items()), list(expected.items()))
+            for name in expected:
+                self.assertEqual(getattr(ns, name), expected[name])
+
+        check(types.SimpleNamespace(), {})
+        check(types.SimpleNamespace(x=1, y=2), {'x': 1, 'y': 2})
+        check(types.SimpleNamespace(**dict(x=1, y=2)), {'x': 1, 'y': 2})
+        check(types.SimpleNamespace({'x': 1, 'y': 2}, x=4, z=3),
+              {'x': 4, 'y': 2, 'z': 3})
+        check(types.SimpleNamespace([['x', 1], ['y', 2]], x=4, z=3),
+              {'x': 4, 'y': 2, 'z': 3})
+        check(types.SimpleNamespace(UserDict({'x': 1, 'y': 2}), x=4, z=3),
+              {'x': 4, 'y': 2, 'z': 3})
+        check(types.SimpleNamespace({'x': 1, 'y': 2}), {'x': 1, 'y': 2})
+        check(types.SimpleNamespace([['x', 1], ['y', 2]]), {'x': 1, 'y': 2})
+        check(types.SimpleNamespace([], x=4, z=3), {'x': 4, 'z': 3})
+        check(types.SimpleNamespace({}, x=4, z=3), {'x': 4, 'z': 3})
+        check(types.SimpleNamespace([]), {})
+        check(types.SimpleNamespace({}), {})
 
         with self.assertRaises(TypeError):
-            types.SimpleNamespace(1, 2, 3)
+            types.SimpleNamespace([], [])  # too many positional arguments
         with self.assertRaises(TypeError):
-            types.SimpleNamespace(**{1: 2})
-
-        self.assertEqual(len(ns1.__dict__), 0)
-        self.assertEqual(vars(ns1), {})
-        self.assertEqual(len(ns2.__dict__), 2)
-        self.assertEqual(vars(ns2), {'y': 2, 'x': 1})
-        self.assertEqual(len(ns3.__dict__), 2)
-        self.assertEqual(vars(ns3), {'y': 2, 'x': 1})
+            types.SimpleNamespace(1)  # not a mapping or iterable
+        with self.assertRaises(TypeError):
+            types.SimpleNamespace([1])  # non-iterable
+        with self.assertRaises(ValueError):
+            types.SimpleNamespace([['x']])  # not a pair
+        with self.assertRaises(ValueError):
+            types.SimpleNamespace([['x', 'y', 'z']])
+        with self.assertRaises(TypeError):
+            types.SimpleNamespace(**{1: 2})  # non-string key
+        with self.assertRaises(TypeError):
+            types.SimpleNamespace({1: 2})
+        with self.assertRaises(TypeError):
+            types.SimpleNamespace([[1, 2]])
+        with self.assertRaises(TypeError):
+            types.SimpleNamespace(UserDict({1: 2}))
+        with self.assertRaises(TypeError):
+            types.SimpleNamespace([[[], 2]])  # non-hashable key
 
     def test_unbound(self):
         ns1 = vars(types.SimpleNamespace())
diff --git a/Misc/NEWS.d/next/Library/2023-08-21-10-34-43.gh-issue-108191.GZM3mv.rst b/Misc/NEWS.d/next/Library/2023-08-21-10-34-43.gh-issue-108191.GZM3mv.rst
new file mode 100644 (file)
index 0000000..da4ce57
--- /dev/null
@@ -0,0 +1,3 @@
+The :class:`types.SimpleNamespace` now accepts an optional positional
+argument which specifies initial values of attributes as a dict or an
+iterable of key-value pairs.
index b2a224b9b2bda50ca4b4c274a6650a1c60c9097a..5b7547103a2b3fc5f239effe70a45934a3b6e056 100644 (file)
@@ -43,10 +43,28 @@ namespace_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
 static int
 namespace_init(_PyNamespaceObject *ns, PyObject *args, PyObject *kwds)
 {
-    if (PyTuple_GET_SIZE(args) != 0) {
-        PyErr_Format(PyExc_TypeError, "no positional arguments expected");
+    PyObject *arg = NULL;
+    if (!PyArg_UnpackTuple(args, _PyType_Name(Py_TYPE(ns)), 0, 1, &arg)) {
         return -1;
     }
+    if (arg != NULL) {
+        PyObject *dict;
+        if (PyDict_CheckExact(arg)) {
+            dict = Py_NewRef(arg);
+        }
+        else {
+            dict = PyObject_CallOneArg((PyObject *)&PyDict_Type, arg);
+            if (dict == NULL) {
+                return -1;
+            }
+        }
+        int err = (!PyArg_ValidateKeywordArguments(dict) ||
+                   PyDict_Update(ns->ns_dict, dict) < 0);
+        Py_DECREF(dict);
+        if (err) {
+            return -1;
+        }
+    }
     if (kwds == NULL) {
         return 0;
     }
@@ -227,7 +245,7 @@ static PyMethodDef namespace_methods[] = {
 
 
 PyDoc_STRVAR(namespace_doc,
-"SimpleNamespace(**kwargs)\n\
+"SimpleNamespace(mapping_or_iterable=(), /, **kwargs)\n\
 --\n\n\
 A simple attribute-based namespace.");