gh-107704: Argument Clinic: add support for deprecating keyword use of parameters (GH-107984)

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")

Co-authored-by: Erlend E. Aasland <erlend@python.org>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Serhiy Storchaka 2023-08-19 10:13:35 +03:00 committed by GitHub
parent eb953d6e44
commit 2f311437cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 2993 additions and 1345 deletions

View file

@ -1941,54 +1941,70 @@ The generated docstring ends up looking like this:
.. _clinic-howto-deprecate-positional: .. _clinic-howto-deprecate-positional:
.. _clinic-howto-deprecate-keyword:
How to deprecate passing parameters positionally How to deprecate passing parameters positionally or by keyword
------------------------------------------------ --------------------------------------------------------------
Argument Clinic provides syntax that makes it possible to generate code that Argument Clinic provides syntax that makes it possible to generate code that
deprecates passing :term:`arguments <argument>` positionally. deprecates passing :term:`arguments <argument>` for positional-or-keyword
:term:`parameters <parameter>` positionally or by keyword.
For example, say we've got a module-level function :py:func:`!foo.myfunc` For example, say we've got a module-level function :py:func:`!foo.myfunc`
that has three :term:`parameters <parameter>`: that has five parameters: a positional-only parameter *a*, three
positional-or-keyword parameters *a* and *b*, and a keyword-only parameter *c*:: positional-or-keyword parameters *b*, *c* and *d*, and a keyword-only
parameter *e*::
/*[clinic input] /*[clinic input]
module foo module foo
myfunc myfunc
a: int a: int
/
b: int b: int
*
c: int c: int
d: int
*
e: int
[clinic start generated output]*/ [clinic start generated output]*/
We now want to make the *b* parameter keyword-only; We now want to make the *b* parameter positional-only and the *d* parameter
however, we'll have to wait two releases before making this change, keyword-only;
however, we'll have to wait two releases before making these changes,
as mandated by Python's backwards-compatibility policy (see :pep:`387`). as mandated by Python's backwards-compatibility policy (see :pep:`387`).
For this example, imagine we're in the development phase for Python 3.12: For this example, imagine we're in the development phase for Python 3.12:
that means we'll be allowed to introduce deprecation warnings in Python 3.12 that means we'll be allowed to introduce deprecation warnings in Python 3.12
whenever the *b* parameter is passed positionally, whenever an argument for the *b* parameter is passed by keyword or an argument
and we'll be allowed to make it keyword-only in Python 3.14 at the earliest. for the *d* parameter is passed positionally, and we'll be allowed to make
them positional-only and keyword-only respectively in Python 3.14 at
the earliest.
We can use Argument Clinic to emit the desired deprecation warnings We can use Argument Clinic to emit the desired deprecation warnings
using the ``* [from ...]`` syntax, using the ``[from ...]`` syntax, by adding the line ``/ [from 3.14]`` right
by adding the line ``* [from 3.14]`` right above the *b* parameter:: below the *b* parameter and adding the line ``* [from 3.14]`` right above
the *d* parameter::
/*[clinic input] /*[clinic input]
module foo module foo
myfunc myfunc
a: int a: int
* [from 3.14] /
b: int b: int
* / [from 3.14]
c: int c: int
* [from 3.14]
d: int
*
e: int
[clinic start generated output]*/ [clinic start generated output]*/
Next, regenerate Argument Clinic code (``make clinic``), Next, regenerate Argument Clinic code (``make clinic``),
and add unit tests for the new behaviour. and add unit tests for the new behaviour.
The generated code will now emit a :exc:`DeprecationWarning` The generated code will now emit a :exc:`DeprecationWarning`
when an :term:`argument` for the :term:`parameter` *b* is passed positionally. when an :term:`argument` for the :term:`parameter` *d* is passed positionally
(e.g ``myfunc(1, 2, 3, 4, e=5)``) or an argument for the parameter *b* is
passed by keyword (e.g ``myfunc(1, b=2, c=3, d=4, e=5)``).
C preprocessor directives are also generated for emitting C preprocessor directives are also generated for emitting
compiler warnings if the ``* [from ...]`` line has not been removed compiler warnings if the ``[from ...]`` lines have not been removed
from the Argument Clinic input when the deprecation period is over, from the Argument Clinic input when the deprecation period is over,
which means when the alpha phase of the specified Python version kicks in. which means when the alpha phase of the specified Python version kicks in.
@ -2001,21 +2017,26 @@ Luckily for us, compiler warnings are now generated:
.. code-block:: none .. code-block:: none
In file included from Modules/foomodule.c:139: In file included from Modules/foomodule.c:139:
Modules/clinic/foomodule.c.h:139:8: warning: In 'foomodule.c', update parameter(s) 'a' and 'b' in the clinic input of 'mymod.myfunc' to be keyword-only. [-W#warnings] Modules/clinic/foomodule.c.h:139:8: warning: In 'foomodule.c', update the clinic input of 'mymod.myfunc'. [-W#warnings]
# warning "In 'foomodule.c', update parameter(s) 'a' and 'b' in the clinic input of 'mymod.myfunc' to be keyword-only. [-W#warnings]" # warning "In 'foomodule.c', update the clinic input of 'mymod.myfunc'. [-W#warnings]"
^ ^
We now close the deprecation phase by making *b* keyword-only; We now close the deprecation phase by making *a* positional-only and *c*
replace the ``* [from ...]`` line above *b* keyword-only;
with the ``*`` from the line above *c*:: replace the ``/ [from ...]`` line below *b* with the ``/`` from the line
below *a* and the ``* [from ...]`` line above *d* with the ``*`` from
the line above *e*::
/*[clinic input] /*[clinic input]
module foo module foo
myfunc myfunc
a: int a: int
*
b: int b: int
/
c: int c: int
*
d: int
e: int
[clinic start generated output]*/ [clinic start generated output]*/
Finally, run ``make clinic`` to regenerate the Argument Clinic code, Finally, run ``make clinic`` to regenerate the Argument Clinic code,

View file

@ -1611,7 +1611,7 @@ class ClinicParserTest(TestCase):
"module foo\nfoo.bar\n this: int\n *", "module foo\nfoo.bar\n this: int\n *",
"module foo\nfoo.bar\n this: int\n *\nDocstring.", "module foo\nfoo.bar\n this: int\n *\nDocstring.",
) )
err = "Function 'foo.bar' specifies '*' without any parameters afterwards." err = "Function 'bar' specifies '*' without following parameters."
for block in dataset: for block in dataset:
with self.subTest(block=block): with self.subTest(block=block):
self.expect_failure(block, err) self.expect_failure(block, err)
@ -1679,7 +1679,7 @@ class ClinicParserTest(TestCase):
Docstring. Docstring.
""" """
err = ( err = (
"Function 'foo.bar': expected format '* [from major.minor]' " "Function 'bar': expected format '[from major.minor]' "
"where 'major' and 'minor' are integers; got '3'" "where 'major' and 'minor' are integers; got '3'"
) )
self.expect_failure(block, err, lineno=3) self.expect_failure(block, err, lineno=3)
@ -1693,7 +1693,7 @@ class ClinicParserTest(TestCase):
Docstring. Docstring.
""" """
err = ( err = (
"Function 'foo.bar': expected format '* [from major.minor]' " "Function 'bar': expected format '[from major.minor]' "
"where 'major' and 'minor' are integers; got 'a.b'" "where 'major' and 'minor' are integers; got 'a.b'"
) )
self.expect_failure(block, err, lineno=3) self.expect_failure(block, err, lineno=3)
@ -1707,7 +1707,7 @@ class ClinicParserTest(TestCase):
Docstring. Docstring.
""" """
err = ( err = (
"Function 'foo.bar': expected format '* [from major.minor]' " "Function 'bar': expected format '[from major.minor]' "
"where 'major' and 'minor' are integers; got '1.2.3'" "where 'major' and 'minor' are integers; got '1.2.3'"
) )
self.expect_failure(block, err, lineno=3) self.expect_failure(block, err, lineno=3)
@ -1721,8 +1721,24 @@ class ClinicParserTest(TestCase):
Docstring. Docstring.
""" """
err = ( err = (
"Function 'foo.bar' specifies '* [from ...]' without " "Function 'bar' specifies '* [from ...]' without "
"any parameters afterwards" "following parameters."
)
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 'bar' specifies '* [from ...]' without "
"following parameters."
) )
self.expect_failure(block, err, lineno=4) self.expect_failure(block, err, lineno=4)
@ -1735,7 +1751,7 @@ class ClinicParserTest(TestCase):
* [from 3.14] * [from 3.14]
Docstring. Docstring.
""" """
err = "Function 'foo.bar': '* [from ...]' must come before '*'" err = "Function 'bar': '* [from ...]' must come before '*'"
self.expect_failure(block, err, lineno=4) self.expect_failure(block, err, lineno=4)
def test_depr_star_duplicate(self): def test_depr_star_duplicate(self):
@ -1749,7 +1765,49 @@ class ClinicParserTest(TestCase):
c: int c: int
Docstring. Docstring.
""" """
err = "Function 'foo.bar' uses '[from ...]' more than once" err = "Function 'bar' uses '* [from ...]' more than once."
self.expect_failure(block, err, lineno=5)
def test_depr_star_duplicate2(self):
block = """
module foo
foo.bar
a: int
* [from 3.14]
b: int
* [from 3.15]
c: int
Docstring.
"""
err = "Function '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."
self.expect_failure(block, err, lineno=5)
def test_depr_slash_duplicate2(self):
block = """
module foo
foo.bar
a: int
/ [from 3.14]
b: int
/ [from 3.15]
c: int
Docstring.
"""
err = "Function 'bar' uses '/ [from ...]' more than once."
self.expect_failure(block, err, lineno=5) self.expect_failure(block, err, lineno=5)
def test_single_slash(self): def test_single_slash(self):
@ -1765,6 +1823,34 @@ class ClinicParserTest(TestCase):
) )
self.expect_failure(block, err) 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 "
"preceding parameters."
)
self.expect_failure(block, err, lineno=2)
def test_parameters_required_before_depr_slash2(self):
block = """
module foo
foo.bar
a: int
/
/ [from 3.14]
Docstring.
"""
err = (
"Function 'bar' specifies '/ [from ...]' without "
"preceding parameters."
)
self.expect_failure(block, err, lineno=4)
def test_double_slash(self): def test_double_slash(self):
block = """ block = """
module foo module foo
@ -1787,12 +1873,61 @@ class ClinicParserTest(TestCase):
z: int z: int
/ /
""" """
err = ( err = "Function 'bar': '/' must precede '*'"
"Function 'bar' mixes keyword-only and positional-only parameters, "
"which is unsupported."
)
self.expect_failure(block, err) 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': '/' must precede '* [from ...]'"
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': '/ [from ...]' must precede '* [from ...]'"
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': '/ [from ...]' must precede '*'"
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 'bar': '/' must precede '/ [from ...]'"
self.expect_failure(block, err, lineno=4)
def test_parameters_not_permitted_after_slash_for_now(self): def test_parameters_not_permitted_after_slash_for_now(self):
block = """ block = """
module foo module foo
@ -2589,11 +2724,33 @@ class ClinicFunctionalTest(unittest.TestCase):
locals().update((name, getattr(ac_tester, name)) locals().update((name, getattr(ac_tester, name))
for name in dir(ac_tester) if name.startswith('test_')) 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 = ( regex = (
fr"Passing( more than)?( [0-9]+)? positional argument(s)? to " fr"Passing( more than)?( [0-9]+)? positional argument(s)? to "
fr"{fn.__name__}\(\) is deprecated. Parameter(s)? {pnames} will " fr"{re.escape(name)}\(\) is deprecated. Parameters? {pnames} will "
fr"become( a)? keyword-only parameter(s)? in Python 3\.14" 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."
) )
with self.assertWarnsRegex(DeprecationWarning, regex) as cm: with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
# Record the line number, so we're sure we've got the correct stack # Record the line number, so we're sure we've got the correct stack
@ -3067,46 +3224,67 @@ class ClinicFunctionalTest(unittest.TestCase):
self.assertEqual(func(), name) self.assertEqual(func(), name)
def test_depr_star_new(self): def test_depr_star_new(self):
regex = re.escape( cls = ac_tester.DeprStarNew
"Passing positional arguments to _testclinic.DeprStarNew() is " cls()
"deprecated. Parameter 'a' will become a keyword-only parameter " cls(a=None)
"in Python 3.14." self.check_depr_star("'a'", cls, None)
)
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
ac_tester.DeprStarNew(None)
self.assertEqual(cm.filename, __file__)
def test_depr_star_new_cloned(self): def test_depr_star_new_cloned(self):
regex = re.escape( fn = ac_tester.DeprStarNew().cloned
"Passing positional arguments to _testclinic.DeprStarNew.cloned() " fn()
"is deprecated. Parameter 'a' will become a keyword-only parameter " fn(a=None)
"in Python 3.14." self.check_depr_star("'a'", fn, None, name='_testclinic.DeprStarNew.cloned')
)
obj = ac_tester.DeprStarNew(a=None)
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
obj.cloned(None)
self.assertEqual(cm.filename, __file__)
def test_depr_star_init(self): def test_depr_star_init(self):
regex = re.escape( cls = ac_tester.DeprStarInit
"Passing positional arguments to _testclinic.DeprStarInit() is " cls()
"deprecated. Parameter 'a' will become a keyword-only parameter " cls(a=None)
"in Python 3.14." self.check_depr_star("'a'", cls, None)
)
with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
ac_tester.DeprStarInit(None)
self.assertEqual(cm.filename, __file__)
def test_depr_star_init_cloned(self): def test_depr_star_init_cloned(self):
regex = re.escape( fn = ac_tester.DeprStarInit().cloned
"Passing positional arguments to _testclinic.DeprStarInit.cloned() " fn()
"is deprecated. Parameter 'a' will become a keyword-only parameter " fn(a=None)
"in Python 3.14." self.check_depr_star("'a'", fn, None, name='_testclinic.DeprStarInit.cloned')
)
obj = ac_tester.DeprStarInit(a=None) def test_depr_star_init_noinline(self):
with self.assertWarnsRegex(DeprecationWarning, regex) as cm: cls = ac_tester.DeprStarInitNoInline
obj.cloned(None) self.assertRaises(TypeError, cls, "a")
self.assertEqual(cm.filename, __file__) cls(a="a", b="b")
cls(a="a", b="b", c="c")
cls("a", b="b")
cls("a", b="b", c="c")
check = partial(self.check_depr_star, "'b' and 'c'", cls)
check("a", "b")
check("a", "b", "c")
check("a", "b", c="c")
self.assertRaises(TypeError, cls, "a", "b", "c", "d")
def test_depr_kwd_new(self):
cls = ac_tester.DeprKwdNew
cls()
cls(None)
self.check_depr_kwd("'a'", cls, a=None)
def test_depr_kwd_init(self):
cls = ac_tester.DeprKwdInit
cls()
cls(None)
self.check_depr_kwd("'a'", cls, a=None)
def test_depr_kwd_init_noinline(self):
cls = ac_tester.DeprKwdInitNoInline
cls = ac_tester.depr_star_noinline
self.assertRaises(TypeError, cls, "a")
cls(a="a", b="b")
cls(a="a", b="b", c="c")
cls("a", b="b")
cls("a", b="b", c="c")
check = partial(self.check_depr_star, "'b' and 'c'", cls)
check("a", "b")
check("a", "b", "c")
check("a", "b", c="c")
self.assertRaises(TypeError, cls, "a", "b", "c", "d")
def test_depr_star_pos0_len1(self): def test_depr_star_pos0_len1(self):
fn = ac_tester.depr_star_pos0_len1 fn = ac_tester.depr_star_pos0_len1
@ -3177,6 +3355,103 @@ class ClinicFunctionalTest(unittest.TestCase):
check("a", "b", "c", d=0, e=0) check("a", "b", "c", d=0, e=0)
check("a", "b", "c", "d", e=0) check("a", "b", "c", "d", e=0)
def test_depr_star_noinline(self):
fn = ac_tester.depr_star_noinline
self.assertRaises(TypeError, fn, "a")
fn(a="a", b="b")
fn(a="a", b="b", c="c")
fn("a", b="b")
fn("a", b="b", c="c")
check = partial(self.check_depr_star, "'b' and 'c'", fn)
check("a", "b")
check("a", "b", "c")
check("a", "b", c="c")
self.assertRaises(TypeError, fn, "a", "b", "c", "d")
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")
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): class PermutationTests(unittest.TestCase):
"""Test permutation support functions.""" """Test permutation support functions."""

