Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import re
import sys
import unittest
import warnings

test_tools.skip_if_missing('clinic')
with test_tools.imports_under_tool('clinic'):
Expand Down Expand Up @@ -2207,6 +2208,128 @@ def test_depr_slash_duplicate2(self):
err = "Function 'bar': '/ [from 3.14]' must precede '/ [from 3.15]'"
self.expect_failure(block, err, lineno=5)

def test_alias(self):
function = self.parse_function("""
module foo
foo.bar
a: int
*
b as a: int = 0
Docstring.
""")
_, a, b = function.parameters.values()
self.assertIsNone(a.converter.alias_of)
self.assertIs(b.converter.alias_of, a)
self.assertEqual(function.docstring.splitlines()[0],
"bar($module, /, a)")

def test_alias_must_be_keyword_only(self):
block = """
module foo
foo.bar
a: int
b as a: int = 0
Docstring.
"""
err = "Alias 'b' of the parameter 'a' must be keyword-only."
self.expect_failure(block, err, lineno=3)

def test_alias_must_have_default(self):
block = """
module foo
foo.bar
a: int
*
b as a: int
Docstring.
"""
err = "Alias 'b' of the parameter 'a' must have a default value."
self.expect_failure(block, err, lineno=4)

def test_alias_deprecated(self):
function = self.parse_function("""
module foo
foo.bar
a: int
*
[until 3.14] b as a: int = 0
Docstring.
""")
_, a, b = function.parameters.values()
self.assertIsNone(a.deprecated_until)
self.assertEqual(b.deprecated_until, (3, 14))

def test_deprecated_last_positional_only_parameters(self):
function = self.parse_function("""
module foo
foo.bar
a: int = 0
[until 3.14] b: int = 0
[until 3.14] c: int = 0
/
d: int = 0
Docstring.
""")
_, a, b, c, d = function.parameters.values()
self.assertIsNone(a.deprecated_until)
self.assertEqual(b.deprecated_until, (3, 14))
self.assertEqual(c.deprecated_until, (3, 14))
self.assertIsNone(d.deprecated_until)

def test_deprecated_non_last_positional_only_parameter(self):
block = """
module foo
foo.bar
[until 3.14] a: int = 0
b: int = 0
/
Docstring.
"""
err = ("Parameter 'b' cannot follow the deprecated parameter 'a': "
"only the last positional-only parameters can be deprecated.")
self.expect_failure(block, err, lineno=4)

def test_deprecated_non_positional_only_parameters(self):
# The following parameters can still be passed by keyword.
function = self.parse_function("""
module foo
foo.bar
[until 3.14] a: int = 0
b: int = 0
*
[until 3.14] c: int = 0
d: int = 0
Docstring.
""")
_, a, b, c, d = function.parameters.values()
self.assertEqual(a.deprecated_until, (3, 14))
self.assertIsNone(b.deprecated_until)
self.assertEqual(c.deprecated_until, (3, 14))
self.assertIsNone(d.deprecated_until)

def test_deprecated_parameter_without_default(self):
block = """
module foo
foo.bar
[until 3.14] a: int
Docstring.
"""
err = "Deprecated parameter 'a' must have a default value."
self.expect_failure(block, err, lineno=2)

def test_deprecated_invalid_format(self):
block = """
module foo
foo.bar
[until 3] a: int = 0
Docstring.
"""
err = (
"Function 'bar': expected format '[until major.minor]' "
"where 'major' and 'minor' are integers; got '3'"
)
self.expect_failure(block, err, lineno=2)

