Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2167,6 +2167,21 @@ class Spam(types.SimpleNamespace):
self.assertIs(type(spam2), Spam)
self.assertEqual(vars(spam2), {'ham': 5, 'eggs': 9})

def test_replace_invalid_subtype(self):
# See https://github.com/python/cpython/issues/143636.
class NS(types.SimpleNamespace):
def __new__(cls, *args, **kwargs):
if created:
return object()
return super().__new__(cls)

created = False
ns = NS()
created = True
err = re.escape("NS.__new__() must return an instance "
"of a subclass of types.SimpleNamespace")
self.assertRaisesRegex(TypeError, err, copy.replace, ns)

def test_fake_namespace_compare(self):
# Issue #24257: Incorrect use of PyObject_IsInstance() caused
# SystemError.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a crash when calling :class:`SimpleNamespace.__replace__()
<types.SimpleNamespace>` on non-namespace instances. Patch by Bénédikt Tran.
9 changes: 9 additions & 0 deletions Objects/namespaceobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ typedef struct {
} _PyNamespaceObject;

#define _PyNamespace_CAST(op) _Py_CAST(_PyNamespaceObject*, (op))
#define _PyNamespace_Check(op) PyObject_TypeCheck((op), &_PyNamespace_Type)


static PyMemberDef namespace_members[] = {
Expand Down Expand Up @@ -234,6 +235,14 @@ namespace_replace(PyObject *self, PyObject *args, PyObject *kwargs)
if (!result) {
return NULL;
}
if (!_PyNamespace_Check(result)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many ways to clone an object, you should make a decision whether call __init__ and/or __new__bor just create an instance of the base class. This is done differently for other types.

The following code follows the way of __reduce__() (which is also used in copy.copy()). It is equivalent to:

new_ns = type(ns)()
new_dict = dict(ns.__dict__)
for k, v in kwargs.items():
    new_dict[k] = v
for k, v in new_dict.items():
    new_ns.__dict__[k] = v

but more efficient.

This is what copy.copy() will execute.

We can implement this for the case when the result of type(ns)() is not a SimpleNamespace, but this will add an amount of code, and all this for the case which perhaps never occurs in real code. So, I am fine with simply raising an error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to change the existing behavior. Maybe we could decide in a follow-up PR how to change this?

PyErr_Format(PyExc_TypeError,
"%T.__new__() must return an instance of a subclass of %s",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think about something like "%T() returned %T object". __new__() is rarely called directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had this locally but I wondered whether this could be confused with __init__ or a simple function call.

self, _PyNamespace_Type.tp_name);
Py_DECREF(result);
return NULL;
}

if (PyDict_Update(((_PyNamespaceObject*)result)->ns_dict,
((_PyNamespaceObject*)self)->ns_dict) < 0)
{
Expand Down
Loading