Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 93b7ed7

Browse files
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).
1 parent 85ec1c2 commit 93b7ed7

File tree

5 files changed

+92
-20
lines changed

5 files changed

+92
-20
lines changed

Doc/library/types.rst

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -481,14 +481,25 @@ Additional Utility Classes and Functions
481481
A simple :class:`object` subclass that provides attribute access to its
482482
namespace, as well as a meaningful repr.
483483

484-
Unlike :class:`object`, with ``SimpleNamespace`` you can add and remove
485-
attributes. If a ``SimpleNamespace`` object is initialized with keyword
486-
arguments, those are directly added to the underlying namespace.
484+
Unlike :class:`object`, with :class:`!SimpleNamespace` you can add and remove
485+
attributes.
486+
487+
:py:class:`SimpleNamespace` objects may be initialized
488+
in the same way as :class:`dict`: either with keyword arguments,
489+
with a single positional argument, or with both.
490+
When initialized with keyword arguments,
491+
those are directly added to the underlying namespace.
492+
Alternatively, when initialized with a positional argument,
493+
the underlying namespace will be updated with key-value pairs
494+
from that argument (either a mapping object or
495+
an :term:`iterable` object producing key-value pairs).
496+
All such keys must be strings.
487497

488498
The type is roughly equivalent to the following code::
489499

490500
class SimpleNamespace:
491-
def __init__(self, /, **kwargs):
501+
def __init__(self, mapping_or_iterable=(), /, **kwargs):
502+
self.__dict__.update(mapping_or_iterable)
492503
self.__dict__.update(kwargs)
493504

494505
def __repr__(self):
@@ -512,6 +523,9 @@ Additional Utility Classes and Functions
512523
Attribute order in the repr changed from alphabetical to insertion (like
513524
``dict``).
514525

526+
.. versionchanged:: 3.13
527+
Added support for an optional positional argument.
528+
515529
.. function:: DynamicClassAttribute(fget=None, fset=None, fdel=None, doc=None)
516530

517531
Route attribute access on a class to __getattr__.

Doc/whatsnew/3.13.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,14 @@ traceback
804804
``True``) to indicate whether ``exc_type`` should be saved.
805805
(Contributed by Irit Katriel in :gh:`112332`.)
806806

807+
types
808+
-----
809+
810+
* :class:`~types.SimpleNamespace` constructor now allows specifying initial
811+
values of attributes as a positional argument which must be a mapping or
812+
an iterable of key-value pairs.
813+
(Contributed by Serhiy Storchaka in :gh:`108191`.)
814+
807815
typing
808816
------
809817

Lib/test/test_types.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from test.support import run_with_locale, cpython_only, MISSING_C_DOCSTRINGS
44
import collections.abc
5-
from collections import namedtuple
5+
from collections import namedtuple, UserDict
66
import copy
77
import _datetime
88
import gc
@@ -1755,21 +1755,50 @@ class Model(metaclass=ModelBase):
17551755
class SimpleNamespaceTests(unittest.TestCase):
17561756

17571757
def test_constructor(self):
1758-
ns1 = types.SimpleNamespace()
1759-
ns2 = types.SimpleNamespace(x=1, y=2)
1760-
ns3 = types.SimpleNamespace(**dict(x=1, y=2))
1758+
def check(ns, expected):
1759+
self.assertEqual(len(ns.__dict__), len(expected))
1760+
self.assertEqual(vars(ns), expected)
1761+
# check order
1762+
self.assertEqual(list(vars(ns).items()), list(expected.items()))
1763+
for name in expected:
1764+
self.assertEqual(getattr(ns, name), expected[name])
1765+
1766+
check(types.SimpleNamespace(), {})
1767+
check(types.SimpleNamespace(x=1, y=2), {'x': 1, 'y': 2})
1768+
check(types.SimpleNamespace(**dict(x=1, y=2)), {'x': 1, 'y': 2})
1769+
check(types.SimpleNamespace({'x': 1, 'y': 2}, x=4, z=3),
1770+
{'x': 4, 'y': 2, 'z': 3})
1771+
check(types.SimpleNamespace([['x', 1], ['y', 2]], x=4, z=3),
1772+
{'x': 4, 'y': 2, 'z': 3})
1773+
check(types.SimpleNamespace(UserDict({'x': 1, 'y': 2}), x=4, z=3),
1774+
{'x': 4, 'y': 2, 'z': 3})
1775+
check(types.SimpleNamespace({'x': 1, 'y': 2}), {'x': 1, 'y': 2})
1776+
check(types.SimpleNamespace([['x', 1], ['y', 2]]), {'x': 1, 'y': 2})
1777+
check(types.SimpleNamespace([], x=4, z=3), {'x': 4, 'z': 3})
1778+
check(types.SimpleNamespace({}, x=4, z=3), {'x': 4, 'z': 3})
1779+
check(types.SimpleNamespace([]), {})
1780+
check(types.SimpleNamespace({}), {})
17611781

