Skip to content

Commit

Permalink
gh-113317: Change how Argument Clinic lists converters (#116853)
Browse files Browse the repository at this point in the history
* Add a new create_parser_namespace() function for
  PythonParser to pass objects to executed code.
* In run_clinic(), list converters using 'converters' and
  'return_converters' dictionarties.
* test_clinic: add 'object()' return converter.
* Use also create_parser_namespace() in eval_ast_expr().

Co-authored-by: Erlend E. Aasland <erlend@python.org>
  • Loading branch information
vstinner and erlend-aasland committed Mar 27, 2024
1 parent 669ef49 commit 7aa89bc
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 32 deletions.
1 change: 1 addition & 0 deletions Lib/test/test_clinic.py
Expand Up @@ -2657,6 +2657,7 @@ def test_cli_converters(self):
float()
int()
long()
object()
Py_ssize_t()
size_t()
unsigned_int()
Expand Down
86 changes: 54 additions & 32 deletions Tools/clinic/clinic.py
Expand Up @@ -56,7 +56,7 @@
from libclinic.block_parser import Block, BlockParser
from libclinic.crenderdata import CRenderData, Include, TemplateDict
from libclinic.converter import (
CConverter, CConverterClassT,
CConverter, CConverterClassT, ConverterType,
converters, legacy_converters)


Expand Down Expand Up @@ -1988,13 +1988,38 @@ def parse_file(
libclinic.write_file(output, cooked)


@functools.cache
def _create_parser_base_namespace() -> dict[str, Any]:
ns = dict(
CConverter=CConverter,
CReturnConverter=CReturnConverter,
buffer=buffer,
robuffer=robuffer,
rwbuffer=rwbuffer,
unspecified=unspecified,
NoneType=NoneType,
)
for name, converter in converters.items():
ns[f'{name}_converter'] = converter
for name, return_converter in return_converters.items():
ns[f'{name}_return_converter'] = return_converter
return ns


def create_parser_namespace() -> dict[str, Any]:
base_namespace = _create_parser_base_namespace()
return base_namespace.copy()



class PythonParser:
def __init__(self, clinic: Clinic) -> None:
pass

def parse(self, block: Block) -> None:
namespace = create_parser_namespace()
with contextlib.redirect_stdout(io.StringIO()) as s:
exec(block.input)
exec(block.input, namespace)
block.output = s.getvalue()


Expand Down Expand Up @@ -3443,7 +3468,6 @@ class float_return_converter(double_return_converter):

def eval_ast_expr(
node: ast.expr,
globals: dict[str, Any],
*,
filename: str = '-'
) -> Any:
Expand All @@ -3460,8 +3484,9 @@ def eval_ast_expr(
node = node.value

expr = ast.Expression(node)
namespace = create_parser_namespace()
co = compile(expr, filename, 'eval')
fn = FunctionType(co, globals)
fn = FunctionType(co, namespace)
return fn()


Expand Down Expand Up @@ -4463,12 +4488,11 @@ def parse_converter(
case ast.Name(name):
return name, False, {}
case ast.Call(func=ast.Name(name)):
symbols = globals()
kwargs: ConverterArgs = {}
for node in annotation.keywords:
if not isinstance(node.arg, str):
fail("Cannot use a kwarg splat in a function-call annotation")
kwargs[node.arg] = eval_ast_expr(node.value, symbols)
kwargs[node.arg] = eval_ast_expr(node.value)
return name, False, kwargs
case _:
fail(
Expand Down Expand Up @@ -4984,25 +5008,21 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
parser.error(
"can't specify --converters and a filename at the same time"
)
converters: list[tuple[str, str]] = []
return_converters: list[tuple[str, str]] = []
ignored = set("""
add_c_converter
add_c_return_converter
add_default_legacy_c_converter
add_legacy_c_converter
""".strip().split())
module = globals()
for name in module:
for suffix, ids in (
("_return_converter", return_converters),
("_converter", converters),
):
if name in ignored:
continue
if name.endswith(suffix):
ids.append((name, name.removesuffix(suffix)))
break
AnyConverterType = ConverterType | ReturnConverterType
converter_list: list[tuple[str, AnyConverterType]] = []
return_converter_list: list[tuple[str, AnyConverterType]] = []

for name, converter in converters.items():
converter_list.append((
name,
converter,
))
for name, return_converter in return_converters.items():
return_converter_list.append((
name,
return_converter
))

print()

print("Legacy converters:")
Expand All @@ -5012,15 +5032,17 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
print()

for title, attribute, ids in (
("Converters", 'converter_init', converters),
("Return converters", 'return_converter_init', return_converters),
("Converters", 'converter_init', converter_list),
("Return converters", 'return_converter_init', return_converter_list),
):
print(title + ":")

ids.sort(key=lambda item: item[0].lower())
longest = -1
for name, short_name in ids:
longest = max(longest, len(short_name))
for name, short_name in sorted(ids, key=lambda x: x[1].lower()):
cls = module[name]
for name, _ in ids:
longest = max(longest, len(name))

for name, cls in ids:
callable = getattr(cls, attribute, None)
if not callable:
continue
Expand All @@ -5033,7 +5055,7 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
else:
s = parameter_name
parameters.append(s)
print(' {}({})'.format(short_name, ', '.join(parameters)))
print(' {}({})'.format(name, ', '.join(parameters)))
print()
print("All converters also accept (c_default=None, py_default=None, annotation=None).")
print("All return converters also accept (py_default=None).")
Expand Down

0 comments on commit 7aa89bc

Please sign in to comment.