View file

@ -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.

View file

@ -16,6 +16,17 @@ pysqlite_connection_init_impl(pysqlite_Connection *self, PyObject *database,
int cache_size, int uri, int cache_size, int uri,
enum autocommit_mode autocommit); enum autocommit_mode autocommit);
// Emit compiler warnings when we get to Python 3.15.
#if PY_VERSION_HEX >= 0x030f00C0
# error "Update the clinic input of '_sqlite3.Connection.__init__'."
#elif PY_VERSION_HEX >= 0x030f00A0
# ifdef _MSC_VER
# pragma message ("Update the clinic input of '_sqlite3.Connection.__init__'.")
# else
# warning "Update the clinic input of '_sqlite3.Connection.__init__'."
# endif
#endif
static int static int
pysqlite_connection_init(PyObject *self, PyObject *args, PyObject *kwargs) pysqlite_connection_init(PyObject *self, PyObject *args, PyObject *kwargs)
{ {
@ -59,28 +70,6 @@ pysqlite_connection_init(PyObject *self, PyObject *args, PyObject *kwargs)
int uri = 0; int uri = 0;
enum autocommit_mode autocommit = LEGACY_TRANSACTION_CONTROL; enum autocommit_mode autocommit = LEGACY_TRANSACTION_CONTROL;
// Emit compiler warnings when we get to Python 3.15.
#if PY_VERSION_HEX >= 0x030f00C0
# error \
"In connection.c, update parameter(s) 'timeout', 'detect_types', " \
"'isolation_level', 'check_same_thread', 'factory', " \
"'cached_statements' and 'uri' in the clinic input of " \
"'_sqlite3.Connection.__init__' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030f00A0
# ifdef _MSC_VER
# pragma message ( \
"In connection.c, update parameter(s) 'timeout', 'detect_types', " \
"'isolation_level', 'check_same_thread', 'factory', " \
"'cached_statements' and 'uri' in the clinic input of " \
"'_sqlite3.Connection.__init__' to be keyword-only.")
# else
# warning \
"In connection.c, update parameter(s) 'timeout', 'detect_types', " \
"'isolation_level', 'check_same_thread', 'factory', " \
"'cached_statements' and 'uri' in the clinic input of " \
"'_sqlite3.Connection.__init__' to be keyword-only."
# endif
#endif
if (nargs > 1 && nargs <= 8) { if (nargs > 1 && nargs <= 8) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, if (PyErr_WarnEx(PyExc_DeprecationWarning,
"Passing more than 1 positional argument to _sqlite3.Connection()" "Passing more than 1 positional argument to _sqlite3.Connection()"
@ -89,7 +78,7 @@ pysqlite_connection_init(PyObject *self, PyObject *args, PyObject *kwargs)
"'cached_statements' and 'uri' will become keyword-only " "'cached_statements' and 'uri' will become keyword-only "
"parameters in Python 3.15.", 1)) "parameters in Python 3.15.", 1))
{ {
goto exit; goto exit;
} }
} }
fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 1, 8, 0, argsbuf); fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 1, 8, 0, argsbuf);
@ -1692,4 +1681,4 @@ exit:
#ifndef DESERIALIZE_METHODDEF #ifndef DESERIALIZE_METHODDEF
#define DESERIALIZE_METHODDEF #define DESERIALIZE_METHODDEF
#endif /* !defined(DESERIALIZE_METHODDEF) */ #endif /* !defined(DESERIALIZE_METHODDEF) */
/*[clinic end generated code: output=5a05e5294ad9d2ce input=a9049054013a1b77]*/ /*[clinic end generated code: output=0ad9d55977a51b8f input=a9049054013a1b77]*/

