Skip to content

gh-113317: Argument Clinic: Add libclinic.function #116807

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 1 commit into from
Mar 14, 2024
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
248 changes: 8 additions & 240 deletions Tools/clinic/clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import builtins as bltns
import collections
import contextlib
import copy
import dataclasses as dc
import enum
import functools
Expand Down Expand Up @@ -50,7 +49,14 @@
# Local imports.
import libclinic
import libclinic.cpp
from libclinic import ClinicError, fail, warn
from libclinic import (
ClinicError, Sentinels, VersionTuple,
fail, warn, unspecified, unknown)
from libclinic.function import (
Module, Class, Function, Parameter,
ClassDict, ModuleDict, FunctionKind,
CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW,
GETTER, SETTER)


# TODO:
Expand All @@ -70,18 +76,6 @@
LIMITED_CAPI_REGEX = re.compile(r'# *define +Py_LIMITED_API')


class Sentinels(enum.Enum):
unspecified = "unspecified"
unknown = "unknown"

def __repr__(self) -> str:
return f"<{self.value.capitalize()}>"


unspecified: Final = Sentinels.unspecified
unknown: Final = Sentinels.unknown


# This one needs to be a distinct class, unlike the other two
class Null:
def __repr__(self) -> str:
Expand Down Expand Up @@ -2096,9 +2090,7 @@ def dump(self) -> str:
extensions['py'] = PythonLanguage


ClassDict = dict[str, "Class"]
DestinationDict = dict[str, Destination]
ModuleDict = dict[str, "Module"]


class Parser(Protocol):
Expand Down Expand Up @@ -2418,38 +2410,6 @@ def parse(self, block: Block) -> None:
block.output = s.getvalue()


@dc.dataclass(repr=False)
class Module:
name: str
module: Module | Clinic

def __post_init__(self) -> None:
self.parent = self.module
self.modules: ModuleDict = {}
self.classes: ClassDict = {}
self.functions: list[Function] = []

def __repr__(self) -> str:
return "<clinic.Module " + repr(self.name) + " at " + str(id(self)) + ">"


@dc.dataclass(repr=False)
class Class:
name: str
module: Module | Clinic
cls: Class | None
typedef: str
type_object: str

def __post_init__(self) -> None:
self.parent = self.cls or self.module
self.classes: ClassDict = {}
self.functions: list[Function] = []

def __repr__(self) -> str:
return "<clinic.Class " + repr(self.name) + " at " + str(id(self)) + ">"


unsupported_special_methods: set[str] = set("""

__abs__
Expand Down Expand Up @@ -2522,201 +2482,9 @@ def __repr__(self) -> str:
""".strip().split())


class FunctionKind(enum.Enum):
INVALID = enum.auto()
CALLABLE = enum.auto()
STATIC_METHOD = enum.auto()
CLASS_METHOD = enum.auto()
METHOD_INIT = enum.auto()
METHOD_NEW = enum.auto()
GETTER = enum.auto()
SETTER = enum.auto()

@functools.cached_property
def new_or_init(self) -> bool:
return self in {FunctionKind.METHOD_INIT, FunctionKind.METHOD_NEW}

def __repr__(self) -> str:
return f"<clinic.FunctionKind.{self.name}>"


INVALID: Final = FunctionKind.INVALID
CALLABLE: Final = FunctionKind.CALLABLE
STATIC_METHOD: Final = FunctionKind.STATIC_METHOD
CLASS_METHOD: Final = FunctionKind.CLASS_METHOD
METHOD_INIT: Final = FunctionKind.METHOD_INIT
METHOD_NEW: Final = FunctionKind.METHOD_NEW
GETTER: Final = FunctionKind.GETTER
SETTER: Final = FunctionKind.SETTER

ParamDict = dict[str, "Parameter"]
ReturnConverterType = Callable[..., "CReturnConverter"]


