Skip to content

gh-95065: Produce nicer deprecation messages in Argument Clinic #107808

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
193 changes: 192 additions & 1 deletion Lib/test/clinic.test.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ output preset block
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=3c81ac2402d06a8b]*/

/*[clinic input]
module m
class m.T "TestObj *" "TestType"
class Test "TestObj *" "TestType"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=fc7e50384d12b83f]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=f761b4d55cb179cf]*/

/*[clinic input]
test_object_converter
Expand Down Expand Up @@ -6437,3 +6439,192 @@ test_deprecate_positional_pos2_len3_with_kwdonly_impl(PyObject *module,
PyObject *d,
PyObject *e)
/*[clinic end generated code: output=383d56b03f7c2dcb input=154fd450448d8935]*/


/*[clinic input]
@classmethod
Test.__new__
* [from 3.14]
a: object
The deprecation message should use the class name instead of __new__.
[clinic start generated code]*/

PyDoc_STRVAR(Test__doc__,
"Test(a)\n"
"--\n"
"\n"
"The deprecation message should use the class name instead of __new__.\n"
"\n"
"Note: Passing positional arguments to Test() is deprecated. Parameter\n"
"\'a\' will become a keyword-only parameter in Python 3.14.\n"
"");

static PyObject *
Test_impl(PyTypeObject *type, PyObject *a);

static PyObject *
Test(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)

#define NUM_KEYWORDS 1
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
PyObject *ob_item[NUM_KEYWORDS];
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_item = { &_Py_ID(a), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)

#else // !Py_BUILD_CORE
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE

static const char * const _keywords[] = {"a", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "Test",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[1];
PyObject * const *fastargs;
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
PyObject *a;

// Emit compiler warnings when we get to Python 3.14.
#if PY_VERSION_HEX >= 0x030e00C0
# error \
"In clinic.test.c, update parameter(s) 'a' in the clinic input of" \
" 'Test.__new__' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ( \
"In clinic.test.c, update parameter(s) 'a' in the clinic input of" \
" 'Test.__new__' to be keyword-only.")
# else
# warning \
"In clinic.test.c, update parameter(s) 'a' in the clinic input of" \
" 'Test.__new__' to be keyword-only."
# endif
#endif
if (nargs == 1) {
if (PyErr_WarnEx(PyExc_DeprecationWarning,
"Passing positional arguments to Test() is deprecated. Parameter "
"'a' will become a keyword-only parameter in Python 3.14.", 1))
{
goto exit;
}
}
fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 1, 1, 0, argsbuf);
if (!fastargs) {
goto exit;
}
a = fastargs[0];
return_value = Test_impl(type, a);

exit:
return return_value;
}

static PyObject *
Test_impl(PyTypeObject *type, PyObject *a)
/*[clinic end generated code: output=d15a69ea37ec6502 input=f133dc077aef49ec]*/


/*[clinic input]
m.T.__init__
* [from 3.14]
a: object
The deprecation message should use the class name instead of __init__.
[clinic start generated code]*/

PyDoc_STRVAR(m_T___init____doc__,
"T(a)\n"
"--\n"
"\n"
"The deprecation message should use the class name instead of __init__.\n"
"\n"
"Note: Passing positional arguments to m.T() is deprecated. Parameter\n"
"\'a\' will become a keyword-only parameter in Python 3.14.\n"
"");

static int
m_T___init___impl(TestObj *self, PyObject *a);

static int
m_T___init__(PyObject *self, PyObject *args, PyObject *kwargs)
{
int return_value = -1;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)

#define NUM_KEYWORDS 1
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
PyObject *ob_item[NUM_KEYWORDS];
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_item = { &_Py_ID(a), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)

#else // !Py_BUILD_CORE
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE

static const char * const _keywords[] = {"a", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "T",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[1];
PyObject * const *fastargs;
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
PyObject *a;

// Emit compiler warnings when we get to Python 3.14.
#if PY_VERSION_HEX >= 0x030e00C0
# error \
"In clinic.test.c, update parameter(s) 'a' in the clinic input of" \
" 'm.T.__init__' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ( \
"In clinic.test.c, update parameter(s) 'a' in the clinic input of" \
" 'm.T.__init__' to be keyword-only.")
# else
# warning \
"In clinic.test.c, update parameter(s) 'a' in the clinic input of" \
" 'm.T.__init__' to be keyword-only."
# endif
#endif
if (nargs == 1) {
if (PyErr_WarnEx(PyExc_DeprecationWarning,
"Passing positional arguments to m.T() is deprecated. Parameter "
"'a' will become a keyword-only parameter in Python 3.14.", 1))
{
goto exit;
}
}
fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 1, 1, 0, argsbuf);
if (!fastargs) {
goto exit;
}
a = fastargs[0];
return_value = m_T___init___impl((TestObj *)self, a);

exit:
return return_value;
}

static int
m_T___init___impl(TestObj *self, PyObject *a)
/*[clinic end generated code: output=ef43c425816a549f input=f71b51dbe19fa657]*/
36 changes: 19 additions & 17 deletions Tools/clinic/clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ def deprecate_positional_use(
if first_pos:
preamble = f"Passing {first_pos+1} positional arguments to "
depr_message = preamble + (
f"{func.full_name}() is deprecated. Parameter {pstr} will "
f"{func.fulldisplayname}() is deprecated. Parameter {pstr} will "
f"become a keyword-only parameter in Python {major}.{minor}."
)
else:
Expand All @@ -939,7 +939,7 @@ def deprecate_positional_use(
f"argument{'s' if first_pos != 1 else ''} to "
)
depr_message = preamble + (
f"{func.full_name}() is deprecated. Parameters {pstr} will "
f"{func.fulldisplayname}() is deprecated. Parameters {pstr} will "
f"become keyword-only parameters in Python {major}.{minor}."
)

Expand Down Expand Up @@ -1673,14 +1673,7 @@ def render_function(

full_name = f.full_name
template_dict = {'full_name': full_name}

if new_or_init:
assert isinstance(f.cls, Class)
name = f.cls.name
else:
name = f.name

template_dict['name'] = name
template_dict['name'] = f.displayname

if f.c_basename:
c_basename = f.c_basename
Expand Down Expand Up @@ -2678,6 +2671,21 @@ def __post_init__(self) -> None:
self.self_converter: self_converter | None = None
self.__render_parameters__: list[Parameter] | None = None

@functools.cached_property
def displayname(self) -> str:
"""Pretty-printable name."""
if self.kind.new_or_init:
assert isinstance(self.cls, Class)
return self.cls.name
else:
return self.name

@functools.cached_property
def fulldisplayname(self) -> str:
if isinstance(self.module, Module):
return f"{self.module.name}.{self.displayname}"
return self.displayname

@property
def render_parameters(self) -> list[Parameter]:
if not self.__render_parameters__:
Expand Down Expand Up @@ -5522,13 +5530,7 @@ def format_docstring_signature(
self, f: Function, parameters: list[Parameter]
) -> str:
text, add, output = _text_accumulator()
if f.kind.new_or_init:
# classes get *just* the name of the class
# not __new__, not __init__, and not module.classname
assert f.cls
add(f.cls.name)
else:
add(f.name)
add(f.displayname)
if self.forced_text_signature:
add(self.forced_text_signature)
else:
Expand Down