17621782
with self.assertRaises(TypeError):
1763-
types.SimpleNamespace(1, 2, 3)
1783+
types.SimpleNamespace([], []) # too many positional arguments
17641784
with self.assertRaises(TypeError):
1765-
types.SimpleNamespace(**{1: 2})
1766-
1767-
self.assertEqual(len(ns1.__dict__), 0)
1768-
self.assertEqual(vars(ns1), {})
1769-
self.assertEqual(len(ns2.__dict__), 2)
1770-
self.assertEqual(vars(ns2), {'y': 2, 'x': 1})
1771-
self.assertEqual(len(ns3.__dict__), 2)
1772-
self.assertEqual(vars(ns3), {'y': 2, 'x': 1})
1785+
types.SimpleNamespace(1) # not a mapping or iterable
1786+
with self.assertRaises(TypeError):
1787+
types.SimpleNamespace([1]) # non-iterable
1788+
with self.assertRaises(ValueError):
1789+
types.SimpleNamespace([['x']]) # not a pair
1790+
with self.assertRaises(ValueError):
1791+
types.SimpleNamespace([['x', 'y', 'z']])
1792+
with self.assertRaises(TypeError):
1793+
types.SimpleNamespace(**{1: 2}) # non-string key
1794+
with self.assertRaises(TypeError):
1795+
types.SimpleNamespace({1: 2})
1796+
with self.assertRaises(TypeError):
1797+
types.SimpleNamespace([[1, 2]])
1798+
with self.assertRaises(TypeError):
1799+
types.SimpleNamespace(UserDict({1: 2}))
1800+
with self.assertRaises(TypeError):
1801+
types.SimpleNamespace([[[], 2]]) # non-hashable key
17731802

17741803
def test_unbound(self):
17751804
ns1 = vars(types.SimpleNamespace())
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The :class:`types.SimpleNamespace` now accepts an optional positional
2+
argument which specifies initial values of attributes as a dict or an
3+
iterable of key-value pairs.

Objects/namespaceobject.c

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,28 @@ namespace_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
4343
static int
4444
namespace_init(_PyNamespaceObject *ns, PyObject *args, PyObject *kwds)
4545
{
46-
if (PyTuple_GET_SIZE(args) != 0) {
47-
PyErr_Format(PyExc_TypeError, "no positional arguments expected");
46+
PyObject *arg = NULL;
47+
if (!PyArg_UnpackTuple(args, _PyType_Name(Py_TYPE(ns)), 0, 1, &arg)) {
4848
return -1;
4949
}
50+
if (arg != NULL) {
51+
PyObject *dict;
52+
if (PyDict_CheckExact(arg)) {
53+
dict = Py_NewRef(arg);
54+
}
55+
else {
56+
dict = PyObject_CallOneArg((PyObject *)&PyDict_Type, arg);
57+
if (dict == NULL) {
58+
return -1;
59+
}
60+
}
61+
int err = (!PyArg_ValidateKeywordArguments(dict) ||
62+
PyDict_Update(ns->ns_dict, dict) < 0);
63+
Py_DECREF(dict);
64+
if (err) {
65+
return -1;
66+
}
67+
}
5068
if (kwds == NULL) {
5169
return 0;
5270
}
@@ -227,7 +245,7 @@ static PyMethodDef namespace_methods[] = {
227245

228246

229247
PyDoc_STRVAR(namespace_doc,
230-
"SimpleNamespace(**kwargs)\n\
248+
"SimpleNamespace(mapping_or_iterable=(), /, **kwargs)\n\
231249
--\n\n\
232250
A simple attribute-based namespace.");
233251

0 commit comments

Comments
 (0)