Skip to content

Commit 01399a3

Browse files
gh-108271, gh-108270: Argument Clinic: parameter aliases and deprecation (GH-155248)
A parameter can be given an alternative name by declaring a keyword-only parameter with a default value which shares the C name of a preceding one: a: object = None * b as a: object = None Only one of the alternative names can be used in a call; passing both is a TypeError. An alias is not shown in the signature. The `[until X.Y]` prefix marks a parameter which will be removed in that release. Passing it emits a DeprecationWarning, and the generated code warns at compile time when that release is reached. A deprecated parameter must have a default value, and only the last positional-only parameters can be deprecated, because removing one would leave no way to pass those which follow it.
1 parent ce32912 commit 01399a3

11 files changed

Lines changed: 782 additions & 13 deletions

File tree

Lib/test/test_clinic.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import re
1616
import sys
1717
import unittest
18+
import warnings
1819

1920
test_tools.skip_if_missing('clinic')
2021
with test_tools.imports_under_tool('clinic'):
@@ -2305,6 +2306,128 @@ def test_depr_slash_duplicate2(self):
23052306
err = "Function 'bar': '/ [from 3.14]' must precede '/ [from 3.15]'"
23062307
self.expect_failure(block, err, lineno=5)
23072308

2309+
def test_alias(self):
2310+
function = self.parse_function("""
2311+
module foo
2312+
foo.bar
2313+
a: int
2314+
*
2315+
b as a: int = 0
2316+
Docstring.
2317+
""")
2318+
_, a, b = function.parameters.values()
2319+
self.assertIsNone(a.converter.alias_of)
2320+
self.assertIs(b.converter.alias_of, a)
2321+
self.assertEqual(function.docstring.splitlines()[0],
2322+
"bar($module, /, a)")
2323+
2324+
def test_alias_must_be_keyword_only(self):
2325+
block = """
2326+
module foo
2327+
foo.bar
2328+
a: int
2329+
b as a: int = 0
2330+
Docstring.
2331+
"""
2332+
err = "Alias 'b' of the parameter 'a' must be keyword-only."
2333+
self.expect_failure(block, err, lineno=3)
2334+
2335+
def test_alias_must_have_default(self):
2336+
block = """
2337+
module foo
2338+
foo.bar
2339+
a: int
2340+
*
2341+
b as a: int
2342+
Docstring.
2343+
"""
2344+
err = "Alias 'b' of the parameter 'a' must have a default value."
2345+
self.expect_failure(block, err, lineno=4)
2346+
2347+
def test_alias_deprecated(self):
2348+
function = self.parse_function("""
2349+
module foo
2350+
foo.bar
2351+
a: int
2352+
*
2353+
[until 3.14] b as a: int = 0
2354+
Docstring.
2355+
""")
2356+
_, a, b = function.parameters.values()
2357+
self.assertIsNone(a.deprecated_until)
2358+
self.assertEqual(b.deprecated_until, (3, 14))
2359+
2360+
def test_deprecated_last_positional_only_parameters(self):
2361+
function = self.parse_function("""
2362+
module foo
2363+
foo.bar
2364+
a: int = 0
2365+
[until 3.14] b: int = 0
2366+
[until 3.14] c: int = 0
2367+
/
2368+
d: int = 0
2369+
Docstring.
2370+
""")
2371+
_, a, b, c, d = function.parameters.values()
2372+
self.assertIsNone(a.deprecated_until)
2373+
self.assertEqual(b.deprecated_until, (3, 14))
2374+
self.assertEqual(c.deprecated_until, (3, 14))
2375+
self.assertIsNone(d.deprecated_until)
2376+
2377+
def test_deprecated_non_last_positional_only_parameter(self):
2378+
block = """
2379+
module foo
2380+
foo.bar
2381+
[until 3.14] a: int = 0
2382+
b: int = 0
2383+
/
2384+
Docstring.
2385+
"""
2386+
err = ("Parameter 'b' cannot follow the deprecated parameter 'a': "
2387+
"only the last positional-only parameters can be deprecated.")
2388+
self.expect_failure(block, err, lineno=4)
2389+
2390+
def test_deprecated_non_positional_only_parameters(self):
2391+
# The following parameters can still be passed by keyword.
2392+
function = self.parse_function("""
2393+
module foo
2394+
foo.bar
2395+
[until 3.14] a: int = 0
2396+
b: int = 0
2397+
*
2398+
[until 3.14] c: int = 0
2399+
d: int = 0
2400+
Docstring.
2401+
""")
2402+
_, a, b, c, d = function.parameters.values()
2403+
self.assertEqual(a.deprecated_until, (3, 14))
2404+
self.assertIsNone(b.deprecated_until)
2405+
self.assertEqual(c.deprecated_until, (3, 14))
2406+
self.assertIsNone(d.deprecated_until)
2407+
2408+
def test_deprecated_parameter_without_default(self):
2409+
block = """
2410+
module foo
2411+
foo.bar
2412+
[until 3.14] a: int
2413+
Docstring.
2414+
"""
2415+
err = "Deprecated parameter 'a' must have a default value."
2416+
self.expect_failure(block, err, lineno=2)
2417+
2418+
def test_deprecated_invalid_format(self):
2419+
block = """
2420+
module foo
2421+
foo.bar
2422+
[until 3] a: int = 0
2423+
Docstring.
2424+
"""
2425+
err = (
2426+
"Function 'bar': expected format '[until major.minor]' "
2427+
"where 'major' and 'minor' are integers; got '3'"
2428+
)
2429+
self.expect_failure(block, err, lineno=2)
2430+
23082431
def test_single_slash(self):
23092432
block = """
23102433
module foo
@@ -5072,6 +5195,58 @@ def test_depr_multi(self):
50725195
check("a", b="b", c="c", d="d", e="e", f="f", g="g")
50735196
self.assertRaises(TypeError, fn, a="a", b="b", c="c", d="d", e="e", f="f", g="g")
50745197

5198+
def test_alias_pos(self):
5199+
fn = ac_tester.alias_pos
5200+
self.assertIsNone(fn())
5201+
self.assertEqual(fn(1), 1)
5202+
self.assertEqual(fn(a=1), 1)
5203+
self.assertEqual(fn(b=1), 1)
5204+
self.assertEqual(fn.__text_signature__, "($module, /, a=None)")
5205+
errmsg = re.escape(
5206+
"argument for alias_pos() given by name ('b') and position (1)")
5207+
self.assertRaisesRegex(TypeError, errmsg, fn, 1, b=2)
5208+
errmsg = re.escape(
5209+
"argument for alias_pos() given by name ('b') and name ('a')")
5210+
self.assertRaisesRegex(TypeError, errmsg, fn, a=1, b=2)
5211+
5212+
def test_alias_kwonly(self):
5213+
fn = ac_tester.alias_kwonly
5214+
self.assertIsNone(fn())
5215+
self.assertEqual(fn(a=1), 1)
5216+
self.assertEqual(fn(b=1), 1)
5217+
self.assertEqual(fn.__text_signature__, "($module, /, *, a=None)")
5218+
self.assertRaises(TypeError, fn, 1)
5219+
errmsg = re.escape(
5220+
"argument for alias_kwonly() given by name ('b') and name ('a')")
5221+
self.assertRaisesRegex(TypeError, errmsg, fn, a=1, b=2)
5222+
5223+
def test_depr_alias(self):
5224+
fn = ac_tester.depr_alias
5225+
self.assertEqual(fn(1), 1)
5226+
self.assertEqual(fn(a=1), 1)
5227+
errmsg = ("Passing the argument 'b' to depr_alias() is deprecated. "
5228+
"Use 'a' instead. It will be removed in Python 3.14.")
5229+
self.check_depr(re.escape(errmsg), fn, b=1)
5230+
5231+
def test_depr_param(self):
5232+
fn = ac_tester.depr_param
5233+
self.assertEqual(fn(), (None, None, None, None))
5234+
self.assertEqual(fn(1), (1, None, None, None))
5235+
def errmsg(name):
5236+
return re.escape(f"Passing the argument {name!r} to depr_param() "
5237+
f"is deprecated. "
5238+
f"It will be removed in Python 3.14.")
5239+
self.check_depr(errmsg('b'), fn, 1, 2)
5240+
self.check_depr(errmsg('d'), fn, 1, d=4)
5241+
# Each deprecated parameter is reported on its own.
5242+
with warnings.catch_warnings(record=True) as caught:
5243+
warnings.simplefilter("always")
5244+
self.assertEqual(fn(1, 2, 3), (1, 2, 3, None))
5245+
self.assertEqual(len(caught), 2)
5246+
for warning, name in zip(caught, 'bc'):
5247+
self.assertIs(warning.category, DeprecationWarning)
5248+
self.assertRegex(str(warning.message), errmsg(name))
5249+
50755250
def test_lone_kwds(self):
50765251
with self.assertRaises(TypeError):
50775252
ac_tester.lone_kwds(1, 2)
@@ -5266,6 +5441,26 @@ def test_limited_capi_double(self):
52665441
self.assertIn("double f;", generated)
52675442
self.assertIn("f = PyFloat_AsDouble", generated)
52685443

5444+
def test_limited_capi_alias(self):
5445+
block = self.wrap_clinic_input("""
5446+
func
5447+
a: object = None
5448+
*
5449+
b as a: object = None
5450+
""")
5451+
err = ("Parameter 'b' cannot be an alias: "
5452+
"the arguments are not parsed one by one.")
5453+
_expect_failure(self, self.clinic.parse, block, err)
5454+
5455+
def test_limited_capi_deprecated(self):
5456+
block = self.wrap_clinic_input("""
5457+
func
5458+
[until 3.14] a: object = None
5459+
""")
5460+
err = ("Parameter 'a' cannot be deprecated: "
5461+
"the arguments are not parsed one by one.")
5462+
_expect_failure(self, self.clinic.parse, block, err)
5463+
52695464

52705465
try:
52715466
import _testclinic_limited
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Argument Clinic: add support for parameter aliases.
2+
A keyword-only parameter with a default value which shares the C name of a
3+
preceding parameter declares an alternative name for it.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Argument Clinic: add support for deprecating a parameter with the ``[until
2+
X.Y]`` marker.
3+
Passing such argument emits a :exc:`DeprecationWarning`.

Modules/_testclinic.c

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,40 @@ clone_with_conv_f2_impl(PyObject *module, custom_t path)
15231523
}
15241524

15251525

1526+
/*[clinic input]
1527+
alias_pos
1528+
1529+
a: object = None
1530+
*
1531+
b as a: object = None
1532+
1533+
[clinic start generated code]*/
1534+
1535+
static PyObject *
1536+
alias_pos_impl(PyObject *module, PyObject *a)
1537+
/*[clinic end generated code: output=f6cd3c7f098a894d input=8018ee6c26e3f435]*/
1538+
{
1539+
return Py_NewRef(a);
1540+
}
1541+
1542+
1543+
/*[clinic input]
1544+
alias_kwonly
1545+
1546+
*
1547+
a: object = None
1548+
b as a: object = None
1549+
1550+
[clinic start generated code]*/
1551+
1552+
static PyObject *
1553+
alias_kwonly_impl(PyObject *module, PyObject *a)
1554+
/*[clinic end generated code: output=9a6d4202ba972f46 input=8ad2d6c0f326571d]*/
1555+
{
1556+
return Py_NewRef(a);
1557+
}
1558+
1559+
15261560
/*[clinic input]
15271561
class _testclinic.TestClass "PyObject *" "&PyBaseObject_Type"
15281562
[clinic start generated code]*/
@@ -2399,6 +2433,40 @@ depr_kwd_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
23992433
}
24002434

24012435

2436+
/*[clinic input]
2437+
depr_alias
2438+
a: object = None
2439+
*
2440+
[until 3.14] b as a: object = None
2441+
[clinic start generated code]*/
2442+
2443+
static PyObject *
2444+
depr_alias_impl(PyObject *module, PyObject *a)
2445+
/*[clinic end generated code: output=85e89838716d9423 input=92efd3f244c2ec3f]*/
2446+
{
2447+
return Py_NewRef(a);
2448+
}
2449+
2450+
2451+
/*[clinic input]
2452+
depr_param
2453+
a: object = None
2454+
[until 3.14] b: object = None
2455+
[until 3.14] c: object = None
2456+
/
2457+
*
2458+
[until 3.14] d: object = None
2459+
[clinic start generated code]*/
2460+
2461+
static PyObject *
2462+
depr_param_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
2463+
PyObject *d)
2464+
/*[clinic end generated code: output=5a42b461851c467b input=f689a85166408359]*/
2465+
{
2466+
return pack_arguments_newref(4, a, b, c, d);
2467+
}
2468+
2469+
24022470
/*[clinic input]
24032471
depr_multi
24042472
a: object
@@ -2736,6 +2804,9 @@ static PyMethodDef tester_methods[] = {
27362804
CLONE_WITH_CONV_F1_METHODDEF
27372805
CLONE_WITH_CONV_F2_METHODDEF
27382806

2807+
ALIAS_POS_METHODDEF
2808+
ALIAS_KWONLY_METHODDEF
2809+
27392810
DEPR_STAR_POS0_LEN1_METHODDEF
27402811
DEPR_STAR_POS0_LEN2_METHODDEF
27412812
DEPR_STAR_POS0_LEN3_WITH_KWD_METHODDEF
@@ -2756,6 +2827,8 @@ static PyMethodDef tester_methods[] = {
27562827
DEPR_KWD_NOINLINE_METHODDEF
27572828
DEPR_KWD_MULTI_METHODDEF
27582829
DEPR_MULTI_METHODDEF
2830+
DEPR_ALIAS_METHODDEF
2831+
DEPR_PARAM_METHODDEF
27592832

27602833
LONE_KWDS_METHODDEF
27612834
KWDS_WITH_POS_ONLY_METHODDEF

0 commit comments

Comments
 (0)