Skip to content

Commit 059cd6a

Browse files
gh-107704: Argument Clinic: add support for deprecating keyword use of 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")
1 parent 34e1917 commit 059cd6a

File tree

6 files changed

+1672
-400
lines changed

6 files changed

+1672
-400
lines changed

Lib/test/test_clinic.py

+274-41
Original file line numberDiff line numberDiff line change
@@ -1627,7 +1627,7 @@ def test_depr_star_invalid_format_1(self):
16271627
Docstring.
16281628
"""
16291629
err = (
1630-
"Function 'foo.bar': expected format '* [from major.minor]' "
1630+
"Function 'foo.bar': expected format '[from major.minor]' "
16311631
"where 'major' and 'minor' are integers; got '3'"
16321632
)
16331633
self.expect_failure(block, err, lineno=3)
@@ -1641,7 +1641,7 @@ def test_depr_star_invalid_format_2(self):
16411641
Docstring.
16421642
"""
16431643
err = (
1644-
"Function 'foo.bar': expected format '* [from major.minor]' "
1644+
"Function 'foo.bar': expected format '[from major.minor]' "
16451645
"where 'major' and 'minor' are integers; got 'a.b'"
16461646
)
16471647
self.expect_failure(block, err, lineno=3)
@@ -1655,7 +1655,7 @@ def test_depr_star_invalid_format_3(self):
16551655
Docstring.
16561656
"""
16571657
err = (
1658-
"Function 'foo.bar': expected format '* [from major.minor]' "
1658+
"Function 'foo.bar': expected format '[from major.minor]' "
16591659
"where 'major' and 'minor' are integers; got '1.2.3'"
16601660
)
16611661
self.expect_failure(block, err, lineno=3)
@@ -1674,6 +1674,22 @@ def test_parameters_required_after_depr_star(self):
16741674
)
16751675
self.expect_failure(block, err, lineno=4)
16761676

1677+
def test_parameters_required_after_depr_star2(self):
1678+
block = """
1679+
module foo
1680+
foo.bar
1681+
a: int
1682+
* [from 3.14]
1683+
*
1684+
b: int
1685+
Docstring.
1686+
"""
1687+
err = (
1688+
"Function 'foo.bar' specifies '* [from ...]' without "
1689+
"any parameters afterwards"
1690+
)
1691+
self.expect_failure(block, err, lineno=4)
1692+
16771693
def test_depr_star_must_come_before_star(self):
16781694
block = """
16791695
module foo
@@ -1697,7 +1713,21 @@ def test_depr_star_duplicate(self):
16971713
c: int
16981714
Docstring.
16991715
"""
1700-
err = "Function 'foo.bar' uses '[from ...]' more than once"
1716+
err = "Function 'foo.bar' uses '* [from ...]' more than once."
1717+
self.expect_failure(block, err, lineno=5)
1718+
1719+
def test_depr_slash_duplicate(self):
1720+
block = """
1721+
module foo
1722+
foo.bar
1723+
a: int
1724+
/ [from 3.14]
1725+
b: int
1726+
/ [from 3.14]
1727+
c: int
1728+
Docstring.
1729+
"""
1730+
err = "Function 'bar' uses '/ [from ...]' more than once."
17011731
self.expect_failure(block, err, lineno=5)
17021732

17031733
def test_single_slash(self):
@@ -1713,6 +1743,48 @@ def test_single_slash(self):
17131743
)
17141744
self.expect_failure(block, err)
17151745

1746+
def test_parameters_required_before_depr_slash(self):
1747+
block = """
1748+
module foo
1749+
foo.bar
1750+
/ [from 3.14]
1751+
Docstring.
1752+
"""
1753+
err = (
1754+
"Function 'bar' specifies '/ [from ...]' without "
1755+
"any parameters beforehead."
1756+
)
1757+
self.expect_failure(block, err, lineno=2)
1758+
1759+
def test_parameters_required_before_depr_slash2(self):
1760+
block = """
1761+
module foo
1762+
foo.bar
1763+
/
1764+
/ [from 3.14]
1765+
Docstring.
1766+
"""
1767+
err = (
1768+
"Function 'bar' has an unsupported group configuration. "
1769+
"(Unexpected state 0.d)"
1770+
)
1771+
self.expect_failure(block, err, lineno=2)
1772+
1773+
def test_parameters_required_before_depr_slash3(self):
1774+
block = """
1775+
module foo
1776+
foo.bar
1777+
a: int
1778+
/
1779+
/ [from 3.14]
1780+
Docstring.
1781+
"""
1782+
err = (
1783+
"Function 'bar' specifies '/ [from ...]' without "
1784+
"any parameters beforehead."
1785+
)
1786+
self.expect_failure(block, err, lineno=4)
1787+
17161788
def test_double_slash(self):
17171789
block = """
17181790
module foo
@@ -1741,6 +1813,67 @@ def test_mix_star_and_slash(self):
17411813
)
17421814
self.expect_failure(block, err)
17431815

