gh-86457: Fix signature for code.replace() (GH-23199)

Also add support of @text_signature in Argument Clinic.
This commit is contained in:
Serhiy Storchaka 2023-08-07 23:34:53 +03:00 committed by GitHub
parent bea5f93196
commit 0e6e32fb84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 173 additions and 168 deletions

View File

@ -0,0 +1,2 @@
Argument Clinic now supports overriding automatically generated signature by
using directive `@text_signature`.

View File

@ -154,12 +154,7 @@ exit:
} }
PyDoc_STRVAR(code_replace__doc__, PyDoc_STRVAR(code_replace__doc__,
"replace($self, /, *, co_argcount=-1, co_posonlyargcount=-1,\n" "replace($self, /, **changes)\n"
" co_kwonlyargcount=-1, co_nlocals=-1, co_stacksize=-1,\n"
" co_flags=-1, co_firstlineno=-1, co_code=None, co_consts=None,\n"
" co_names=None, co_varnames=None, co_freevars=None,\n"
" co_cellvars=None, co_filename=None, co_name=None,\n"
" co_qualname=None, co_linetable=None, co_exceptiontable=None)\n"
"--\n" "--\n"
"\n" "\n"
"Return a copy of the code object with new values for the specified fields."); "Return a copy of the code object with new values for the specified fields.");
@ -171,13 +166,12 @@ static PyObject *
code_replace_impl(PyCodeObject *self, int co_argcount, code_replace_impl(PyCodeObject *self, int co_argcount,
int co_posonlyargcount, int co_kwonlyargcount, int co_posonlyargcount, int co_kwonlyargcount,
int co_nlocals, int co_stacksize, int co_flags, int co_nlocals, int co_stacksize, int co_flags,
int co_firstlineno, PyBytesObject *co_code, int co_firstlineno, PyObject *co_code, PyObject *co_consts,
PyObject *co_consts, PyObject *co_names, PyObject *co_names, PyObject *co_varnames,
PyObject *co_varnames, PyObject *co_freevars, PyObject *co_freevars, PyObject *co_cellvars,
PyObject *co_cellvars, PyObject *co_filename, PyObject *co_filename, PyObject *co_name,
PyObject *co_name, PyObject *co_qualname, PyObject *co_qualname, PyObject *co_linetable,
PyBytesObject *co_linetable, PyObject *co_exceptiontable);
PyBytesObject *co_exceptiontable);
static PyObject * static PyObject *
code_replace(PyCodeObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) code_replace(PyCodeObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
@ -217,7 +211,7 @@ code_replace(PyCodeObject *self, PyObject *const *args, Py_ssize_t nargs, PyObje
int co_stacksize = self->co_stacksize; int co_stacksize = self->co_stacksize;
int co_flags = self->co_flags; int co_flags = self->co_flags;
int co_firstlineno = self->co_firstlineno; int co_firstlineno = self->co_firstlineno;
PyBytesObject *co_code = NULL; PyObject *co_code = NULL;
PyObject *co_consts = self->co_consts; PyObject *co_consts = self->co_consts;
PyObject *co_names = self->co_names; PyObject *co_names = self->co_names;
PyObject *co_varnames = NULL; PyObject *co_varnames = NULL;
@ -226,8 +220,8 @@ code_replace(PyCodeObject *self, PyObject *const *args, Py_ssize_t nargs, PyObje
PyObject *co_filename = self->co_filename; PyObject *co_filename = self->co_filename;
PyObject *co_name = self->co_name; PyObject *co_name = self->co_name;
PyObject *co_qualname = self->co_qualname; PyObject *co_qualname = self->co_qualname;
PyBytesObject *co_linetable = (PyBytesObject *)self->co_linetable; PyObject *co_linetable = self->co_linetable;
PyBytesObject *co_exceptiontable = (PyBytesObject *)self->co_exceptiontable; PyObject *co_exceptiontable = self->co_exceptiontable;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 0, 0, 0, argsbuf); args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 0, 0, 0, argsbuf);
if (!args) { if (!args) {
@ -304,7 +298,7 @@ code_replace(PyCodeObject *self, PyObject *const *args, Py_ssize_t nargs, PyObje
_PyArg_BadArgument("replace", "argument 'co_code'", "bytes", args[7]); _PyArg_BadArgument("replace", "argument 'co_code'", "bytes", args[7]);
goto exit; goto exit;
} }
co_code = (PyBytesObject *)args[7]; co_code = args[7];
if (!--noptargs) { if (!--noptargs) {
goto skip_optional_kwonly; goto skip_optional_kwonly;
} }
@ -394,7 +388,7 @@ code_replace(PyCodeObject *self, PyObject *const *args, Py_ssize_t nargs, PyObje
_PyArg_BadArgument("replace", "argument 'co_linetable'", "bytes", args[16]); _PyArg_BadArgument("replace", "argument 'co_linetable'", "bytes", args[16]);
goto exit; goto exit;
} }
co_linetable = (PyBytesObject *)args[16]; co_linetable = args[16];
if (!--noptargs) { if (!--noptargs) {
goto skip_optional_kwonly; goto skip_optional_kwonly;
} }
@ -403,7 +397,7 @@ code_replace(PyCodeObject *self, PyObject *const *args, Py_ssize_t nargs, PyObje
_PyArg_BadArgument("replace", "argument 'co_exceptiontable'", "bytes", args[17]); _PyArg_BadArgument("replace", "argument 'co_exceptiontable'", "bytes", args[17]);
goto exit; goto exit;
} }
co_exceptiontable = (PyBytesObject *)args[17]; co_exceptiontable = args[17];
skip_optional_kwonly: skip_optional_kwonly:
return_value = code_replace_impl(self, co_argcount, co_posonlyargcount, co_kwonlyargcount, co_nlocals, co_stacksize, co_flags, co_firstlineno, co_code, co_consts, co_names, co_varnames, co_freevars, co_cellvars, co_filename, co_name, co_qualname, co_linetable, co_exceptiontable); return_value = code_replace_impl(self, co_argcount, co_posonlyargcount, co_kwonlyargcount, co_nlocals, co_stacksize, co_flags, co_firstlineno, co_code, co_consts, co_names, co_varnames, co_freevars, co_cellvars, co_filename, co_name, co_qualname, co_linetable, co_exceptiontable);
@ -470,4 +464,4 @@ code__varname_from_oparg(PyCodeObject *self, PyObject *const *args, Py_ssize_t n
exit: exit:
return return_value; return return_value;
} }
/*[clinic end generated code: output=4ca4c0c403dbfa71 input=a9049054013a1b77]*/ /*[clinic end generated code: output=16c95266bbc4bc03 input=a9049054013a1b77]*/

