Skip to content

Commit 86b55a6

Browse files
authored
gh-87613: Argument Clinic vectorcall decorator (GH-145381)
Add `@vectorcall` as a decorator to Argument Clinic (AC) which generates a new [Vectorcall Protocol](https://docs.python.org/3/c-api/call.html#the-vectorcall-protocol) argument parsing C function named `{}_vectorcall`. This is only supported for `__new__` and `__init__` currently to simplify implementation. The generated code has similar or better performance to existing hand-written cases for `list`, `float`, `str`, `tuple`, `enumerate`, `reversed`, and `int`. Using the decorator added vectorcall to `bytearray` and construction got 1.09x faster. For more details see the comments in gh-87613.
1 parent 9bd670c commit 86b55a6

18 files changed

Lines changed: 1310 additions & 198 deletions

Lib/test/test_clinic.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,9 @@ def test_directive_output_invalid_command(self):
644644
- 'methoddef_define'
645645
- 'impl_prototype'
646646
- 'parser_prototype'
647+
- 'parser_helper'
647648
- 'parser_definition'
649+
- 'vectorcall_definition'
648650
- 'cpp_endif'
649651
- 'methoddef_ifndef'
650652
- 'impl_definition'
@@ -2887,6 +2889,112 @@ def test_duplicate_coexist(self):
28872889
"""
28882890
self.expect_failure(block, err, lineno=2)
28892891

2892+
def test_duplicate_vectorcall(self):
2893+
err = "Called @vectorcall twice"
2894+
block = """
2895+
module m
2896+
class Foo "FooObject *" ""
2897+
@vectorcall
2898+
@vectorcall
2899+
Foo.__init__
2900+
"""
2901+
self.expect_failure(block, err, lineno=3)
2902+
2903+
def test_vectorcall_on_regular_method(self):
2904+
err = "@vectorcall can only be used with __init__ and __new__ methods"
2905+
block = """
2906+
module m
2907+
class Foo "FooObject *" ""
2908+
@vectorcall
2909+
Foo.some_method
2910+
"""
2911+
self.expect_failure(block, err, lineno=3)
2912+
2913+
def test_vectorcall_on_module_function(self):
2914+
err = "@vectorcall can only be used with __init__ and __new__ methods"
2915+
block = """
2916+
module m
2917+
@vectorcall
2918+
m.fn
2919+
"""
2920+
self.expect_failure(block, err, lineno=2)
2921+
2922+
def test_vectorcall_on_init(self):
2923+
block = """
2924+
module m
2925+
class Foo "FooObject *" "Foo_Type"
2926+
@vectorcall
2927+
Foo.__init__
2928+
iterable: object = NULL
2929+
/
2930+
"""
2931+
func = self.parse_function(block, signatures_in_block=3,
2932+
function_index=2)
2933+
self.assertTrue(func.vectorcall)
2934+
2935+
def test_vectorcall_on_new(self):
2936+
block = """
2937+
module m
2938+
class Foo "FooObject *" "Foo_Type"
2939+
@classmethod
2940+
@vectorcall
2941+
Foo.__new__
2942+
x: object = NULL
2943+
/
2944+
"""
2945+
func = self.parse_function(block, signatures_in_block=3,
2946+
function_index=2)
2947+
self.assertTrue(func.vectorcall)
2948+
2949+
def test_vectorcall_takes_no_arguments(self):
2950+
err = "at_vectorcall() takes 1 positional argument but 2 were given"
2951+
block = """
2952+
module m
2953+
class Foo "FooObject *" "Foo_Type"
2954+
@vectorcall bogus=True
2955+
Foo.__init__
2956+
"""
2957+
self.expect_failure(block, err, lineno=2)
2958+
2959+
def test_vectorcall_without_type_object(self):
2960+
err = "@vectorcall requires the type object of 'Foo'"
2961+
block = """
2962+
module m
2963+
class Foo "FooObject *" ""
2964+
@vectorcall
2965+
Foo.__init__
2966+
"""
2967+
self.expect_failure(block, err, lineno=3)
2968+
2969+
def test_vectorcall_unsupported_converter(self):
2970+
# str(encoding=...) has no parse_arg() implementation.
2971+
err = ("@vectorcall requires all converters to support "
2972+
"parse_arg(); parameter 's' does not")
2973+
block = """
2974+
module m
2975+
class Foo "FooObject *" "Foo_Type"
2976+
@classmethod
2977+
@vectorcall
2978+
Foo.__new__
2979+
s: str(encoding="utf-8")
2980+
/
2981+
"""
2982+
self.expect_failure(block, err, lineno=6)
2983+
2984+
def test_vectorcall_with_option_groups(self):
2985+
err = "@vectorcall does not support optional groups"
2986+
block = """
2987+
module m
2988+
class Foo "FooObject *" "Foo_Type"
2989+
@vectorcall
2990+
Foo.__init__
2991+
[
2992+
a: object
2993+
]
2994+
/
2995+
"""
2996+
self.expect_failure(block, err, lineno=7)
2997+
28902998
def test_unused_param(self):
28912999
block = self.parse("""
28923000
module foo
@@ -5020,6 +5128,105 @@ def test_kwds_with_pos_only_and_stararg(self):
50205128
self.assertEqual(ac_tester.kwds_with_pos_only_and_stararg(1, 2, *args, **kwds), (1, 2, args, kwds))
50215129

50225130

5131+
@unittest.skipIf(ac_tester is None, "_testclinic is missing")
5132+
class VectorcallFunctionalTest(unittest.TestCase):
5133+
"""Runtime tests for @vectorcall exemplar types."""
5134+
5135+
def test_vc_new(self):
5136+
self.assertIsInstance(ac_tester.VcNew(), ac_tester.VcNew)
5137+
self.assertIsInstance(ac_tester.VcNew(1), ac_tester.VcNew)
5138+
self.assertIsInstance(ac_tester.VcNew(a=1), ac_tester.VcNew)
5139+
5140+
def test_vc_new_rejects_extra_args(self):
5141+
with self.assertRaises(TypeError):
5142+
ac_tester.VcNew(1, 2)
5143+
5144+
def test_vc_init(self):
5145+
self.assertIsInstance(ac_tester.VcInit(1), ac_tester.VcInit)
5146+
self.assertIsInstance(ac_tester.VcInit(1, 2), ac_tester.VcInit)
5147+
self.assertIsInstance(ac_tester.VcInit(1, b=2), ac_tester.VcInit)
5148+
5149+
def test_vc_init_missing_required(self):
5150+
with self.assertRaises(TypeError):
5151+
ac_tester.VcInit()
5152+
5153+
def test_vc_init_rejects_a_as_keyword(self):
5154+
# 'a' is positional-only
5155+
with self.assertRaises(TypeError):
5156+
ac_tester.VcInit(a=1)
5157+
5158+
def test_vc_new_base(self):
5159+
self.assertIsInstance(ac_tester.VcNewBase(1), ac_tester.VcNewBase)
5160+
self.assertIsInstance(ac_tester.VcNewBase(1, 2), ac_tester.VcNewBase)
5161+
self.assertIsInstance(ac_tester.VcNewBase(1, b=2), ac_tester.VcNewBase)
5162+
5163+
def test_vc_new_base_missing_required(self):
5164+
with self.assertRaises(TypeError):
5165+
ac_tester.VcNewBase()
5166+
5167+
def test_vc_new_base_subclass(self):
5168+
# tp_vectorcall is not inherited, so the subclass is constructed
5169+
# through tp_new. The generated vectorcall asserts on that, so a
5170+
# debug build aborts here if that ever stops holding.
5171+
Sub = type('Sub', (ac_tester.VcNewBase,), {})
5172+
obj = Sub(1)
5173+
self.assertIsInstance(obj, Sub)
5174+
self.assertIsInstance(obj, ac_tester.VcNewBase)
5175+
5176+
def test_vc_kwonly(self):
5177+
# keyword-only 'b': vectorcall has no kwnames==NULL fast path,
5178+
# so every call goes through the helper.
5179+
self.assertIsInstance(ac_tester.VcKwOnly(1), ac_tester.VcKwOnly)
5180+
self.assertIsInstance(ac_tester.VcKwOnly(1, b=2), ac_tester.VcKwOnly)
5181+
self.assertIsInstance(ac_tester.VcKwOnly(a=1, b=2), ac_tester.VcKwOnly)
5182+
5183+
def test_vc_kwonly_b_as_positional(self):
5184+
with self.assertRaises(TypeError):
5185+
ac_tester.VcKwOnly(1, 2)
5186+
5187+
def test_vc_kwonly_missing_required(self):
5188+
with self.assertRaises(TypeError):
5189+
ac_tester.VcKwOnly()
5190+
5191+
def test_parse_errors_match_slot(self):
5192+
# tp_vectorcall and tp_new/tp_init slot should match in argument parsing
5193+
# error messages. Explicit calls to __new__ and __init__, as well as
5194+
# subtype calls, will not hit the vectorcall slot. Test errors match.
5195+
def error(func, args, kwargs):
5196+
try:
5197+
func(*args, **kwargs)
5198+
except TypeError as exc:
5199+
return str(exc)
5200+
return None
5201+
5202+
def through_new(cls):
5203+
return cls, partial(cls.__new__, cls)
5204+
5205+
def through_init(cls):
5206+
# Not subclassable, and tp_new is PyType_GenericNew, so reach
5207+
# tp_init through the __init__ slot wrapper on an instance.
5208+
return cls, partial(cls.__init__, cls(1))
5209+
5210+
entry_points = [
5211+
through_new(enumerate), # the only non-test @vectorcall function
5212+
through_new(ac_tester.VcNew),
5213+
through_new(ac_tester.VcNewBase),
5214+
through_new(ac_tester.VcKwOnly),
5215+
through_init(ac_tester.VcInit),
5216+
]
5217+
invalid_calls = [
5218+
((), {}), # too few positional arguments
5219+
((1, 2, 3), {}), # too many positional arguments
5220+
((), {'zz': 1}), # unknown keyword argument
5221+
]
5222+
5223+
for direct, slot in entry_points:
5224+
for args, kwargs in invalid_calls:
5225+
with self.subTest(cls=direct, args=args, kwargs=kwargs):
5226+
self.assertEqual(error(direct, args, kwargs),
5227+
error(slot, args, kwargs))
5228+
5229+
50235230
class LimitedCAPIOutputTests(unittest.TestCase):
50245231

50255232
def setUp(self):

Lib/test/test_tuple.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def test_constructors(self):
3838
self.assertEqual(tuple(x for x in range(10) if x % 2),
3939
(1, 3, 5, 7, 9))
4040

41+
def test_too_many_args(self):
42+
with self.assertRaises(TypeError):
43+
tuple([1, 2], 3)
44+
4145
def test_keyword_args(self):
4246
with self.assertRaisesRegex(TypeError, 'keyword argument'):
4347
tuple(sequence=())
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add a ``@vectorcall`` decorator to Argument Clinic to generate :ref:`vectorcall`
2+
parsing code for :func:`object.__init__` and :func:`object.__new__`.

0 commit comments

Comments
 (0)