Skip to content

Commit 04f6733

Browse files
gh-102500: Implement PEP 688 (#102521)
Co-authored-by: Kumar Aditya <[email protected]>
1 parent b17d32c commit 04f6733

26 files changed

+640
-15
lines changed

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ struct _Py_global_strings {
8181
STRUCT_FOR_ID(__await__)
8282
STRUCT_FOR_ID(__bases__)
8383
STRUCT_FOR_ID(__bool__)
84+
STRUCT_FOR_ID(__buffer__)
8485
STRUCT_FOR_ID(__build_class__)
8586
STRUCT_FOR_ID(__builtins__)
8687
STRUCT_FOR_ID(__bytes__)
@@ -180,6 +181,7 @@ struct _Py_global_strings {
180181
STRUCT_FOR_ID(__rdivmod__)
181182
STRUCT_FOR_ID(__reduce__)
182183
STRUCT_FOR_ID(__reduce_ex__)
184+
STRUCT_FOR_ID(__release_buffer__)
183185
STRUCT_FOR_ID(__repr__)
184186
STRUCT_FOR_ID(__reversed__)
185187
STRUCT_FOR_ID(__rfloordiv__)
@@ -610,6 +612,7 @@ struct _Py_global_strings {
610612
STRUCT_FOR_ID(reducer_override)
611613
STRUCT_FOR_ID(registry)
612614
STRUCT_FOR_ID(rel_tol)
615+
STRUCT_FOR_ID(release)
613616
STRUCT_FOR_ID(reload)
614617
STRUCT_FOR_ID(repl)
615618
STRUCT_FOR_ID(replace)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#ifndef Py_INTERNAL_MEMORYOBJECT_H
2+
#define Py_INTERNAL_MEMORYOBJECT_H
3+
#ifdef __cplusplus
4+
extern "C" {
5+
#endif
6+
7+
#ifndef Py_BUILD_CORE
8+
# error "this header requires Py_BUILD_CORE define"
9+
#endif
10+
11+
PyObject *
12+
PyMemoryView_FromObjectAndFlags(PyObject *v, int flags);
13+
14+
#ifdef __cplusplus
15+
}
16+
#endif
17+
#endif /* !Py_INTERNAL_MEMORYOBJECT_H */

Include/internal/pycore_runtime_init_generated.h

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_typeobject.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ _Py_type_getattro(PyTypeObject *type, PyObject *name);
138138
PyObject *_Py_slot_tp_getattro(PyObject *self, PyObject *name);
139139
PyObject *_Py_slot_tp_getattr_hook(PyObject *self, PyObject *name);
140140

141+
PyAPI_DATA(PyTypeObject) _PyBufferWrapper_Type;
142+
141143
PyObject *
142144
_PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *meth_found);
143145
PyObject *

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/pybuffer.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ PyAPI_FUNC(void) PyBuffer_Release(Py_buffer *view);
104104
/* Maximum number of dimensions */
105105
#define PyBUF_MAX_NDIM 64
106106

107-
/* Flags for getting buffers */
107+
/* Flags for getting buffers. Keep these in sync with inspect.BufferFlags. */
108108
#define PyBUF_SIMPLE 0
109109
#define PyBUF_WRITABLE 0x0001
110110

Lib/_collections_abc.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def _f(): pass
4949
"Mapping", "MutableMapping",
5050
"MappingView", "KeysView", "ItemsView", "ValuesView",
5151
"Sequence", "MutableSequence",
52-
"ByteString",
52+
"ByteString", "Buffer",
5353
]
5454

5555
# This module has been renamed from collections.abc to _collections_abc to
@@ -439,6 +439,21 @@ def __subclasshook__(cls, C):
439439
return NotImplemented
440440

441441

442+
class Buffer(metaclass=ABCMeta):
443+
444+
__slots__ = ()
445+
446+
@abstractmethod
447+
def __buffer__(self, flags: int, /) -> memoryview:
448+
raise NotImplementedError
449+
450+
@classmethod
451+
def __subclasshook__(cls, C):
452+
if cls is Buffer:
453+
return _check_methods(C, "__buffer__")
454+
return NotImplemented
455+
456+
442457
class _CallableGenericAlias(GenericAlias):
443458
""" Represent `Callable[argtypes, resulttype]`.
444459

Lib/inspect.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"Attribute",
4444
"BlockFinder",
4545
"BoundArguments",
46+
"BufferFlags",
4647
"CORO_CLOSED",
4748
"CORO_CREATED",
4849
"CORO_RUNNING",
@@ -3312,6 +3313,28 @@ def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=F
33123313
globals=globals, locals=locals, eval_str=eval_str)
33133314

33143315

3316+
class BufferFlags(enum.IntFlag):
3317+
SIMPLE = 0x0
3318+
WRITABLE = 0x1
3319+
FORMAT = 0x4
3320+
ND = 0x8
3321+
STRIDES = 0x10 | ND
3322+
C_CONTIGUOUS = 0x20 | STRIDES
3323+
F_CONTIGUOUS = 0x40 | STRIDES
3324+
ANY_CONTIGUOUS = 0x80 | STRIDES
3325+
INDIRECT = 0x100 | STRIDES
3326+
CONTIG = ND | WRITABLE
3327+
CONTIG_RO = ND
3328+
STRIDED = STRIDES | WRITABLE
3329+
STRIDED_RO = STRIDES
3330+
RECORDS = STRIDES | WRITABLE | FORMAT
3331+
RECORDS_RO = STRIDES | FORMAT
3332+
FULL = INDIRECT | WRITABLE | FORMAT
3333+
FULL_RO = INDIRECT | FORMAT
3334+
READ = 0x100
3335+
WRITE = 0x200
3336+
3337+
33153338
def _main():
33163339
""" Logic for inspecting an object given at command line """
33173340
import argparse

Lib/test/test_buffer.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import unittest
1818
from test import support
1919
from test.support import os_helper
20+
import inspect
2021
from itertools import permutations, product
2122
from random import randrange, sample, choice
2223
import warnings
@@ -4438,5 +4439,146 @@ def test_pybuffer_size_from_format(self):
44384439
struct.calcsize(format))
44394440

44404441

4442+
class TestPythonBufferProtocol(unittest.TestCase):
4443+
def test_basic(self):
4444+
class MyBuffer:
4445+
def __buffer__(self, flags):
4446+
return memoryview(b"hello")
4447+
4448+
mv = memoryview(MyBuffer())
4449+
self.assertEqual(mv.tobytes(), b"hello")
4450+
self.assertEqual(bytes(MyBuffer()), b"hello")
4451+
4452+
def test_bad_buffer_method(self):
4453+
class MustReturnMV:
4454+
def __buffer__(self, flags):
4455+
return 42
4456+
4457+
self.assertRaises(TypeError, memoryview, MustReturnMV())
4458+
4459+
class NoBytesEither:
4460+
def __buffer__(self, flags):
4461+
return b"hello"
4462+
4463+
self.assertRaises(TypeError, memoryview, NoBytesEither())
4464+
4465+
class WrongArity:
4466+
def __buffer__(self):
4467+
return memoryview(b"hello")
4468+
4469+
self.assertRaises(TypeError, memoryview, WrongArity())
4470+
4471+
def test_release_buffer(self):
4472+
class WhatToRelease:
4473+
def __init__(self):
4474+
self.held = False
4475+
self.ba = bytearray(b"hello")
4476+
4477+
def __buffer__(self, flags):
4478+
if self.held:
4479+
raise TypeError("already held")
4480+
self.held = True
4481+
return memoryview(self.ba)
4482+
4483+
def __release_buffer__(self, buffer):
4484+
self.held = False
4485+
4486+
wr = WhatToRelease()
4487+
self.assertFalse(wr.held)
4488+
with memoryview(wr) as mv:
4489+
self.assertTrue(wr.held)
4490+
self.assertEqual(mv.tobytes(), b"hello")
4491+
self.assertFalse(wr.held)
4492+
4493+
def test_same_buffer_returned(self):
4494+
class WhatToRelease:
4495+
def __init__(self):
4496+
self.held = False
4497+
self.ba = bytearray(b"hello")
4498+
self.created_mv = None
4499+
4500+
def __buffer__(self, flags):
4501+
if self.held:
4502+
raise TypeError("already held")
4503+
self.held = True
4504+
self.created_mv = memoryview(self.ba)
4505+
return self.created_mv
4506+
4507+
def __release_buffer__(self, buffer):
4508+
assert buffer is self.created_mv
4509+
self.held = False
4510+
4511+
wr = WhatToRelease()
4512+
self.assertFalse(wr.held)
4513+
with memoryview(wr) as mv:
4514+
self.assertTrue(wr.held)
4515+
self.assertEqual(mv.tobytes(), b"hello")
4516+
self.assertFalse(wr.held)
4517+
4518+
def test_buffer_flags(self):
4519+
class PossiblyMutable:
4520+
def __init__(self, data, mutable) -> None:
4521+
self._data = bytearray(data)
4522+
self._mutable = mutable
4523+
4524+
def __buffer__(self, flags):
4525+
if flags & inspect.BufferFlags.WRITABLE:
4526+
if not self._mutable:
4527+
raise RuntimeError("not mutable")
4528+
return memoryview(self._data)
4529+
else:
4530+
return memoryview(bytes(self._data))
4531+
4532+
mutable = PossiblyMutable(b"hello", True)
4533+
immutable = PossiblyMutable(b"hello", False)
4534+
with memoryview._from_flags(mutable, inspect.BufferFlags.WRITABLE) as mv:
4535+
self.assertEqual(mv.tobytes(), b"hello")
4536+
mv[0] = ord(b'x')
4537+
self.assertEqual(mv.tobytes(), b"xello")
4538+
with memoryview._from_flags(mutable, inspect.BufferFlags.SIMPLE) as mv:
4539+
self.assertEqual(mv.tobytes(), b"xello")
4540+
with self.assertRaises(TypeError):
4541+
mv[0] = ord(b'h')
4542+
self.assertEqual(mv.tobytes(), b"xello")
4543+
with memoryview._from_flags(immutable, inspect.BufferFlags.SIMPLE) as mv:
4544+
self.assertEqual(mv.tobytes(), b"hello")
4545+
with self.assertRaises(TypeError):
4546+
mv[0] = ord(b'x')
4547+
self.assertEqual(mv.tobytes(), b"hello")
4548+
4549+
with self.assertRaises(RuntimeError):
4550+
memoryview._from_flags(immutable, inspect.BufferFlags.WRITABLE)
4551+
with memoryview(immutable) as mv:
4552+
self.assertEqual(mv.tobytes(), b"hello")
4553+
with self.assertRaises(TypeError):
4554+
mv[0] = ord(b'x')
4555+
self.assertEqual(mv.tobytes(), b"hello")
4556+
4557+
def test_call_builtins(self):
4558+
ba = bytearray(b"hello")
4559+
mv = ba.__buffer__(0)
4560+
self.assertEqual(mv.tobytes(), b"hello")
4561+
ba.__release_buffer__(mv)
4562+
with self.assertRaises(OverflowError):
4563+
ba.__buffer__(sys.maxsize + 1)
4564+
4565+
@unittest.skipIf(_testcapi is None, "requires _testcapi")
4566+
def test_c_buffer(self):
4567+
buf = _testcapi.testBuf()
4568+
self.assertEqual(buf.references, 0)
4569+
mv = buf.__buffer__(0)
4570+
self.assertIsInstance(mv, memoryview)
4571+
self.assertEqual(mv.tobytes(), b"test")
4572+
self.assertEqual(buf.references, 1)
4573+
buf.__release_buffer__(mv)
4574+
self.assertEqual(buf.references, 0)
4575+
with self.assertRaises(ValueError):
4576+
mv.tobytes()
4577+
# Calling it again doesn't cause issues
4578+
with self.assertRaises(ValueError):
4579+
buf.__release_buffer__(mv)
4580+
self.assertEqual(buf.references, 0)
4581+
4582+
44414583
if __name__ == "__main__":
44424584
unittest.main()

Lib/test/test_collections.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from collections.abc import Set, MutableSet
2626
from collections.abc import Mapping, MutableMapping, KeysView, ItemsView, ValuesView
2727
from collections.abc import Sequence, MutableSequence
28-
from collections.abc import ByteString
28+
from collections.abc import ByteString, Buffer
2929

3030

3131
class TestUserObjects(unittest.TestCase):
@@ -1949,6 +1949,15 @@ def test_ByteString(self):
19491949
self.assertFalse(issubclass(memoryview, ByteString))
19501950
self.validate_abstract_methods(ByteString, '__getitem__', '__len__')
19511951

1952+
def test_Buffer(self):
1953+
for sample in [bytes, bytearray, memoryview]:
1954+
self.assertIsInstance(sample(b"x"), Buffer)
1955+
self.assertTrue(issubclass(sample, Buffer))
1956+
for sample in [str, list, tuple]:
1957+
self.assertNotIsInstance(sample(), Buffer)
1958+
self.assertFalse(issubclass(sample, Buffer))
1959+
self.validate_abstract_methods(Buffer, '__buffer__')
1960+
19521961
def test_MutableSequence(self):
19531962
for sample in [tuple, str, bytes]:
19541963
self.assertNotIsInstance(sample(), MutableSequence)

Lib/test/test_doctest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ def non_Python_modules(): r"""
707707
708708
>>> import builtins
709709
>>> tests = doctest.DocTestFinder().find(builtins)
710-
>>> 830 < len(tests) < 850 # approximate number of objects with docstrings
710+
>>> 830 < len(tests) < 860 # approximate number of objects with docstrings
711711
True
712712
>>> real_tests = [t for t in tests if len(t.examples) > 0]
713713
>>> len(real_tests) # objects that actually have doctests
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Make the buffer protocol accessible in Python code using the new
2+
``__buffer__`` and ``__release_buffer__`` magic methods. See :pep:`688` for
3+
details. Patch by Jelle Zijlstra.

0 commit comments

Comments
 (0)