Skip to content

Commit 548afd3

Browse files
committed
Make our UUID implementation compatible with 3.8
1 parent c649123 commit 548afd3

File tree

2 files changed

+197
-24
lines changed

2 files changed

+197
-24
lines changed

codecs/uuid.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ cdef uuid_encode(CodecContext settings, WriteBuffer wbuf, obj):
1111

1212
if type(obj) is pg_UUID:
1313
wbuf.write_int32(<int32_t>16)
14-
wbuf.write_cstr((<PgBaseUUID>obj)._data, 16)
14+
wbuf.write_cstr((<UUID>obj)._data, 16)
1515
elif cpython.PyUnicode_Check(obj):
1616
pg_uuid_bytes_from_str(obj, buf)
1717
wbuf.write_int32(<int32_t>16)

uuid.pyx

Lines changed: 196 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import functools
22
import uuid
33

44
from libc.stdint cimport uint64_t, uint8_t, int8_t
5-
from libc.string cimport memcpy
5+
from libc.string cimport memcpy, strncmp
66

77

88
# A more efficient UUID type implementation
@@ -40,14 +40,6 @@ cdef inline char i64_to_hex(uint64_t num, char *s):
4040
return 0
4141

4242

43-
cdef pg_uuid_from_buf(const char *buf):
44-
cdef:
45-
PgBaseUUID u = UUID.__new__(UUID)
46-
memcpy(u._data, buf, 16)
47-
u._int = None
48-
return u
49-
50-
5143
cdef pg_uuid_bytes_from_str(str u, char *out):
5244
cdef:
5345
char *orig_buf
@@ -95,11 +87,27 @@ cdef pg_uuid_bytes_from_str(str u, char *out):
9587
f'invalid UUID {u!r}: decodes to less than 16 bytes')
9688

9789

98-
cdef class PgBaseUUID:
90+
cdef class __UUIDReplaceMe:
91+
pass
92+
93+
94+
cdef pg_uuid_from_buf(const char *buf):
95+
cdef:
96+
UUID u = UUID.__new__(UUID)
97+
memcpy(u._data, buf, 16)
98+
return u
99+
100+
101+
@cython.final
102+
@cython.no_gc_clear
103+
@cython.freelist(128)
104+
cdef class UUID(__UUIDReplaceMe):
99105

100106
cdef:
101107
char _data[16]
102108
object _int
109+
object _hash
110+
object __weakref__
103111

104112
def __init__(self, inp):
105113
cdef:
@@ -117,19 +125,21 @@ cdef class PgBaseUUID:
117125
else:
118126
raise TypeError(f'a bytes or str object expected, got {inp!r}')
119127

120-
self._int = None
128+
@property
129+
def bytes(self):
130+
return cpython.PyBytes_FromStringAndSize(self._data, 16)
121131

122-
property bytes:
123-
def __get__(self):
124-
return cpython.PyBytes_FromStringAndSize(self._data, 16)
132+
@property
133+
def int(self):
134+
if self._int is None:
135+
# The cache is important because `self.int` can be
136+
# used multiple times by __hash__ etc.
137+
self._int = int.from_bytes(self.bytes, 'big')
138+
return self._int
125139

126-
property int:
127-
def __get__(self):
128-
if self._int is None:
129-
# The cache is important because `self.int` can be
130-
# used multiple times by __hash__ etc.
131-
self._int = int.from_bytes(self.bytes, 'big')
132-
return self._int
140+
@property
141+
def is_safe(self):
142+
return uuid.SafeUUID.unknown
133143

134144
def __str__(self):
135145
cdef:
@@ -158,9 +168,172 @@ cdef class PgBaseUUID:
158168
def __reduce__(self):
159169
return (type(self), (self.bytes,))
160170