View File

@ -1968,27 +1968,28 @@ code_linesiterator(PyCodeObject *code, PyObject *Py_UNUSED(args))
} }
/*[clinic input] /*[clinic input]
@text_signature "($self, /, **changes)"
code.replace code.replace
* *
co_argcount: int(c_default="self->co_argcount") = -1 co_argcount: int(c_default="self->co_argcount") = unchanged
co_posonlyargcount: int(c_default="self->co_posonlyargcount") = -1 co_posonlyargcount: int(c_default="self->co_posonlyargcount") = unchanged
co_kwonlyargcount: int(c_default="self->co_kwonlyargcount") = -1 co_kwonlyargcount: int(c_default="self->co_kwonlyargcount") = unchanged
co_nlocals: int(c_default="self->co_nlocals") = -1 co_nlocals: int(c_default="self->co_nlocals") = unchanged
co_stacksize: int(c_default="self->co_stacksize") = -1 co_stacksize: int(c_default="self->co_stacksize") = unchanged
co_flags: int(c_default="self->co_flags") = -1 co_flags: int(c_default="self->co_flags") = unchanged
co_firstlineno: int(c_default="self->co_firstlineno") = -1 co_firstlineno: int(c_default="self->co_firstlineno") = unchanged
co_code: PyBytesObject(c_default="NULL") = None co_code: object(subclass_of="&PyBytes_Type", c_default="NULL") = unchanged
co_consts: object(subclass_of="&PyTuple_Type", c_default="self->co_consts") = None co_consts: object(subclass_of="&PyTuple_Type", c_default="self->co_consts") = unchanged
co_names: object(subclass_of="&PyTuple_Type", c_default="self->co_names") = None co_names: object(subclass_of="&PyTuple_Type", c_default="self->co_names") = unchanged
co_varnames: object(subclass_of="&PyTuple_Type", c_default="NULL") = None co_varnames: object(subclass_of="&PyTuple_Type", c_default="NULL") = unchanged
co_freevars: object(subclass_of="&PyTuple_Type", c_default="NULL") = None co_freevars: object(subclass_of="&PyTuple_Type", c_default="NULL") = unchanged
co_cellvars: object(subclass_of="&PyTuple_Type", c_default="NULL") = None co_cellvars: object(subclass_of="&PyTuple_Type", c_default="NULL") = unchanged
co_filename: unicode(c_default="self->co_filename") = None co_filename: unicode(c_default="self->co_filename") = unchanged
co_name: unicode(c_default="self->co_name") = None co_name: unicode(c_default="self->co_name") = unchanged
co_qualname: unicode(c_default="self->co_qualname") = None co_qualname: unicode(c_default="self->co_qualname") = unchanged
co_linetable: PyBytesObject(c_default="(PyBytesObject *)self->co_linetable") = None co_linetable: object(subclass_of="&PyBytes_Type", c_default="self->co_linetable") = unchanged
co_exceptiontable: PyBytesObject(c_default="(PyBytesObject *)self->co_exceptiontable") = None co_exceptiontable: object(subclass_of="&PyBytes_Type", c_default="self->co_exceptiontable") = unchanged
Return a copy of the code object with new values for the specified fields. Return a copy of the code object with new values for the specified fields.
[clinic start generated code]*/ [clinic start generated code]*/
@ -1997,14 +1998,13 @@ static PyObject *
code_replace_impl(PyCodeObject *self, int co_argcount, code_replace_impl(PyCodeObject *self, int co_argcount,
int co_posonlyargcount, int co_kwonlyargcount, int co_posonlyargcount, int co_kwonlyargcount,
int co_nlocals, int co_stacksize, int co_flags, int co_nlocals, int co_stacksize, int co_flags,
int co_firstlineno, PyBytesObject *co_code, int co_firstlineno, PyObject *co_code, PyObject *co_consts,
PyObject *co_consts, PyObject *co_names, PyObject *co_names, PyObject *co_varnames,
PyObject *co_varnames, PyObject *co_freevars, PyObject *co_freevars, PyObject *co_cellvars,
PyObject *co_cellvars, PyObject *co_filename, PyObject *co_filename, PyObject *co_name,
PyObject *co_name, PyObject *co_qualname, PyObject *co_qualname, PyObject *co_linetable,
PyBytesObject *co_linetable, PyObject *co_exceptiontable)
PyBytesObject *co_exceptiontable) /*[clinic end generated code: output=e75c48a15def18b9 input=18e280e07846c122]*/
/*[clinic end generated code: output=b6cd9988391d5711 input=f6f68e03571f8d7c]*/
{ {
#define CHECK_INT_ARG(ARG) \ #define CHECK_INT_ARG(ARG) \
if (ARG < 0) { \ if (ARG < 0) { \
@ -2029,7 +2029,7 @@ code_replace_impl(PyCodeObject *self, int co_argcount,
if (code == NULL) { if (code == NULL) {
return NULL; return NULL;
} }
co_code = (PyBytesObject *)code; co_code = code;
} }
if (PySys_Audit("code.__new__", "OOOiiiiii", if (PySys_Audit("code.__new__", "OOOiiiiii",
@ -2068,10 +2068,10 @@ code_replace_impl(PyCodeObject *self, int co_argcount,
co = PyCode_NewWithPosOnlyArgs( co = PyCode_NewWithPosOnlyArgs(
co_argcount, co_posonlyargcount, co_kwonlyargcount, co_nlocals, co_argcount, co_posonlyargcount, co_kwonlyargcount, co_nlocals,
co_stacksize, co_flags, (PyObject*)co_code, co_consts, co_names, co_stacksize, co_flags, co_code, co_consts, co_names,
co_varnames, co_freevars, co_cellvars, co_filename, co_name, co_varnames, co_freevars, co_cellvars, co_filename, co_name,
co_qualname, co_firstlineno, co_qualname, co_firstlineno,
(PyObject*)co_linetable, (PyObject*)co_exceptiontable); co_linetable, co_exceptiontable);
error: error:
Py_XDECREF(code); Py_XDECREF(code);

View File

@ -4595,6 +4595,7 @@ class DSLParser:
self.indent = IndentStack() self.indent = IndentStack()
self.kind = CALLABLE self.kind = CALLABLE
self.coexist = False self.coexist = False
self.forced_text_signature: str | None = None
self.parameter_continuation = '' self.parameter_continuation = ''
self.preserve_output = False self.preserve_output = False
@ -4735,6 +4736,11 @@ class DSLParser:
fail("Called @coexist twice!") fail("Called @coexist twice!")
self.coexist = True self.coexist = True
def at_text_signature(self, text_signature: str) -> None:
if self.forced_text_signature:
fail("Called @text_signature twice!")
self.forced_text_signature = text_signature
def parse(self, block: Block) -> None: def parse(self, block: Block) -> None:
self.reset() self.reset()
self.block = block self.block = block
@ -5515,142 +5521,145 @@ class DSLParser:
add(f.cls.name) add(f.cls.name)
else: else:
add(f.name) add(f.name)
add('(') if self.forced_text_signature:
add(self.forced_text_signature)
else:
add('(')
# populate "right_bracket_count" field for every parameter # populate "right_bracket_count" field for every parameter
assert parameters, "We should always have a self parameter. " + repr(f) assert parameters, "We should always have a self parameter. " + repr(f)
assert isinstance(parameters[0].converter, self_converter) assert isinstance(parameters[0].converter, self_converter)
# self is always positional-only. # self is always positional-only.
assert parameters[0].is_positional_only() assert parameters[0].is_positional_only()
assert parameters[0].right_bracket_count == 0 assert parameters[0].right_bracket_count == 0
positional_only = True positional_only = True
for p in parameters[1:]: for p in parameters[1:]:
if not p.is_positional_only(): if not p.is_positional_only():
positional_only = False positional_only = False
else: else:
assert positional_only assert positional_only
if positional_only: if positional_only:
p.right_bracket_count = abs(p.group) p.right_bracket_count = abs(p.group)
else: else:
# don't put any right brackets around non-positional-only parameters, ever. # don't put any right brackets around non-positional-only parameters, ever.
p.right_bracket_count = 0 p.right_bracket_count = 0
right_bracket_count = 0 right_bracket_count = 0
def fix_right_bracket_count(desired: int) -> str: def fix_right_bracket_count(desired: int) -> str:
nonlocal right_bracket_count nonlocal right_bracket_count
s = '' s = ''
while right_bracket_count < desired: while right_bracket_count < desired:
s += '[' s += '['
right_bracket_count += 1 right_bracket_count += 1
while right_bracket_count > desired: while right_bracket_count > desired:
s += ']' s += ']'
right_bracket_count -= 1 right_bracket_count -= 1
return s return s
need_slash = False need_slash = False
added_slash = False added_slash = False
need_a_trailing_slash = False need_a_trailing_slash = False
# we only need a trailing slash: # we only need a trailing slash:
# * if this is not a "docstring_only" signature # * if this is not a "docstring_only" signature
# * and if the last *shown* parameter is # * and if the last *shown* parameter is
# positional only # positional only
if not f.docstring_only: if not f.docstring_only:
for p in reversed(parameters): for p in reversed(parameters):
if not p.converter.show_in_signature:
continue
if p.is_positional_only():
need_a_trailing_slash = True
break
added_star = False
first_parameter = True
last_p = parameters[-1]
line_length = len(''.join(text))
indent = " " * line_length
def add_parameter(text: str) -> None:
nonlocal line_length
nonlocal first_parameter
if first_parameter:
s = text
first_parameter = False
else:
s = ' ' + text
if line_length + len(s) >= 72:
add('\n')
add(indent)
line_length = len(indent)
s = text
line_length += len(s)
add(s)
for p in parameters:
if not p.converter.show_in_signature: if not p.converter.show_in_signature:
continue continue
assert p.name
is_self = isinstance(p.converter, self_converter)
if is_self and f.docstring_only:
# this isn't a real machine-parsable signature,
# so let's not print the "self" parameter
continue
if p.is_positional_only(): if p.is_positional_only():
need_a_trailing_slash = True need_slash = not f.docstring_only
break elif need_slash and not (added_slash or p.is_positional_only()):
added_slash = True
add_parameter('/,')
if p.is_keyword_only() and not added_star:
added_star = True
add_parameter('*,')
added_star = False p_add, p_output = text_accumulator()
p_add(fix_right_bracket_count(p.right_bracket_count))
first_parameter = True if isinstance(p.converter, self_converter):
last_p = parameters[-1] # annotate first parameter as being a "self".
line_length = len(''.join(text)) #
indent = " " * line_length # if inspect.Signature gets this function,
def add_parameter(text: str) -> None: # and it's already bound, the self parameter
nonlocal line_length # will be stripped off.
nonlocal first_parameter #
if first_parameter: # if it's not bound, it should be marked
s = text # as positional-only.
first_parameter = False #
else: # note: we don't print "self" for __init__,
s = ' ' + text # because this isn't actually the signature
if line_length + len(s) >= 72: # for __init__. (it can't be, __init__ doesn't
add('\n') # have a docstring.) if this is an __init__
add(indent) # (or __new__), then this signature is for
line_length = len(indent) # calling the class to construct a new instance.
s = text p_add('$')
line_length += len(s)
add(s)
for p in parameters: if p.is_vararg():
if not p.converter.show_in_signature: p_add("*")
continue
assert p.name
is_self = isinstance(p.converter, self_converter) name = p.converter.signature_name or p.name
if is_self and f.docstring_only: p_add(name)
# this isn't a real machine-parsable signature,
# so let's not print the "self" parameter
continue
if p.is_positional_only(): if not p.is_vararg() and p.converter.is_optional():
need_slash = not f.docstring_only p_add('=')
elif need_slash and not (added_slash or p.is_positional_only()): value = p.converter.py_default
added_slash = True if not value:
add_parameter('/,') value = repr(p.converter.default)
p_add(value)
if p.is_keyword_only() and not added_star: if (p != last_p) or need_a_trailing_slash:
added_star = True p_add(',')
add_parameter('*,')
p_add, p_output = text_accumulator() add_parameter(p_output())
p_add(fix_right_bracket_count(p.right_bracket_count))
if isinstance(p.converter, self_converter): add(fix_right_bracket_count(0))
# annotate first parameter as being a "self". if need_a_trailing_slash:
# add_parameter('/')
# if inspect.Signature gets this function, add(')')
# and it's already bound, the self parameter
# will be stripped off.
#
# if it's not bound, it should be marked
# as positional-only.
#
# note: we don't print "self" for __init__,
# because this isn't actually the signature
# for __init__. (it can't be, __init__ doesn't
# have a docstring.) if this is an __init__
# (or __new__), then this signature is for
# calling the class to construct a new instance.
p_add('$')
if p.is_vararg():
p_add("*")
name = p.converter.signature_name or p.name
p_add(name)
if not p.is_vararg() and p.converter.is_optional():
p_add('=')
value = p.converter.py_default
if not value:
value = repr(p.converter.default)
p_add(value)
if (p != last_p) or need_a_trailing_slash:
p_add(',')
add_parameter(p_output())
add(fix_right_bracket_count(0))
if need_a_trailing_slash:
add_parameter('/')
add(')')
# PEP 8 says: # PEP 8 says:
# #