View file

@ -1195,14 +1195,14 @@ clone_with_conv_f2_impl(PyObject *module, custom_t path)
/*[clinic input] /*[clinic input]
output push output push
destination deprstar new file '{dirname}/clinic/_testclinic_depr_star.c.h' destination deprstar new file '{dirname}/clinic/_testclinic_depr.c.h'
output everything deprstar output everything deprstar
#output methoddef_ifndef buffer 1 #output methoddef_ifndef buffer 1
output docstring_prototype suppress output docstring_prototype suppress
output parser_prototype suppress output parser_prototype suppress
output impl_definition block output impl_definition block
[clinic start generated code]*/ [clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=f88f37038e00fb0a]*/ /*[clinic end generated code: output=da39a3ee5e6b4b0d input=32116eac48a42d34]*/
// Mock Python version 3.8 // Mock Python version 3.8
@ -1211,7 +1211,7 @@ output impl_definition block
#define PY_VERSION_HEX 0x03080000 #define PY_VERSION_HEX 0x03080000
#include "clinic/_testclinic_depr_star.c.h" #include "clinic/_testclinic_depr.c.h"
/*[clinic input] /*[clinic input]
@ -1219,13 +1219,13 @@ class _testclinic.DeprStarNew "PyObject *" "PyObject"
@classmethod @classmethod
_testclinic.DeprStarNew.__new__ as depr_star_new _testclinic.DeprStarNew.__new__ as depr_star_new
* [from 3.14] * [from 3.14]
a: object a: object = None
The deprecation message should use the class name instead of __new__. The deprecation message should use the class name instead of __new__.
[clinic start generated code]*/ [clinic start generated code]*/
static PyObject * static PyObject *
depr_star_new_impl(PyTypeObject *type, PyObject *a) depr_star_new_impl(PyTypeObject *type, PyObject *a)
/*[clinic end generated code: output=bdbb36244f90cf46 input=f4ae7dafbc23c378]*/ /*[clinic end generated code: output=bdbb36244f90cf46 input=fdd640db964b4dc1]*/
{ {
return type->tp_alloc(type, 0); return type->tp_alloc(type, 0);
} }
@ -1260,13 +1260,13 @@ static PyTypeObject DeprStarNew = {
class _testclinic.DeprStarInit "PyObject *" "PyObject" class _testclinic.DeprStarInit "PyObject *" "PyObject"
_testclinic.DeprStarInit.__init__ as depr_star_init _testclinic.DeprStarInit.__init__ as depr_star_init
* [from 3.14] * [from 3.14]
a: object a: object = None
The deprecation message should use the class name instead of __init__. The deprecation message should use the class name instead of __init__.
[clinic start generated code]*/ [clinic start generated code]*/
static int static int
depr_star_init_impl(PyObject *self, PyObject *a) depr_star_init_impl(PyObject *self, PyObject *a)
/*[clinic end generated code: output=8d27b43c286d3ecc input=659ebc748d87fa86]*/ /*[clinic end generated code: output=8d27b43c286d3ecc input=5575b77229d5e2be]*/
{ {
return 0; return 0;
} }
@ -1298,6 +1298,116 @@ static PyTypeObject DeprStarInit = {
}; };
/*[clinic input]
class _testclinic.DeprStarInitNoInline "PyObject *" "PyObject"
_testclinic.DeprStarInitNoInline.__init__ as depr_star_init_noinline
a: object
* [from 3.14]
b: object
c: object = None
*
# Force to use _PyArg_ParseTupleAndKeywordsFast.
d: str(accept={str, robuffer}, zeroes=True) = ''
[clinic start generated code]*/
static int
depr_star_init_noinline_impl(PyObject *self, PyObject *a, PyObject *b,
PyObject *c, const char *d, Py_ssize_t d_length)
/*[clinic end generated code: output=9b31fc167f1bf9f7 input=5a887543122bca48]*/
{
return 0;
}
static PyTypeObject DeprStarInitNoInline = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "_testclinic.DeprStarInitNoInline",
.tp_basicsize = sizeof(PyObject),
.tp_new = PyType_GenericNew,
.tp_init = depr_star_init_noinline,
.tp_flags = Py_TPFLAGS_DEFAULT,
};
/*[clinic input]
class _testclinic.DeprKwdNew "PyObject *" "PyObject"
@classmethod
_testclinic.DeprKwdNew.__new__ as depr_kwd_new
a: object = None
/ [from 3.14]
The deprecation message should use the class name instead of __new__.
[clinic start generated code]*/
static PyObject *
depr_kwd_new_impl(PyTypeObject *type, PyObject *a)
/*[clinic end generated code: output=618d07afc5616149 input=6c7d13c471013c10]*/
{
return type->tp_alloc(type, 0);
}
static PyTypeObject DeprKwdNew = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "_testclinic.DeprKwdNew",
.tp_basicsize = sizeof(PyObject),
.tp_new = depr_kwd_new,
.tp_flags = Py_TPFLAGS_DEFAULT,
};
/*[clinic input]
class _testclinic.DeprKwdInit "PyObject *" "PyObject"
_testclinic.DeprKwdInit.__init__ as depr_kwd_init
a: object = None
/ [from 3.14]
The deprecation message should use the class name instead of __init__.
[clinic start generated code]*/
static int
depr_kwd_init_impl(PyObject *self, PyObject *a)
/*[clinic end generated code: output=6e02eb724a85d840 input=b9bf3c20f012d539]*/
{
return 0;
}
static PyTypeObject DeprKwdInit = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "_testclinic.DeprKwdInit",
.tp_basicsize = sizeof(PyObject),
.tp_new = PyType_GenericNew,
.tp_init = depr_kwd_init,
.tp_flags = Py_TPFLAGS_DEFAULT,
};
/*[clinic input]
class _testclinic.DeprKwdInitNoInline "PyObject *" "PyObject"
_testclinic.DeprKwdInitNoInline.__init__ as depr_kwd_init_noinline
a: object
/
b: object
c: object = None
/ [from 3.14]
# Force to use _PyArg_ParseTupleAndKeywordsFast.
d: str(accept={str, robuffer}, zeroes=True) = ''
[clinic start generated code]*/
static int
depr_kwd_init_noinline_impl(PyObject *self, PyObject *a, PyObject *b,
PyObject *c, const char *d, Py_ssize_t d_length)
/*[clinic end generated code: output=27759d70ddd25873 input=c19d982c8c70a930]*/
{
return 0;
}
static PyTypeObject DeprKwdInitNoInline = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "_testclinic.DeprKwdInitNoInline",
.tp_basicsize = sizeof(PyObject),
.tp_new = PyType_GenericNew,
.tp_init = depr_kwd_init_noinline,
.tp_flags = Py_TPFLAGS_DEFAULT,
};
/*[clinic input] /*[clinic input]
depr_star_pos0_len1 depr_star_pos0_len1
* [from 3.14] * [from 3.14]
@ -1450,6 +1560,148 @@ depr_star_pos2_len2_with_kwd_impl(PyObject *module, PyObject *a, PyObject *b,
} }
/*[clinic input]
depr_star_noinline
a: object
* [from 3.14]
b: object
c: object = None
*
# Force to use _PyArg_ParseStackAndKeywords.
d: str(accept={str, robuffer}, zeroes=True) = ''
[clinic start generated code]*/
static PyObject *
depr_star_noinline_impl(PyObject *module, PyObject *a, PyObject *b,
PyObject *c, const char *d, Py_ssize_t d_length)
/*[clinic end generated code: output=cc27dacf5c2754af input=d36cc862a2daef98]*/
{
Py_RETURN_NONE;
}
/*[clinic input]
depr_kwd_required_1
a: object
/
b: object
/ [from 3.14]
[clinic start generated code]*/
static PyObject *
depr_kwd_required_1_impl(PyObject *module, PyObject *a, PyObject *b)
/*[clinic end generated code: output=1d8ab19ea78418af input=53f2c398b828462d]*/
{
Py_RETURN_NONE;
}
/*[clinic input]
depr_kwd_required_2
a: object
/
b: object
c: object
/ [from 3.14]
[clinic start generated code]*/
static PyObject *
depr_kwd_required_2_impl(PyObject *module, PyObject *a, PyObject *b,
PyObject *c)
/*[clinic end generated code: output=44a89cb82509ddde input=a2b0ef37de8a01a7]*/
{
Py_RETURN_NONE;
}
/*[clinic input]
depr_kwd_optional_1
a: object
/
b: object = None
/ [from 3.14]
[clinic start generated code]*/
static PyObject *
depr_kwd_optional_1_impl(PyObject *module, PyObject *a, PyObject *b)
/*[clinic end generated code: output=a8a3d67efcc7b058 input=e416981eb78c3053]*/
{
Py_RETURN_NONE;
}
/*[clinic input]
depr_kwd_optional_2
a: object
/
b: object = None
c: object = None
/ [from 3.14]
[clinic start generated code]*/
static PyObject *
depr_kwd_optional_2_impl(PyObject *module, PyObject *a, PyObject *b,
PyObject *c)
/*[clinic end generated code: output=aa2d967f26fdb9f6 input=cae3afb783bfc855]*/
{
Py_RETURN_NONE;
}
/*[clinic input]
depr_kwd_optional_3
a: object = None
b: object = None
c: object = None
/ [from 3.14]
[clinic start generated code]*/
static PyObject *
depr_kwd_optional_3_impl(PyObject *module, PyObject *a, PyObject *b,
PyObject *c)
/*[clinic end generated code: output=a26025bf6118fd07 input=c9183b2f9ccaf992]*/
{
Py_RETURN_NONE;
}
/*[clinic input]
depr_kwd_required_optional
a: object
/
b: object
c: object = None
/ [from 3.14]
[clinic start generated code]*/
static PyObject *
depr_kwd_required_optional_impl(PyObject *module, PyObject *a, PyObject *b,
PyObject *c)
/*[clinic end generated code: output=e53a8b7a250d8ffc input=23237a046f8388f5]*/
{
Py_RETURN_NONE;
}
/*[clinic input]
depr_kwd_noinline
a: object
/
b: object
c: object = None
/ [from 3.14]
# Force to use _PyArg_ParseStackAndKeywords.
d: str(accept={str, robuffer}, zeroes=True) = ''
[clinic start generated code]*/
static PyObject *
depr_kwd_noinline_impl(PyObject *module, PyObject *a, PyObject *b,
PyObject *c, const char *d, Py_ssize_t d_length)
/*[clinic end generated code: output=f59da8113f2bad7c input=1d6db65bebb069d7]*/
{
Py_RETURN_NONE;
}
// Reset PY_VERSION_HEX // Reset PY_VERSION_HEX
#undef PY_VERSION_HEX #undef PY_VERSION_HEX
#define PY_VERSION_HEX _SAVED_PY_VERSION #define PY_VERSION_HEX _SAVED_PY_VERSION
@ -1526,6 +1778,14 @@ static PyMethodDef tester_methods[] = {
DEPR_STAR_POS2_LEN1_METHODDEF DEPR_STAR_POS2_LEN1_METHODDEF
DEPR_STAR_POS2_LEN2_METHODDEF DEPR_STAR_POS2_LEN2_METHODDEF
DEPR_STAR_POS2_LEN2_WITH_KWD_METHODDEF DEPR_STAR_POS2_LEN2_WITH_KWD_METHODDEF
DEPR_STAR_NOINLINE_METHODDEF
DEPR_KWD_REQUIRED_1_METHODDEF
DEPR_KWD_REQUIRED_2_METHODDEF
DEPR_KWD_OPTIONAL_1_METHODDEF
DEPR_KWD_OPTIONAL_2_METHODDEF
DEPR_KWD_OPTIONAL_3_METHODDEF
DEPR_KWD_REQUIRED_OPTIONAL_METHODDEF
DEPR_KWD_NOINLINE_METHODDEF
{NULL, NULL} {NULL, NULL}
}; };
@ -1549,6 +1809,18 @@ PyInit__testclinic(void)
if (PyModule_AddType(m, &DeprStarInit) < 0) { if (PyModule_AddType(m, &DeprStarInit) < 0) {
goto error; goto error;
} }
if (PyModule_AddType(m, &DeprStarInitNoInline) < 0) {
goto error;
}
if (PyModule_AddType(m, &DeprKwdNew) < 0) {
goto error;
}
if (PyModule_AddType(m, &DeprKwdInit) < 0) {
goto error;
}
if (PyModule_AddType(m, &DeprKwdInitNoInline) < 0) {
goto error;
}
return m; return m;
error: error:

