Mailing List Archive

gh-108191: Add support of positional argument in SimpleNamespace constructor (GH-108195)
https://github.com/python/cpython/commit/93b7ed7c6b1494f41818fa571b1843ca3dfe1bd1
commit: 93b7ed7c6b1494f41818fa571b1843ca3dfe1bd1
branch: main
author: Serhiy Storchaka <storchaka@gmail.com>
committer: serhiy-storchaka <storchaka@gmail.com>
date: 2024-04-25T00:39:54+03:00
summary:

gh-108191: Add support of positional argument in SimpleNamespace constructor (GH-108195)

SimpleNamespace({'a': 1, 'b': 2}) and SimpleNamespace([('a', 1), ('b', 2)])
are now the same as SimpleNamespace(a=1, b=2).

files:
A Misc/NEWS.d/next/Library/2023-08-21-10-34-43.gh-issue-108191.GZM3mv.rst
M Doc/library/types.rst
M Doc/whatsnew/3.13.rst
M Lib/test/test_types.py
M Objects/namespaceobject.c

diff --git a/Doc/library/types.rst b/Doc/library/types.rst
index b856544e44207c..89bc0a600c0af8 100644
--- a/Doc/library/types.rst
+++ b/Doc/library/types.rst
@@ -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__.
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 89694afdfa3fec..ad107aad5db3bd 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -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
------

diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py
index 16985122bc0219..fbca198aab5180 100644
--- a/Lib/test/test_types.py
+++ b/Lib/test/test_types.py
@@ -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 Model(metaclass=ModelBase):
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
index 00000000000000..da4ce5742549e6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-08-21-10-34-43.gh-issue-108191.GZM3mv.rst
@@ -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.
diff --git a/Objects/namespaceobject.c b/Objects/namespaceobject.c
index b2a224b9b2bda5..5b7547103a2b3f 100644
--- a/Objects/namespaceobject.c
+++ b/Objects/namespaceobject.c
@@ -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.");


_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-leave@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: list-python-checkins@lists.gossamer-threads.com