1816+
def test_depr_star_must_come_after_slash(self):
1817+
block = """
1818+
module foo
1819+
foo.bar
1820+
a: int
1821+
* [from 3.14]
1822+
/
1823+
b: int
1824+
Docstring.
1825+
"""
1826+
err = (
1827+
"Function 'bar' mixes keyword-only and positional-only parameters, "
1828+
"which is unsupported."
1829+
)
1830+
self.expect_failure(block, err, lineno=4)
1831+
1832+
def test_depr_star_must_come_after_depr_slash(self):
1833+
block = """
1834+
module foo
1835+
foo.bar
1836+
a: int
1837+
* [from 3.14]
1838+
/ [from 3.14]
1839+
b: int
1840+
Docstring.
1841+
"""
1842+
err = (
1843+
"Function 'bar' mixes keyword-only and positional-only parameters, "
1844+
"which is unsupported."
1845+
)
1846+
self.expect_failure(block, err, lineno=4)
1847+
1848+
def test_star_must_come_after_depr_slash(self):
1849+
block = """
1850+
module foo
1851+
foo.bar
1852+
a: int
1853+
*
1854+
/ [from 3.14]
1855+
b: int
1856+
Docstring.
1857+
"""
1858+
err = (
1859+
"Function 'bar' mixes keyword-only and positional-only parameters, "
1860+
"which is unsupported."
1861+
)
1862+
self.expect_failure(block, err, lineno=4)
1863+
1864+
def test_depr_slash_must_come_after_slash(self):
1865+
block = """
1866+
module foo
1867+
foo.bar
1868+
a: int
1869+
/ [from 3.14]
1870+
/
1871+
b: int
1872+
Docstring.
1873+
"""
1874+
err = "Function 'foo.bar': '/ [from ...]' must come after '/'"
1875+
self.expect_failure(block, err, lineno=4)
1876+
17441877
def test_parameters_not_permitted_after_slash_for_now(self):
17451878
block = """
17461879
module foo
@@ -2537,11 +2670,33 @@ class ClinicFunctionalTest(unittest.TestCase):
25372670
locals().update((name, getattr(ac_tester, name))
25382671
for name in dir(ac_tester) if name.startswith('test_'))
25392672

2540-
def check_depr_star(self, pnames, fn, *args, **kwds):
2673+
def check_depr_star(self, pnames, fn, *args, name=None, **kwds):
2674+
if name is None:
2675+
name = fn.__qualname__
2676+
if isinstance(fn, type):
2677+
name = f'{fn.__module__}.{name}'
25412678
regex = (
25422679
fr"Passing( more than)?( [0-9]+)? positional argument(s)? to "
2543-
fr"{fn.__name__}\(\) is deprecated. Parameter(s)? {pnames} will "
2544-
fr"become( a)? keyword-only parameter(s)? in Python 3\.14"
2680+
fr"{re.escape(name)}\(\) is deprecated. Parameters? {pnames} will "
2681+
fr"become( a)? keyword-only parameters? in Python 3\.14"
2682+
)
2683+
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
2684+
# Record the line number, so we're sure we've got the correct stack
2685+
# level on the deprecation warning.
2686+
_, lineno = fn(*args, **kwds), sys._getframe().f_lineno
2687+
self.assertEqual(cm.filename, __file__)
2688+
self.assertEqual(cm.lineno, lineno)
2689+
2690+
def check_depr_kwd(self, pnames, fn, *args, name=None, **kwds):
2691+
if name is None:
2692+
name = fn.__qualname__
2693+
if isinstance(fn, type):
2694+
name = f'{fn.__module__}.{name}'
2695+
pl = 's' if ' ' in pnames else ''
2696+
regex = (
2697+
fr"Passing keyword argument{pl} {pnames} to "
2698+
fr"{re.escape(name)}\(\) is deprecated. Corresponding parameter{pl} "
2699+
fr"will become positional-only in Python 3\.14."
25452700
)
25462701
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
25472702
# Record the line number, so we're sure we've got the correct stack
@@ -3015,46 +3170,40 @@ def test_cloned_func_with_converter_exception_message(self):
30153170
self.assertEqual(func(), name)
30163171

30173172
def test_depr_star_new(self):
3018-
regex = re.escape(
3019-
"Passing positional arguments to _testclinic.DeprStarNew() is "
3020-
"deprecated. Parameter 'a' will become a keyword-only parameter "
3021-
"in Python 3.14."
3022-
)
3023-
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
3024-
ac_tester.DeprStarNew(None)
3025-
self.assertEqual(cm.filename, __file__)
3173+
cls = ac_tester.DeprStarNew
3174+
cls(a=None)
3175+
self.check_depr_star("'a'", cls, None)
3176+
self.assertRaises(TypeError, cls)
30263177

30273178
def test_depr_star_new_cloned(self):
3028-
regex = re.escape(
3029-
"Passing positional arguments to _testclinic.DeprStarNew.cloned() "
3030-
"is deprecated. Parameter 'a' will become a keyword-only parameter "
3031-
"in Python 3.14."
3032-
)
3033-
obj = ac_tester.DeprStarNew(a=None)
3034-
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
3035-
obj.cloned(None)
3036-
self.assertEqual(cm.filename, __file__)
3179+
fn = ac_tester.DeprStarNew(a=None).cloned
3180+
fn(a=None)
3181+
self.check_depr_star("'a'", fn, None, name='_testclinic.DeprStarNew.cloned')
3182+
self.assertRaises(TypeError, fn)
30373183

30383184
def test_depr_star_init(self):
3039-
regex = re.escape(
3040-
"Passing positional arguments to _testclinic.DeprStarInit() is "
3041-
"deprecated. Parameter 'a' will become a keyword-only parameter "
3042-
"in Python 3.14."
3043-
)
3044-
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
3045-
ac_tester.DeprStarInit(None)
3046-
self.assertEqual(cm.filename, __file__)
3185+
cls = ac_tester.DeprStarInit
3186+
cls(a=None)
3187+
self.check_depr_star("'a'", cls, None)
3188+
self.assertRaises(TypeError, cls)
30473189

30483190
def test_depr_star_init_cloned(self):
3049-
regex = re.escape(
3050-
"Passing positional arguments to _testclinic.DeprStarInit.cloned() "
3051-
"is deprecated. Parameter 'a' will become a keyword-only parameter "
3052-
"in Python 3.14."
3053-
)
3054-
obj = ac_tester.DeprStarInit(a=None)
3055-
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
3056-
obj.cloned(None)
3057-
self.assertEqual(cm.filename, __file__)
3191+
fn = ac_tester.DeprStarInit(a=None).cloned
3192+
fn(a=None)
3193+
self.check_depr_star("'a'", fn, None, name='_testclinic.DeprStarInit.cloned')
3194+
self.assertRaises(TypeError, fn)
3195+
3196+
def test_depr_kwd_new(self):
3197+
cls = ac_tester.DeprKwdNew
3198+
cls(None)
3199+
self.check_depr_kwd("'a'", cls, a=None)
3200+
self.assertRaises(TypeError, cls)
3201+
3202+
def test_depr_kwd_init(self):
3203+
cls = ac_tester.DeprKwdInit
3204+
cls(None)
3205+
self.check_depr_kwd("'a'", cls, a=None)
3206+
self.assertRaises(TypeError, cls)
30583207

30593208
def test_depr_star_pos0_len1(self):
30603209
fn = ac_tester.depr_star_pos0_len1
@@ -3125,6 +3274,90 @@ def test_depr_star_pos2_len2_with_kwd(self):
31253274
check("a", "b", "c", d=0, e=0)
31263275
check("a", "b", "c", "d", e=0)
31273276

3277+
def test_depr_kwd_required_1(self):
3278+
fn = ac_tester.depr_kwd_required_1
3279+
fn("a", "b")
3280+
self.assertRaises(TypeError, fn, "a")
3281+
self.assertRaises(TypeError, fn, "a", "b", "c")
3282+
check = partial(self.check_depr_kwd, "'b'", fn)
3283+
check("a", b="b")
3284+
self.assertRaises(TypeError, fn, a="a", b="b")
3285+
3286+
def test_depr_kwd_required_2(self):
3287+
fn = ac_tester.depr_kwd_required_2
3288+
fn("a", "b", "c")
3289+
self.assertRaises(TypeError, fn, "a", "b")
3290+
self.assertRaises(TypeError, fn, "a", "b", "c", "d")
3291+
check = partial(self.check_depr_kwd, "'b' and 'c'", fn)
3292+
check("a", "b", c="c")
3293+
check("a", b="b", c="c")
3294+
self.assertRaises(TypeError, fn, a="a", b="b", c="c")
3295+
3296+
def test_depr_kwd_optional_1(self):
3297+
fn = ac_tester.depr_kwd_optional_1
3298+
fn("a")
3299+
fn("a", "b")
3300+
self.assertRaises(TypeError, fn)
3301+
self.assertRaises(TypeError, fn, "a", "b", "c")
3302+
check = partial(self.check_depr_kwd, "'b'", fn)
3303+
check("a", b="b")
3304+
self.assertRaises(TypeError, fn, a="a", b="b")
3305+
3306+
def test_depr_kwd_optional_2(self):
3307+
fn = ac_tester.depr_kwd_optional_2
3308+
fn("a")
3309+
fn("a", "b")
3310+
fn("a", "b", "c")
3311+
self.assertRaises(TypeError, fn)
3312+
self.assertRaises(TypeError, fn, "a", "b", "c", "d")
3313+
check = partial(self.check_depr_kwd, "'b' and 'c'", fn)
3314+
check("a", b="b")
3315+
check("a", c="c")
3316+
check("a", b="b", c="c")
3317+
check("a", c="c", b="b")
3318+
check("a", "b", c="c")
3319+
self.assertRaises(TypeError, fn, a="a", b="b", c="c")
3320+
3321+
def test_depr_kwd_optional_3(self):
3322+
fn = ac_tester.depr_kwd_optional_3
3323+
fn()
3324+
fn("a")
3325+
fn("a", "b")
3326+
fn("a", "b", "c")
3327+
self.assertRaises(TypeError, fn, "a", "b", "c", "d")
3328+
check = partial(self.check_depr_kwd, "'a', 'b' and 'c'", fn)
3329+
check("a", "b", c="c")
3330+
check("a", b="b")
3331+
check(a="a")
3332+
3333+
def test_depr_kwd_required_optional(self):
3334+
fn = ac_tester.depr_kwd_required_optional
3335+
fn("a", "b")
3336+
fn("a", "b", "c")
3337+
self.assertRaises(TypeError, fn)
3338+
self.assertRaises(TypeError, fn, "a")
3339+
self.assertRaises(TypeError, fn, "a", "b", "c", "d")
3340+
check = partial(self.check_depr_kwd, "'b' and 'c'", fn)
3341+
check("a", b="b")
3342+
check("a", b="b", c="c")
3343+
check("a", c="c", b="b")
3344+
check("a", "b", c="c")
3345+
self.assertRaises(TypeError, fn, "a", c="c")
3346+
self.assertRaises(TypeError, fn, a="a", b="b", c="c")
3347+
3348+
def test_depr_kwd_noinline(self):
3349+
fn = ac_tester.depr_kwd_noinline
3350+
fn("a", "b")
3351+
fn("a", "b", "c")
3352+
self.assertRaises(TypeError, fn, "a")
3353+
check = partial(self.check_depr_kwd, "'b' and 'c'", fn)
3354+
check("a", b="b")
3355+
check("a", b="b", c="c")
3356+
check("a", c="c", b="b")
3357+
check("a", "b", c="c")
3358+
self.assertRaises(TypeError, fn, "a", c="c")
3359+
self.assertRaises(TypeError, fn, a="a", b="b", c="c")
3360+
31283361

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

0 commit comments

Comments
 (0)