@dc.dataclass(repr=False)
class Function:
"""
Mutable duck type for inspect.Function.

docstring - a str containing
* embedded line breaks
* text outdented to the left margin
* no trailing whitespace.
It will always be true that
(not docstring) or ((not docstring[0].isspace()) and (docstring.rstrip() == docstring))
"""
parameters: ParamDict = dc.field(default_factory=dict)
_: dc.KW_ONLY
name: str
module: Module | Clinic
cls: Class | None
c_basename: str
full_name: str
return_converter: CReturnConverter
kind: FunctionKind
coexist: bool
return_annotation: object = inspect.Signature.empty
docstring: str = ''
# docstring_only means "don't generate a machine-readable
# signature, just a normal docstring". it's True for
# functions with optional groups because we can't represent
# those accurately with inspect.Signature in 3.4.
docstring_only: bool = False
critical_section: bool = False
target_critical_section: list[str] = dc.field(default_factory=list)

def __post_init__(self) -> None:
self.parent = self.cls or self.module
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:
parent: Class | Module | Clinic | None
if self.kind.new_or_init:
parent = getattr(self.cls, "parent", None)
else:
parent = self.parent
name = self.displayname
while isinstance(parent, (Module, Class)):
name = f"{parent.name}.{name}"
parent = parent.parent
return name

@property
def render_parameters(self) -> list[Parameter]:
if not self.__render_parameters__:
l: list[Parameter] = []
self.__render_parameters__ = l
for p in self.parameters.values():
p = p.copy()
p.converter.pre_render()
l.append(p)
return self.__render_parameters__

@property
def methoddef_flags(self) -> str | None:
if self.kind.new_or_init:
return None
flags = []
match self.kind:
case FunctionKind.CLASS_METHOD:
flags.append('METH_CLASS')
case FunctionKind.STATIC_METHOD:
flags.append('METH_STATIC')
case _ as kind:
acceptable_kinds = {FunctionKind.CALLABLE, FunctionKind.GETTER, FunctionKind.SETTER}
assert kind in acceptable_kinds, f"unknown kind: {kind!r}"
if self.coexist:
flags.append('METH_COEXIST')
return '|'.join(flags)

def __repr__(self) -> str:
return f'<clinic.Function {self.name!r}>'

def copy(self, **overrides: Any) -> Function:
f = dc.replace(self, **overrides)
f.parameters = {
name: value.copy(function=f)
for name, value in f.parameters.items()
}
return f


VersionTuple = tuple[int, int]


@dc.dataclass(repr=False, slots=True)
class Parameter:
"""
Mutable duck type of inspect.Parameter.
"""
name: str
kind: inspect._ParameterKind
_: dc.KW_ONLY
default: object = inspect.Parameter.empty
function: Function
converter: CConverter
annotation: object = inspect.Parameter.empty
docstring: str = ''
group: int = 0
# (`None` signifies that there is no deprecation)
deprecated_positional: VersionTuple | None = None
deprecated_keyword: VersionTuple | None = None
right_bracket_count: int = dc.field(init=False, default=0)

def __repr__(self) -> str:
return f'<clinic.Parameter {self.name!r}>'

def is_keyword_only(self) -> bool:
return self.kind == inspect.Parameter.KEYWORD_ONLY

def is_positional_only(self) -> bool:
return self.kind == inspect.Parameter.POSITIONAL_ONLY

def is_vararg(self) -> bool:
return self.kind == inspect.Parameter.VAR_POSITIONAL

def is_optional(self) -> bool:
return not self.is_vararg() and (self.default is not unspecified)

def copy(
self,
/,
*,
converter: CConverter | None = None,
function: Function | None = None,
**overrides: Any
) -> Parameter:
function = function or self.function
if not converter:
converter = copy.copy(self.converter)
converter.function = function
return dc.replace(self, **overrides, function=function, converter=converter)

def get_displayname(self, i: int) -> str:
if i == 0:
return 'argument'
if not self.is_positional_only():
return f'argument {self.name!r}'
else:
return f'argument {i}'

def render_docstring(self) -> str:
lines = [f" {self.name}"]
lines.extend(f" {line}" for line in self.docstring.split("\n"))
return "\n".join(lines).rstrip()


CConverterClassT = TypeVar("CConverterClassT", bound=type["CConverter"])

def add_c_converter(
Expand Down
8 changes: 8 additions & 0 deletions Tools/clinic/libclinic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
compute_checksum,
create_regex,
write_file,
VersionTuple,
Sentinels,
unspecified,
unknown,
)


Expand Down Expand Up @@ -60,6 +64,10 @@
"compute_checksum",
"create_regex",
"write_file",
"VersionTuple",
"Sentinels",
"unspecified",
"unknown",
]


Expand Down
Loading