Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
gh-107704: Argument Clinic: add support for deprecating keyword use o…
…f parameters

It is now possible to deprecate passing keyword arguments for
keyword-or-positional parameters with Argument Clinic, using the new
'/ [from X.Y]' syntax.
(To be read as "positional-only from Python version X.Y")
  • Loading branch information
serhiy-storchaka committed Aug 15, 2023
commit 059cd6a3ea86beb2afa95a414f541781df16acaf
315 changes: 274 additions & 41 deletions Lib/test/test_clinic.py
Comment thread
erlend-aasland marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1627,7 +1627,7 @@ def test_depr_star_invalid_format_1(self):
Docstring.
"""
err = (
"Function 'foo.bar': expected format '* [from major.minor]' "
"Function 'foo.bar': expected format '[from major.minor]' "
"where 'major' and 'minor' are integers; got '3'"
)
self.expect_failure(block, err, lineno=3)
Expand All @@ -1641,7 +1641,7 @@ def test_depr_star_invalid_format_2(self):
Docstring.
"""
err = (
"Function 'foo.bar': expected format '* [from major.minor]' "
"Function 'foo.bar': expected format '[from major.minor]' "
"where 'major' and 'minor' are integers; got 'a.b'"
)
self.expect_failure(block, err, lineno=3)
Expand All @@ -1655,7 +1655,7 @@ def test_depr_star_invalid_format_3(self):
Docstring.
"""
err = (
"Function 'foo.bar': expected format '* [from major.minor]' "
"Function 'foo.bar': expected format '[from major.minor]' "
"where 'major' and 'minor' are integers; got '1.2.3'"
)
self.expect_failure(block, err, lineno=3)
Expand All @@ -1674,6 +1674,22 @@ def test_parameters_required_after_depr_star(self):
)
self.expect_failure(block, err, lineno=4)

def test_parameters_required_after_depr_star2(self):
block = """
module foo
foo.bar
a: int
* [from 3.14]
*
b: int
Docstring.
"""
err = (
"Function 'foo.bar' specifies '* [from ...]' without "
"any parameters afterwards"
)
self.expect_failure(block, err, lineno=4)

def test_depr_star_must_come_before_star(self):
block = """
module foo
Expand All @@ -1697,7 +1713,21 @@ def test_depr_star_duplicate(self):
c: int
Docstring.
"""
err = "Function 'foo.bar' uses '[from ...]' more than once"
err = "Function 'foo.bar' uses '* [from ...]' more than once."
self.expect_failure(block, err, lineno=5)

def test_depr_slash_duplicate(self):
block = """
module foo
foo.bar
a: int
/ [from 3.14]
b: int
/ [from 3.14]
c: int
Docstring.
"""
err = "Function 'bar' uses '/ [from ...]' more than once."
Comment thread
erlend-aasland marked this conversation as resolved.
self.expect_failure(block, err, lineno=5)

def test_single_slash(self):
Expand All @@ -1713,6 +1743,48 @@ def test_single_slash(self):
)
self.expect_failure(block, err)

def test_parameters_required_before_depr_slash(self):
block = """
module foo
foo.bar
/ [from 3.14]
Docstring.
"""
err = (
"Function 'bar' specifies '/ [from ...]' without "
"any parameters beforehead."
Comment thread
erlend-aasland marked this conversation as resolved.
Outdated
)
self.expect_failure(block, err, lineno=2)

def test_parameters_required_before_depr_slash2(self):
block = """
module foo
foo.bar
/
/ [from 3.14]
Docstring.
"""
err = (
"Function 'bar' has an unsupported group configuration. "
"(Unexpected state 0.d)"
Comment thread
erlend-aasland marked this conversation as resolved.
Outdated
)
self.expect_failure(block, err, lineno=2)

def test_parameters_required_before_depr_slash3(self):
block = """
module foo
foo.bar
a: int
/
/ [from 3.14]
Docstring.
"""
err = (
"Function 'bar' specifies '/ [from ...]' without "
"any parameters beforehead."
)
self.expect_failure(block, err, lineno=4)

def test_double_slash(self):
block = """
module foo
Expand Down Expand Up @@ -1741,6 +1813,67 @@ def test_mix_star_and_slash(self):
)
self.expect_failure(block, err)

def test_depr_star_must_come_after_slash(self):
block = """
module foo
foo.bar
a: int
* [from 3.14]
/
b: int
Docstring.
"""
err = (
"Function 'bar' mixes keyword-only and positional-only parameters, "
"which is unsupported."
Comment thread
serhiy-storchaka marked this conversation as resolved.
Outdated
)
self.expect_failure(block, err, lineno=4)