def test_single_slash(self):
block = """
module foo
Expand Down Expand Up @@ -4584,6 +4707,58 @@ def test_depr_multi(self):
check("a", b="b", c="c", d="d", e="e", f="f", g="g")
self.assertRaises(TypeError, fn, a="a", b="b", c="c", d="d", e="e", f="f", g="g")

def test_alias_pos(self):
fn = ac_tester.alias_pos
self.assertIsNone(fn())
self.assertEqual(fn(1), 1)
self.assertEqual(fn(a=1), 1)
self.assertEqual(fn(b=1), 1)
self.assertEqual(fn.__text_signature__, "($module, /, a=None)")
errmsg = re.escape(
"argument for alias_pos() given by name ('b') and position (1)")
self.assertRaisesRegex(TypeError, errmsg, fn, 1, b=2)
errmsg = re.escape(
"argument for alias_pos() given by name ('b') and name ('a')")
self.assertRaisesRegex(TypeError, errmsg, fn, a=1, b=2)

def test_alias_kwonly(self):
fn = ac_tester.alias_kwonly
self.assertIsNone(fn())
self.assertEqual(fn(a=1), 1)
self.assertEqual(fn(b=1), 1)
self.assertEqual(fn.__text_signature__, "($module, /, *, a=None)")
self.assertRaises(TypeError, fn, 1)
errmsg = re.escape(
"argument for alias_kwonly() given by name ('b') and name ('a')")
self.assertRaisesRegex(TypeError, errmsg, fn, a=1, b=2)

def test_depr_alias(self):
fn = ac_tester.depr_alias
self.assertEqual(fn(1), 1)
self.assertEqual(fn(a=1), 1)
errmsg = ("Passing the argument 'b' to depr_alias() is deprecated. "
"Use 'a' instead. It will be removed in Python 3.14.")
self.check_depr(re.escape(errmsg), fn, b=1)

def test_depr_param(self):
fn = ac_tester.depr_param
self.assertEqual(fn(), (None, None, None, None))
self.assertEqual(fn(1), (1, None, None, None))
def errmsg(name):
return re.escape(f"Passing the argument {name!r} to depr_param() "
f"is deprecated. "
f"It will be removed in Python 3.14.")
self.check_depr(errmsg('b'), fn, 1, 2)
self.check_depr(errmsg('d'), fn, 1, d=4)
# Each deprecated parameter is reported on its own.
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
self.assertEqual(fn(1, 2, 3), (1, 2, 3, None))
self.assertEqual(len(caught), 2)
for warning, name in zip(caught, 'bc'):
self.assertIs(warning.category, DeprecationWarning)
self.assertRegex(str(warning.message), errmsg(name))

def test_lone_kwds(self):
with self.assertRaises(TypeError):
ac_tester.lone_kwds(1, 2)
Expand Down Expand Up @@ -4667,6 +4842,26 @@ def test_limited_capi_double(self):
self.assertIn("double f;", generated)
self.assertIn("f = PyFloat_AsDouble", generated)

def test_limited_capi_alias(self):
block = self.wrap_clinic_input("""
func
a: object = None
*
b as a: object = None
""")
err = ("Parameter 'b' cannot be an alias: "
"the arguments are not parsed one by one.")
_expect_failure(self, self.clinic.parse, block, err)

def test_limited_capi_deprecated(self):
block = self.wrap_clinic_input("""
func
[until 3.14] a: object = None
""")
err = ("Parameter 'a' cannot be deprecated: "
"the arguments are not parsed one by one.")
_expect_failure(self, self.clinic.parse, block, err)


try:
import _testclinic_limited
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Argument Clinic: add support for parameter aliases.
A keyword-only parameter with a default value which shares the C name of a
preceding parameter declares an alternative name for it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Argument Clinic: add support for deprecating a parameter with the ``[until
X.Y]`` marker.
Passing such argument emits a :exc:`DeprecationWarning`.
73 changes: 73 additions & 0 deletions Modules/_testclinic.c
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,40 @@ clone_with_conv_f2_impl(PyObject *module, custom_t path)
}


/*[clinic input]
alias_pos

a: object = None
*
b as a: object = None

[clinic start generated code]*/

static PyObject *
alias_pos_impl(PyObject *module, PyObject *a)
/*[clinic end generated code: output=f6cd3c7f098a894d input=8018ee6c26e3f435]*/
{
return Py_NewRef(a);
}


/*[clinic input]
alias_kwonly

*
a: object = None
b as a: object = None

[clinic start generated code]*/

static PyObject *
alias_kwonly_impl(PyObject *module, PyObject *a)
/*[clinic end generated code: output=9a6d4202ba972f46 input=8ad2d6c0f326571d]*/
{
return Py_NewRef(a);
}


/*[clinic input]
class _testclinic.TestClass "PyObject *" "&PyBaseObject_Type"
[clinic start generated code]*/
Expand Down Expand Up @@ -2375,6 +2409,40 @@ depr_kwd_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
}


/*[clinic input]
depr_alias
a: object = None
*
[until 3.14] b as a: object = None
[clinic start generated code]*/

static PyObject *
depr_alias_impl(PyObject *module, PyObject *a)
/*[clinic end generated code: output=85e89838716d9423 input=92efd3f244c2ec3f]*/
{
return Py_NewRef(a);
}


/*[clinic input]
depr_param
a: object = None
[until 3.14] b: object = None
[until 3.14] c: object = None
/
*
[until 3.14] d: object = None
[clinic start generated code]*/

static PyObject *
depr_param_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
PyObject *d)
/*[clinic end generated code: output=5a42b461851c467b input=f689a85166408359]*/
{
return pack_arguments_newref(4, a, b, c, d);
}


/*[clinic input]
depr_multi
a: object
Expand Down Expand Up @@ -2569,6 +2637,9 @@ static PyMethodDef tester_methods[] = {
CLONE_WITH_CONV_F1_METHODDEF
CLONE_WITH_CONV_F2_METHODDEF

ALIAS_POS_METHODDEF
ALIAS_KWONLY_METHODDEF

DEPR_STAR_POS0_LEN1_METHODDEF
DEPR_STAR_POS0_LEN2_METHODDEF
DEPR_STAR_POS0_LEN3_WITH_KWD_METHODDEF
Expand All @@ -2589,6 +2660,8 @@ static PyMethodDef tester_methods[] = {
DEPR_KWD_NOINLINE_METHODDEF
DEPR_KWD_MULTI_METHODDEF
DEPR_MULTI_METHODDEF
DEPR_ALIAS_METHODDEF
DEPR_PARAM_METHODDEF

LONE_KWDS_METHODDEF
KWDS_WITH_POS_ONLY_METHODDEF
Expand Down
Loading
Loading