171+
def __eq__(self, other):
172+
if type(other) is UUID:
173+
return strncmp(self._data, (<UUID>other)._data, 16) == 0
174+
if isinstance(other, uuid.UUID):
175+
return self.int == other.int
176+
return NotImplemented
177+
178+
def __ne__(self, other):
179+
if type(other) is UUID:
180+
return strncmp(self._data, (<UUID>other)._data, 16) != 0
181+
if isinstance(other, uuid.UUID):
182+
return self.int != other.int
183+
return NotImplemented
184+
185+
def __lt__(self, other):
186+
if type(other) is UUID:
187+
return strncmp(self._data, (<UUID>other)._data, 16) < 0
188+
if isinstance(other, uuid.UUID):
189+
return self.int < other.int
190+
return NotImplemented
191+
192+
def __gt__(self, other):
193+
if type(other) is UUID:
194+
return strncmp(self._data, (<UUID>other)._data, 16) > 0
195+
if isinstance(other, uuid.UUID):
196+
return self.int > other.int
197+
return NotImplemented
198+
199+
def __le__(self, other):
200+
if type(other) is UUID:
201+
return strncmp(self._data, (<UUID>other)._data, 16) <= 0
202+
if isinstance(other, uuid.UUID):
203+
return self.int <= other.int
204+
return NotImplemented
205+
206+
def __ge__(self, other):
207+
if type(other) is UUID:
208+
return strncmp(self._data, (<UUID>other)._data, 16) >= 0
209+
if isinstance(other, uuid.UUID):
210+
return self.int >= other.int
211+
return NotImplemented
212+
213+
def __hash__(self):
214+
# In EdgeDB every schema object has a uuid and there are
215+
# huge hash-maps of them. We want UUID.__hash__ to be
216+
# as fast as possible.
217+
if self._hash is not None:
218+
return self._hash
219+
220+
self._hash = hash(self.int)
221+
return self._hash
222+
223+
def __int__(self):
224+
return self.int
225+
226+
@property
227+
def bytes_le(self):
228+
bytes = self.bytes
229+
return (bytes[4-1::-1] + bytes[6-1:4-1:-1] + bytes[8-1:6-1:-1] +
230+
bytes[8:])
231+
232+
@property
233+
def fields(self):
234+
return (self.time_low, self.time_mid, self.time_hi_version,
235+
self.clock_seq_hi_variant, self.clock_seq_low, self.node)
236+
237+
@property
238+
def time_low(self):
239+
return self.int >> 96
240+
241+
@property
242+
def time_mid(self):
243+
return (self.int >> 80) & 0xffff
244+
245+
@property
246+
def time_hi_version(self):
247+
return (self.int >> 64) & 0xffff
248+
249+
@property
250+
def clock_seq_hi_variant(self):
251+
return (self.int >> 56) & 0xff
252+
253+
@property
254+
def clock_seq_low(self):
255+
return (self.int >> 48) & 0xff
256+
257+
@property
258+
def time(self):
259+
return (((self.time_hi_version & 0x0fff) << 48) |
260+
(self.time_mid << 32) | self.time_low)
261+
262+
@property
263+
def clock_seq(self):
264+
return (((self.clock_seq_hi_variant & 0x3f) << 8) |
265+
self.clock_seq_low)
266+
267+
@property
268+
def node(self):
269+
return self.int & 0xffffffffffff
270+
271+
@property
272+
def hex(self):
273+
cdef:
274+
uint64_t u
275+
char out[32]
161276

162-
class UUID(PgBaseUUID, uuid.UUID):
163-
__slots__ = ()
277+
u = <uint64_t>hton.unpack_int64(self._data)
278+
i64_to_hex(u, out)
279+
u = <uint64_t>hton.unpack_int64(self._data + 8)
280+
i64_to_hex(u, out + 16)
281+
282+
return cpythonx.PyUnicode_FromKindAndData(
283+
cpythonx.PyUnicode_1BYTE_KIND, <void*>out, 32)
284+
285+
@property
286+
def urn(self):
287+
return 'urn:uuid:' + str(self)
288+
289+
@property
290+
def variant(self):
291+
if not self.int & (0x8000 << 48):
292+
return uuid.RESERVED_NCS
293+
elif not self.int & (0x4000 << 48):
294+
return uuid.RFC_4122
295+
elif not self.int & (0x2000 << 48):
296+
return uuid.RESERVED_MICROSOFT
297+
else:
298+
return uuid.RESERVED_FUTURE
299+
300+
@property
301+
def version(self):
302+
# The version bits are only meaningful for RFC 4122 UUIDs.
303+
if self.variant == uuid.RFC_4122:
304+
return int((self.int >> 76) & 0xf)
305+
306+
307+
# <hack>
308+
# In order for `isinstance(pgproto.UUID, uuid.UUID)` to work,
309+
# patch __bases__ and __mro__ by injecting `uuid.UUID`.
310+
#
311+
# We apply brute-force here because the following pattern stopped
312+
# working with Python 3.8:
313+
#
314+
# cdef class OurUUID:
315+
# ...
316+
#
317+
# class UUID(OurUUID, uuid.UUID):
318+
# ...
319+
#
320+
# With Python 3.8 it now produces
321+
#
322+
# "TypeError: multiple bases have instance lay-out conflict"
323+
#
324+
# error. Maybe it's possible to fix this some other way, but
325+
# the best solution possible would be to just contribute our
326+
# faster UUID to the standard library and not have this problem
327+
# at all. For now this hack is pretty safe and should be
328+
# compatible with future Pythons for long enough.
329+
#
330+
assert UUID.__bases__[0] is __UUIDReplaceMe
331+
assert UUID.__mro__[1] is __UUIDReplaceMe
332+
cpython.Py_INCREF(uuid.UUID)
333+
cpython.PyTuple_SET_ITEM(UUID.__bases__, 0, uuid.UUID)
334+
cpython.Py_INCREF(uuid.UUID)
335+
cpython.PyTuple_SET_ITEM(UUID.__mro__, 1, uuid.UUID)
336+
# </hack>
164337

165338

166339
cdef pg_UUID = UUID

0 commit comments

Comments
 (0)