2095
Modules/clinic/_testclinic_depr.c.h generated Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -323,7 +323,11 @@ Modules/_testcapi/vectorcall.c - MethodDescriptorDerived_Type -
Modules/_testcapi/vectorcall.c - MethodDescriptorNopGet_Type - Modules/_testcapi/vectorcall.c - MethodDescriptorNopGet_Type -
Modules/_testcapi/vectorcall.c - MethodDescriptor2_Type - Modules/_testcapi/vectorcall.c - MethodDescriptor2_Type -
Modules/_testclinic.c - DeprStarInit - Modules/_testclinic.c - DeprStarInit -
Modules/_testclinic.c - DeprStarInitNoInline -
Modules/_testclinic.c - DeprStarNew - Modules/_testclinic.c - DeprStarNew -
Modules/_testclinic.c - DeprKwdInit -
Modules/_testclinic.c - DeprKwdInitNoInline -
Modules/_testclinic.c - DeprKwdNew -
################################## ##################################

Can't render this file because it has a wrong number of fields in line 4.

View file

@ -849,25 +849,24 @@ class CLanguage(Language):
#define {methoddef_name} #define {methoddef_name}
#endif /* !defined({methoddef_name}) */ #endif /* !defined({methoddef_name}) */
""") """)
DEPRECATED_POSITIONAL_PROTOTYPE: Final[str] = r""" COMPILER_DEPRECATION_WARNING_PROTOTYPE: Final[str] = r"""
// Emit compiler warnings when we get to Python {major}.{minor}. // Emit compiler warnings when we get to Python {major}.{minor}.
#if PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00C0 #if PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00C0
# error \ # error {message}
{cpp_message}
#elif PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00A0 #elif PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00A0
# ifdef _MSC_VER # ifdef _MSC_VER
# pragma message ( \ # pragma message ({message})
{cpp_message})
# else # else
# warning \ # warning {message}
{cpp_message}
# endif # endif
#endif #endif
if ({condition}) {{{{ """
DEPRECATION_WARNING_PROTOTYPE: Final[str] = r"""
if ({condition}) {{{{{errcheck}
if (PyErr_WarnEx(PyExc_DeprecationWarning, if (PyErr_WarnEx(PyExc_DeprecationWarning,
{depr_message}, 1)) {message}, 1))
{{{{ {{{{
goto exit; goto exit;
}}}} }}}}
}}}} }}}}
""" """
@ -893,6 +892,30 @@ class CLanguage(Language):
function = o function = o
return self.render_function(clinic, function) return self.render_function(clinic, function)
def compiler_deprecated_warning(
self,
func: Function,
parameters: list[Parameter],
) -> str | None:
minversion: VersionTuple | None = None
for p in parameters:
for version in p.deprecated_positional, p.deprecated_keyword:
if version and (not minversion or minversion > version):
minversion = version
if not minversion:
return None
# Format the preprocessor warning and error messages.
assert isinstance(self.cpp.filename, str)
source = os.path.basename(self.cpp.filename)
message = f"Update the clinic input of {func.full_name!r}."
code = self.COMPILER_DEPRECATION_WARNING_PROTOTYPE.format(
major=minversion[0],
minor=minversion[1],
message=c_repr(message),
)
return normalize_snippet(code)
def deprecate_positional_use( def deprecate_positional_use(
self, self,
func: Function, func: Function,
@ -910,15 +933,7 @@ class CLanguage(Language):
assert first_param.deprecated_positional == last_param.deprecated_positional assert first_param.deprecated_positional == last_param.deprecated_positional
thenceforth = first_param.deprecated_positional thenceforth = first_param.deprecated_positional
assert thenceforth is not None assert thenceforth is not None
# Format the preprocessor warning and error messages.
assert isinstance(self.cpp.filename, str)
source = os.path.basename(self.cpp.filename)
major, minor = thenceforth major, minor = thenceforth
cpp_message = (
f"In {source}, update parameter(s) {pstr} in the clinic "
f"input of {func.full_name!r} to be keyword-only."
)
# Format the deprecation message. # Format the deprecation message.
if first_pos == 0: if first_pos == 0:
@ -927,7 +942,7 @@ class CLanguage(Language):
condition = f"nargs == {first_pos+1}" condition = f"nargs == {first_pos+1}"
if first_pos: if first_pos:
preamble = f"Passing {first_pos+1} positional arguments to " preamble = f"Passing {first_pos+1} positional arguments to "
depr_message = preamble + ( message = preamble + (
f"{func.fulldisplayname}() is deprecated. Parameter {pstr} will " f"{func.fulldisplayname}() is deprecated. Parameter {pstr} will "
f"become a keyword-only parameter in Python {major}.{minor}." f"become a keyword-only parameter in Python {major}.{minor}."
) )
@ -938,26 +953,93 @@ class CLanguage(Language):
f"Passing more than {first_pos} positional " f"Passing more than {first_pos} positional "
f"argument{'s' if first_pos != 1 else ''} to " f"argument{'s' if first_pos != 1 else ''} to "
) )
depr_message = preamble + ( message = preamble + (
f"{func.fulldisplayname}() is deprecated. Parameters {pstr} will " f"{func.fulldisplayname}() is deprecated. Parameters {pstr} will "
f"become keyword-only parameters in Python {major}.{minor}." f"become keyword-only parameters in Python {major}.{minor}."
) )
# Append deprecation warning to docstring. # Append deprecation warning to docstring.
lines = textwrap.wrap(f"Note: {depr_message}") docstring = textwrap.fill(f"Note: {message}")
docstring = "\n".join(lines)
func.docstring += f"\n\n{docstring}\n" func.docstring += f"\n\n{docstring}\n"
# Format and return the code block. # Format and return the code block.
code = self.DEPRECATED_POSITIONAL_PROTOTYPE.format( code = self.DEPRECATION_WARNING_PROTOTYPE.format(
condition=condition, condition=condition,
major=major, errcheck="",
minor=minor, message=wrapped_c_string_literal(message, width=64,
cpp_message=wrapped_c_string_literal(cpp_message, suffix=" \\", subsequent_indent=20),
width=64, )
subsequent_indent=16), return normalize_snippet(code, indent=4)
depr_message=wrapped_c_string_literal(depr_message, width=64,
subsequent_indent=20), def deprecate_keyword_use(
self,
func: Function,
params: dict[int, Parameter],
argname_fmt: str | None,
) -> str:
assert len(params) > 0
names = [repr(p.name) for p in params.values()]
first_param = next(iter(params.values()))
last_param = next(reversed(params.values()))
# Pretty-print list of names.
pstr = pprint_words(names)
# For now, assume there's only one deprecation level.
assert first_param.deprecated_keyword == last_param.deprecated_keyword
thenceforth = first_param.deprecated_keyword
assert thenceforth is not None
major, minor = thenceforth
# Format the deprecation message.
containscheck = ""
conditions = []
for i, p in params.items():
if p.is_optional():
if argname_fmt:
conditions.append(f"nargs < {i+1} && {argname_fmt % i}")
elif func.kind.new_or_init:
conditions.append(f"nargs < {i+1} && PyDict_Contains(kwargs, &_Py_ID({p.name}))")
containscheck = "PyDict_Contains"
else:
conditions.append(f"nargs < {i+1} && PySequence_Contains(kwnames, &_Py_ID({p.name}))")
containscheck = "PySequence_Contains"
else:
conditions = [f"nargs < {i+1}"]
condition = ") || (".join(conditions)
if len(conditions) > 1:
condition = f"(({condition}))"
if last_param.is_optional():
if func.kind.new_or_init:
condition = f"kwargs && PyDict_GET_SIZE(kwargs) && {condition}"
else:
condition = f"kwnames && PyTuple_GET_SIZE(kwnames) && {condition}"
if len(params) == 1:
what1 = "argument"
what2 = "parameter"
else:
what1 = "arguments"
what2 = "parameters"
message = (
f"Passing keyword {what1} {pstr} to {func.fulldisplayname}() is deprecated. "
f"Corresponding {what2} will become positional-only in Python {major}.{minor}."
)
if containscheck:
errcheck = f"""
if (PyErr_Occurred()) {{{{ // {containscheck}() above can fail
goto exit;
}}}}"""
else:
errcheck = ""
if argname_fmt:
# Append deprecation warning to docstring.
docstring = textwrap.fill(f"Note: {message}")
func.docstring += f"\n\n{docstring}\n"
# Format and return the code block.
code = self.DEPRECATION_WARNING_PROTOTYPE.format(
condition=condition,
errcheck=errcheck,
message=wrapped_c_string_literal(message, width=64,
subsequent_indent=20),
) )
return normalize_snippet(code, indent=4) return normalize_snippet(code, indent=4)
@ -1258,6 +1340,14 @@ class CLanguage(Language):
parser_definition = parser_body(parser_prototype, *parser_code) parser_definition = parser_body(parser_prototype, *parser_code)
else: else:
deprecated_positionals: dict[int, Parameter] = {}
deprecated_keywords: dict[int, Parameter] = {}
for i, p in enumerate(parameters):
if p.deprecated_positional:
deprecated_positionals[i] = p
if p.deprecated_keyword:
deprecated_keywords[i] = p
has_optional_kw = (max(pos_only, min_pos) + min_kw_only < len(converters) - int(vararg != NO_VARARG)) has_optional_kw = (max(pos_only, min_pos) + min_kw_only < len(converters) - int(vararg != NO_VARARG))
if vararg == NO_VARARG: if vararg == NO_VARARG:
args_declaration = "_PyArg_UnpackKeywords", "%s, %s, %s" % ( args_declaration = "_PyArg_UnpackKeywords", "%s, %s, %s" % (
@ -1310,7 +1400,10 @@ class CLanguage(Language):
flags = 'METH_METHOD|' + flags flags = 'METH_METHOD|' + flags
parser_prototype = self.PARSER_PROTOTYPE_DEF_CLASS parser_prototype = self.PARSER_PROTOTYPE_DEF_CLASS
deprecated_positionals: dict[int, Parameter] = {} if deprecated_keywords:
code = self.deprecate_keyword_use(f, deprecated_keywords, argname_fmt)
parser_code.append(code)
add_label: str | None = None add_label: str | None = None
for i, p in enumerate(parameters): for i, p in enumerate(parameters):
if isinstance(p.converter, defining_class_converter): if isinstance(p.converter, defining_class_converter):
@ -1325,8 +1418,6 @@ class CLanguage(Language):
parser_code.append("%s:" % add_label) parser_code.append("%s:" % add_label)
add_label = None add_label = None
if not p.is_optional(): if not p.is_optional():
if p.deprecated_positional:
deprecated_positionals[i] = p
parser_code.append(normalize_snippet(parsearg, indent=4)) parser_code.append(normalize_snippet(parsearg, indent=4))
elif i < pos_only: elif i < pos_only:
add_label = 'skip_optional_posonly' add_label = 'skip_optional_posonly'
@ -1356,8 +1447,6 @@ class CLanguage(Language):
goto %s; goto %s;
}} }}
""" % add_label, indent=4)) """ % add_label, indent=4))
if p.deprecated_positional:
deprecated_positionals[i] = p
if i + 1 == len(parameters): if i + 1 == len(parameters):
parser_code.append(normalize_snippet(parsearg, indent=4)) parser_code.append(normalize_snippet(parsearg, indent=4))
else: else:
@ -1373,12 +1462,6 @@ class CLanguage(Language):
}} }}
""" % add_label, indent=4)) """ % add_label, indent=4))
if deprecated_positionals:
code = self.deprecate_positional_use(f, deprecated_positionals)
assert parser_code is not None
# Insert the deprecation code before parameter parsing.
parser_code.insert(0, code)
if parser_code is not None: if parser_code is not None:
if add_label: if add_label:
parser_code.append("%s:" % add_label) parser_code.append("%s:" % add_label)
@ -1398,6 +1481,17 @@ class CLanguage(Language):
goto exit; goto exit;
}} }}
""", indent=4)] """, indent=4)]
if deprecated_positionals or deprecated_keywords:
declarations += "\nPy_ssize_t nargs = PyTuple_GET_SIZE(args);"
if deprecated_keywords:
code = self.deprecate_keyword_use(f, deprecated_keywords, None)
parser_code.append(code)
if deprecated_positionals:
code = self.deprecate_positional_use(f, deprecated_positionals)
# Insert the deprecation code before parameter parsing.
parser_code.insert(0, code)
parser_definition = parser_body(parser_prototype, *parser_code, parser_definition = parser_body(parser_prototype, *parser_code,
declarations=declarations) declarations=declarations)
@ -1478,6 +1572,10 @@ class CLanguage(Language):
parser_definition = parser_definition.replace("{return_value_declaration}", return_value_declaration) parser_definition = parser_definition.replace("{return_value_declaration}", return_value_declaration)
compiler_warning = self.compiler_deprecated_warning(f, parameters)
if compiler_warning:
parser_definition = compiler_warning + "\n\n" + parser_definition
d = { d = {
"docstring_prototype" : docstring_prototype, "docstring_prototype" : docstring_prototype,
"docstring_definition" : docstring_definition, "docstring_definition" : docstring_definition,
@ -2739,6 +2837,7 @@ class Parameter:
group: int = 0 group: int = 0
# (`None` signifies that there is no deprecation) # (`None` signifies that there is no deprecation)
deprecated_positional: VersionTuple | None = None deprecated_positional: VersionTuple | None = None
deprecated_keyword: VersionTuple | None = None
right_bracket_count: int = dc.field(init=False, default=0) right_bracket_count: int = dc.field(init=False, default=0)
def __repr__(self) -> str: def __repr__(self) -> str:
@ -4576,6 +4675,7 @@ class DSLParser:
keyword_only: bool keyword_only: bool
positional_only: bool positional_only: bool
deprecated_positional: VersionTuple | None deprecated_positional: VersionTuple | None
deprecated_keyword: VersionTuple | None
group: int group: int
parameter_state: ParamState parameter_state: ParamState
indent: IndentStack indent: IndentStack
@ -4583,11 +4683,7 @@ class DSLParser:
coexist: bool coexist: bool
parameter_continuation: str parameter_continuation: str
preserve_output: bool preserve_output: bool
star_from_version_re = create_regex( from_version_re = re.compile(r'([*/]) +\[from +(.+)\]')
before="* [from ",
after="]",
word=False,
)
def __init__(self, clinic: Clinic) -> None: def __init__(self, clinic: Clinic) -> None:
self.clinic = clinic self.clinic = clinic
@ -4612,6 +4708,7 @@ class DSLParser:
self.keyword_only = False self.keyword_only = False
self.positional_only = False self.positional_only = False
self.deprecated_positional = None self.deprecated_positional = None
self.deprecated_keyword = None
self.group = 0 self.group = 0
self.parameter_state: ParamState = ParamState.START self.parameter_state: ParamState = ParamState.START
self.indent = IndentStack() self.indent = IndentStack()
@ -5089,21 +5186,22 @@ class DSLParser:
return return
line = line.lstrip() line = line.lstrip()
match = self.star_from_version_re.match(line) version: VersionTuple | None = None
match = self.from_version_re.fullmatch(line)
if match: if match:
self.parse_deprecated_positional(match.group(1)) line = match[1]
return version = self.parse_version(match[2])
func = self.function func = self.function
match line: match line:
case '*': case '*':
self.parse_star(func) self.parse_star(func, version)
case '[': case '[':
self.parse_opening_square_bracket(func) self.parse_opening_square_bracket(func)
case ']': case ']':
self.parse_closing_square_bracket(func) self.parse_closing_square_bracket(func)
case '/': case '/':
self.parse_slash(func) self.parse_slash(func, version)
case param: case param:
self.parse_parameter(param) self.parse_parameter(param)
@ -5404,29 +5502,36 @@ class DSLParser:
"Annotations must be either a name, a function call, or a string." "Annotations must be either a name, a function call, or a string."
) )
def parse_deprecated_positional(self, thenceforth: str) -> None: def parse_version(self, thenceforth: str) -> VersionTuple:
"""Parse Python version in `[from ...]` marker."""
assert isinstance(self.function, Function) assert isinstance(self.function, Function)
fname = self.function.full_name
if self.keyword_only:
fail(f"Function {fname!r}: '* [from ...]' must come before '*'")
if self.deprecated_positional:
fail(f"Function {fname!r} uses '[from ...]' more than once.")
try: try:
major, minor = thenceforth.split(".") major, minor = thenceforth.split(".")
self.deprecated_positional = int(major), int(minor) return int(major), int(minor)
except ValueError: except ValueError:
fail( fail(
f"Function {fname!r}: expected format '* [from major.minor]' " f"Function {self.function.name!r}: expected format '[from major.minor]' "
f"where 'major' and 'minor' are integers; got {thenceforth!r}" f"where 'major' and 'minor' are integers; got {thenceforth!r}"
) )
def parse_star(self, function: Function) -> None: def parse_star(self, function: Function, version: VersionTuple | None) -> None:
"""Parse keyword-only parameter marker '*'.""" """Parse keyword-only parameter marker '*'.
if self.keyword_only:
fail(f"Function {function.name!r} uses '*' more than once.") The 'version' parameter signifies the future version from which
self.deprecated_positional = None the marker will take effect (None means it is already in effect).
self.keyword_only = True """
if version is None:
if self.keyword_only:
fail(f"Function {function.name!r} uses '*' more than once.")
self.check_remaining_star()
self.keyword_only = True
else:
if self.keyword_only:
fail(f"Function {function.name!r}: '* [from ...]' must come before '*'")
if self.deprecated_positional:
fail(f"Function {function.name!r} uses '* [from ...]' more than once.")
self.deprecated_positional = version
def parse_opening_square_bracket(self, function: Function) -> None: def parse_opening_square_bracket(self, function: Function) -> None:
"""Parse opening parameter group symbol '['.""" """Parse opening parameter group symbol '['."""
@ -5460,11 +5565,38 @@ class DSLParser:
f"has an unsupported group configuration. " f"has an unsupported group configuration. "
f"(Unexpected state {st}.c)") f"(Unexpected state {st}.c)")
def parse_slash(self, function: Function) -> None: def parse_slash(self, function: Function, version: VersionTuple | None) -> None:
"""Parse positional-only parameter marker '/'.""" """Parse positional-only parameter marker '/'.
if self.positional_only:
fail(f"Function {function.name!r} uses '/' more than once.") The 'version' parameter signifies the future version from which
the marker will take effect (None means it is already in effect).
"""
if version is None:
if self.deprecated_keyword:
fail(f"Function {function.name!r}: '/' must precede '/ [from ...]'")
if self.deprecated_positional:
fail(f"Function {function.name!r}: '/' must precede '* [from ...]'")
if self.keyword_only:
fail(f"Function {function.name!r}: '/' must precede '*'")
if self.positional_only:
fail(f"Function {function.name!r} uses '/' more than once.")
else:
if self.deprecated_keyword:
fail(f"Function {function.name!r} uses '/ [from ...]' more than once.")
if self.deprecated_positional:
fail(f"Function {function.name!r}: '/ [from ...]' must precede '* [from ...]'")
if self.keyword_only:
fail(f"Function {function.name!r}: '/ [from ...]' must precede '*'")
self.positional_only = True self.positional_only = True
self.deprecated_keyword = version
if version is not None:
found = False
for p in reversed(function.parameters.values()):
found = p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
break
if not found:
fail(f"Function {function.name!r} specifies '/ [from ...]' "
f"without preceding parameters.")
# REQUIRED and OPTIONAL are allowed here, that allows positional-only # REQUIRED and OPTIONAL are allowed here, that allows positional-only
# without option groups to work (and have default values!) # without option groups to work (and have default values!)
allowed = { allowed = {
@ -5476,19 +5608,13 @@ class DSLParser:
if (self.parameter_state not in allowed) or self.group: if (self.parameter_state not in allowed) or self.group:
fail(f"Function {function.name!r} has an unsupported group configuration. " fail(f"Function {function.name!r} has an unsupported group configuration. "
f"(Unexpected state {self.parameter_state}.d)") f"(Unexpected state {self.parameter_state}.d)")
if self.keyword_only:
fail(f"Function {function.name!r} mixes keyword-only and "
"positional-only parameters, which is unsupported.")
# fixup preceding parameters # fixup preceding parameters
for p in function.parameters.values(): for p in function.parameters.values():
if p.is_vararg(): if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
continue if version is None:
if (p.kind is not inspect.Parameter.POSITIONAL_OR_KEYWORD and p.kind = inspect.Parameter.POSITIONAL_ONLY
not isinstance(p.converter, self_converter) else:
): p.deprecated_keyword = version
fail(f"Function {function.name!r} mixes keyword-only and "
"positional-only parameters, which is unsupported.")
p.kind = inspect.Parameter.POSITIONAL_ONLY
def state_parameter_docstring_start(self, line: str) -> None: def state_parameter_docstring_start(self, line: str) -> None:
assert self.indent.margin is not None, "self.margin.infer() has not yet been called to set the margin" assert self.indent.margin is not None, "self.margin.infer() has not yet been called to set the margin"
@ -5773,6 +5899,29 @@ class DSLParser:
signature=signature, signature=signature,
parameters=parameters).rstrip() parameters=parameters).rstrip()
def check_remaining_star(self, lineno: int | None = None) -> None:
assert isinstance(self.function, Function)
if self.keyword_only:
symbol = '*'
elif self.deprecated_positional:
symbol = '* [from ...]'
else:
return
no_param_after_symbol = True
for p in reversed(self.function.parameters.values()):
if self.keyword_only:
if p.kind == inspect.Parameter.KEYWORD_ONLY:
return
elif self.deprecated_positional:
if p.deprecated_positional == self.deprecated_positional:
return
break
fail(f"Function {self.function.name!r} specifies {symbol!r} "
f"without following parameters.", line_number=lineno)
def do_post_block_processing_cleanup(self, lineno: int) -> None: def do_post_block_processing_cleanup(self, lineno: int) -> None:
""" """
Called when processing the block is done. Called when processing the block is done.
@ -5780,28 +5929,7 @@ class DSLParser:
if not self.function: if not self.function:
return return
def check_remaining( self.check_remaining_star(lineno)
symbol: str,
condition: Callable[[Parameter], bool]
) -> None:
assert isinstance(self.function, Function)
if values := self.function.parameters.values():
last_param = next(reversed(values))
no_param_after_symbol = condition(last_param)
else:
no_param_after_symbol = True
if no_param_after_symbol:
fname = self.function.full_name
fail(f"Function {fname!r} specifies {symbol!r} "
"without any parameters afterwards.", line_number=lineno)
if self.keyword_only:
check_remaining("*", lambda p: p.kind != inspect.Parameter.KEYWORD_ONLY)
if self.deprecated_positional:
check_remaining("* [from ...]", lambda p: not p.deprecated_positional)
self.function.docstring = self.format_docstring() self.function.docstring = self.format_docstring()