Skip to content

Commit ecb9f70

Browse files
committed
gh-139122: Restore generic attribute initialization of C UUIDs
1 parent 9f8f8d9 commit ecb9f70

3 files changed

Lines changed: 100 additions & 2 deletions

File tree

‎Lib/test/test_uuid.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,36 @@ class BaseTestUUID:
4343
uuid = None
4444
is_c_uuid = False
4545

46+
def test_generic_setattr(self):
47+
# Libraries such as Pydantic bypass __init__ and set UUID slots directly.
48+
# Exercise that construction path for the base type and a subclass.
49+
class UUIDSubclass(self.uuid.UUID):
50+
pass
51+
52+
class IntSubclass(int):
53+
pass
54+
55+
for cls in (self.uuid.UUID, UUIDSubclass):
56+
for value in (0, 1, 1 << 127, (1 << 128) - 1,
57+
0x123456789abcdef0123456789abcdef0, IntSubclass(42)):
58+
with self.subTest(cls=cls, value=value):
59+
u = cls.__new__(cls)
60+
object.__setattr__(u, 'int', value)
61+
object.__setattr__(u, 'is_safe', self.uuid.SafeUUID.safe)
62+
self.assertEqual(u.int, value)
63+
self.assertEqual(u.bytes, value.to_bytes(16, 'big'))
64+
self.assertEqual(u, self.uuid.UUID(int=value))
65+
self.assertIs(u.is_safe, self.uuid.SafeUUID.safe)
66+
# Replacing the integer must invalidate an already cached hash.
67+
hash(u)
68+
object.__setattr__(u, 'int', 2)
69+
self.assertEqual(hash(u), hash(self.uuid.UUID(int=2)))
70+
self.assertEqual(u.int, 2)
71+
with self.assertRaises(TypeError):
72+
u.int = value
73+
with self.assertRaises(TypeError):
74+
u.is_safe = self.uuid.SafeUUID.unknown
75+
4676
def test_nil_uuid(self):
4777
nil_uuid = self.uuid.NIL
4878

@@ -1763,6 +1793,31 @@ def time_func(self):
17631793

17641794
@unittest.skipUnless(c_uuid, "requires the C _uuid module")
17651795
class TestCImplementationCompat(unittest.TestCase):
1796+
def test_int_setter_errors(self):
1797+
# Reject invalid integers without changing the UUID's bytes or cached hash,
1798+
# even when an int subclass bypasses the preliminary range comparison.
1799+
import uuid
1800+
1801+
class LargeInt(int):
1802+
def __gt__(self, other):
1803+
return False
1804+
1805+
u = uuid.UUID(int=42)
1806+
original_hash = hash(u)
1807+
for value, error in ((None, TypeError), ('42', TypeError),
1808+
(1.5, TypeError), (-1, ValueError),
1809+
(1 << 128, ValueError),
1810+
(LargeInt(1 << 128), OverflowError)):
1811+
with self.subTest(value=value):
1812+
with self.assertRaises(error):
1813+
object.__setattr__(u, 'int', value)
1814+
self.assertEqual(u.int, 42)
1815+
self.assertEqual(hash(u), original_hash)
1816+
for name in ('int', 'is_safe'):
1817+
with self.subTest(name=name):
1818+
with self.assertRaises(AttributeError):
1819+
object.__delattr__(u, name)
1820+
17661821
def test_is_safe_lookup_error(self):
17671822
# Force the lazy SafeUUID lookup to fail before it is cached. The getter
17681823
# must propagate AttributeError and recover when the attribute returns.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Allow setting :class:`uuid.UUID` attributes ``int`` and ``is_safe`` through
2+
:meth:`object.__setattr__` and :c:func:`PyObject_GenericSetAttr`, restoring
3+
compatibility with libraries that construct UUIDs without calling their
4+
initializer. Ordinary attribute assignment remains blocked.

‎Modules/_uuidmodule.c‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,45 @@ Uuid_get_int(PyObject *op, void *Py_UNUSED(closure))
11001100
return get_int(uuidobject_CAST(op));
11011101
}
11021102

1103+
static int
1104+
Uuid_set_int(PyObject *op, PyObject *value, void *Py_UNUSED(closure))
1105+
{
1106+
if (value == NULL) {
1107+
PyErr_SetString(PyExc_AttributeError, "cannot delete UUID.int");
1108+
return -1;
1109+
}
1110+
uuid_state *state = get_uuid_state_by_cls(Py_TYPE(op));
1111+
if (validate_int(state, value) < 0) {
1112+
return -1;
1113+
}
1114+
// Convert before updating the object: conversion errors must leave it intact.
1115+
uint8_t bytes[UUID_SIZE];
1116+
if (_PyLong_AsByteArray((PyLongObject *)value, bytes, sizeof(bytes),
1117+
0, 0, 1) < 0) {
1118+
return -1;
1119+
}
1120+
uuidobject *self = uuidobject_CAST(op);
1121+
Py_BEGIN_CRITICAL_SECTION(op);
1122+
memcpy(self->bytes, bytes, sizeof(bytes));
1123+
self->cached_hash = -1;
1124+
Py_END_CRITICAL_SECTION();
1125+
return 0;
1126+
}
1127+
1128+
static int
1129+
Uuid_set_is_safe(PyObject *op, PyObject *value, void *Py_UNUSED(closure))
1130+
{
1131+
if (value == NULL) {
1132+
PyErr_SetString(PyExc_AttributeError, "cannot delete UUID.is_safe");
1133+
return -1;
1134+
}
1135+
uuidobject *self = uuidobject_CAST(op);
1136+
Py_BEGIN_CRITICAL_SECTION(op);
1137+
Py_XSETREF(self->is_safe, Py_NewRef(value));
1138+
Py_END_CRITICAL_SECTION();
1139+
return 0;
1140+
}
1141+
11031142
static PyObject *
11041143
Uuid_get_is_safe(PyObject *op, void *Py_UNUSED(closure))
11051144
{
@@ -1568,8 +1607,8 @@ _uuid_UUID__from_int_impl(PyTypeObject *type, PyObject *value)
15681607
}
15691608

15701609
static PyGetSetDef Uuid_getset[] = {
1571-
{"int", Uuid_get_int, NULL, "UUID as a 128-bit integer", NULL},
1572-
{"is_safe", Uuid_get_is_safe, NULL, "UUID safety status", NULL},
1610+
{"int", Uuid_get_int, Uuid_set_int, "UUID as a 128-bit integer", NULL},
1611+
{"is_safe", Uuid_get_is_safe, Uuid_set_is_safe, "UUID safety status", NULL},
15731612
{"fields", Uuid_get_fields, NULL, "UUID as a 6-tuple", NULL},
15741613
{"hex", Uuid_get_hex, NULL, "UUID as a 32-character hex string", NULL},
15751614
{"urn", Uuid_get_urn, NULL, "UUID as a URN", NULL},

0 commit comments

Comments
 (0)