def test_depr_star_must_come_after_depr_slash(self):
block = """
module foo
foo.bar
a: int
* [from 3.14]
/ [from 3.14]
b: int
Docstring.
"""
err = (
"Function 'bar' mixes keyword-only and positional-only parameters, "
"which is unsupported."
)
self.expect_failure(block, err, lineno=4)

def test_star_must_come_after_depr_slash(self):
block = """
module foo
foo.bar
a: int
*
/ [from 3.14]
b: int
Docstring.
"""
err = (
"Function 'bar' mixes keyword-only and positional-only parameters, "
"which is unsupported."
)
self.expect_failure(block, err, lineno=4)

def test_depr_slash_must_come_after_slash(self):
block = """
module foo
foo.bar
a: int
/ [from 3.14]
/
b: int
Docstring.
"""
err = "Function 'foo.bar': '/ [from ...]' must come after '/'"
self.expect_failure(block, err, lineno=4)

def test_parameters_not_permitted_after_slash_for_now(self):
block = """
module foo
Expand Down Expand Up @@ -2537,11 +2670,33 @@ class ClinicFunctionalTest(unittest.TestCase):
locals().update((name, getattr(ac_tester, name))
for name in dir(ac_tester) if name.startswith('test_'))

def check_depr_star(self, pnames, fn, *args, **kwds):
def check_depr_star(self, pnames, fn, *args, name=None, **kwds):
if name is None:
name = fn.__qualname__
if isinstance(fn, type):
name = f'{fn.__module__}.{name}'
regex = (
fr"Passing( more than)?( [0-9]+)? positional argument(s)? to "
fr"{fn.__name__}\(\) is deprecated. Parameter(s)? {pnames} will "
fr"become( a)? keyword-only parameter(s)? in Python 3\.14"
fr"{re.escape(name)}\(\) is deprecated. Parameters? {pnames} will "
fr"become( a)? keyword-only parameters? in Python 3\.14"
)
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
# Record the line number, so we're sure we've got the correct stack
# level on the deprecation warning.
_, lineno = fn(*args, **kwds), sys._getframe().f_lineno
self.assertEqual(cm.filename, __file__)
self.assertEqual(cm.lineno, lineno)

def check_depr_kwd(self, pnames, fn, *args, name=None, **kwds):
if name is None:
name = fn.__qualname__
if isinstance(fn, type):
name = f'{fn.__module__}.{name}'
pl = 's' if ' ' in pnames else ''
regex = (
fr"Passing keyword argument{pl} {pnames} to "
fr"{re.escape(name)}\(\) is deprecated. Corresponding parameter{pl} "
fr"will become positional-only in Python 3\.14."
Comment thread
erlend-aasland marked this conversation as resolved.
)
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
# Record the line number, so we're sure we've got the correct stack
Expand Down Expand Up @@ -3015,46 +3170,40 @@ def test_cloned_func_with_converter_exception_message(self):
self.assertEqual(func(), name)

def test_depr_star_new(self):
regex = re.escape(
"Passing positional arguments to _testclinic.DeprStarNew() is "
"deprecated. Parameter 'a' will become a keyword-only parameter "
"in Python 3.14."
)
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
ac_tester.DeprStarNew(None)
self.assertEqual(cm.filename, __file__)
cls = ac_tester.DeprStarNew
cls(a=None)
self.check_depr_star("'a'", cls, None)
Comment thread
serhiy-storchaka marked this conversation as resolved.
self.assertRaises(TypeError, cls)

def test_depr_star_new_cloned(self):
regex = re.escape(
"Passing positional arguments to _testclinic.DeprStarNew.cloned() "
"is deprecated. Parameter 'a' will become a keyword-only parameter "
"in Python 3.14."
)
obj = ac_tester.DeprStarNew(a=None)
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
obj.cloned(None)
self.assertEqual(cm.filename, __file__)
fn = ac_tester.DeprStarNew(a=None).cloned
fn(a=None)
self.check_depr_star("'a'", fn, None, name='_testclinic.DeprStarNew.cloned')
self.assertRaises(TypeError, fn)

def test_depr_star_init(self):
regex = re.escape(
"Passing positional arguments to _testclinic.DeprStarInit() is "
"deprecated. Parameter 'a' will become a keyword-only parameter "
"in Python 3.14."
)
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
ac_tester.DeprStarInit(None)
self.assertEqual(cm.filename, __file__)
cls = ac_tester.DeprStarInit
cls(a=None)
self.check_depr_star("'a'", cls, None)
self.assertRaises(TypeError, cls)

def test_depr_star_init_cloned(self):
regex = re.escape(
"Passing positional arguments to _testclinic.DeprStarInit.cloned() "
"is deprecated. Parameter 'a' will become a keyword-only parameter "
"in Python 3.14."
)
obj = ac_tester.DeprStarInit(a=None)
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
obj.cloned(None)
self.assertEqual(cm.filename, __file__)
fn = ac_tester.DeprStarInit(a=None).cloned
fn(a=None)
self.check_depr_star("'a'", fn, None, name='_testclinic.DeprStarInit.cloned')
self.assertRaises(TypeError, fn)

def test_depr_kwd_new(self):
cls = ac_tester.DeprKwdNew
cls(None)
self.check_depr_kwd("'a'", cls, a=None)
self.assertRaises(TypeError, cls)

def test_depr_kwd_init(self):
cls = ac_tester.DeprKwdInit
cls(None)
self.check_depr_kwd("'a'", cls, a=None)
self.assertRaises(TypeError, cls)

def test_depr_star_pos0_len1(self):
fn = ac_tester.depr_star_pos0_len1
Expand Down Expand Up @@ -3125,6 +3274,90 @@ def test_depr_star_pos2_len2_with_kwd(self):
check("a", "b", "c", d=0, e=0)
check("a", "b", "c", "d", e=0)

def test_depr_kwd_required_1(self):
fn = ac_tester.depr_kwd_required_1
fn("a", "b")
self.assertRaises(TypeError, fn, "a")
self.assertRaises(TypeError, fn, "a", "b", "c")
check = partial(self.check_depr_kwd, "'b'", fn)
check("a", b="b")
self.assertRaises(TypeError, fn, a="a", b="b")
Comment thread
erlend-aasland marked this conversation as resolved.

def test_depr_kwd_required_2(self):
fn = ac_tester.depr_kwd_required_2
fn("a", "b", "c")
self.assertRaises(TypeError, fn, "a", "b")
self.assertRaises(TypeError, fn, "a", "b", "c", "d")
check = partial(self.check_depr_kwd, "'b' and 'c'", fn)
check("a", "b", c="c")
check("a", b="b", c="c")
self.assertRaises(TypeError, fn, a="a", b="b", c="c")

def test_depr_kwd_optional_1(self):
fn = ac_tester.depr_kwd_optional_1
fn("a")
fn("a", "b")
self.assertRaises(TypeError, fn)
self.assertRaises(TypeError, fn, "a", "b", "c")
check = partial(self.check_depr_kwd, "'b'", fn)
check("a", b="b")
self.assertRaises(TypeError, fn, a="a", b="b")

def test_depr_kwd_optional_2(self):
fn = ac_tester.depr_kwd_optional_2
fn("a")
fn("a", "b")
fn("a", "b", "c")
self.assertRaises(TypeError, fn)
self.assertRaises(TypeError, fn, "a", "b", "c", "d")
check = partial(self.check_depr_kwd, "'b' and 'c'", fn)
check("a", b="b")
check("a", c="c")
check("a", b="b", c="c")
check("a", c="c", b="b")
check("a", "b", c="c")
self.assertRaises(TypeError, fn, a="a", b="b", c="c")

def test_depr_kwd_optional_3(self):
fn = ac_tester.depr_kwd_optional_3
fn()
fn("a")
fn("a", "b")
fn("a", "b", "c")
self.assertRaises(TypeError, fn, "a", "b", "c", "d")
check = partial(self.check_depr_kwd, "'a', 'b' and 'c'", fn)
check("a", "b", c="c")
check("a", b="b")
check(a="a")

def test_depr_kwd_required_optional(self):
fn = ac_tester.depr_kwd_required_optional
fn("a", "b")
fn("a", "b", "c")
self.assertRaises(TypeError, fn)
self.assertRaises(TypeError, fn, "a")
self.assertRaises(TypeError, fn, "a", "b", "c", "d")
check = partial(self.check_depr_kwd, "'b' and 'c'", fn)
check("a", b="b")
check("a", b="b", c="c")
check("a", c="c", b="b")
check("a", "b", c="c")
self.assertRaises(TypeError, fn, "a", c="c")
self.assertRaises(TypeError, fn, a="a", b="b", c="c")

def test_depr_kwd_noinline(self):
fn = ac_tester.depr_kwd_noinline
fn("a", "b")
fn("a", "b", "c")
self.assertRaises(TypeError, fn, "a")
check = partial(self.check_depr_kwd, "'b' and 'c'", fn)
check("a", b="b")
check("a", b="b", c="c")
check("a", c="c", b="b")
check("a", "b", c="c")
self.assertRaises(TypeError, fn, "a", c="c")
self.assertRaises(TypeError, fn, a="a", b="b", c="c")


class PermutationTests(unittest.TestCase):
"""Test permutation support functions."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
It is now possible to deprecate passing keyword arguments for
keyword-or-positional parameters with Argument Clinic, using the new ``/
[from X.Y]`` syntax. (To be read as *"positional-only from Python version
X.Y"*.) See :ref:`clinic-howto-deprecate-keyword` for more information.
Loading