gh-113317, AC: Add libclinic.function (#116807)

Move Module, Class, Function and Parameter classes to a new
libclinic.function module.

Move VersionTuple and Sentinels to libclinic.utils.
This commit is contained in:
Victor Stinner 2024-03-14 15:37:22 +01:00 committed by GitHub
parent a76288ad9b
commit b1236a4410
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 270 additions and 241 deletions

View file

@ -12,7 +12,6 @@
import builtins as bltns
import collections
import contextlib
import copy
import dataclasses as dc
import enum
import functools
@ -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:
@ -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:
@ -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):
@ -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__
@ -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(

View file

@ -28,6 +28,10 @@
compute_checksum,
create_regex,
write_file,
VersionTuple,
Sentinels,
unspecified,
unknown,
)
@ -60,6 +64,10 @@
"compute_checksum",
"create_regex",
"write_file",
"VersionTuple",
"Sentinels",
"unspecified",
"unknown",
]

View file

@ -0,0 +1,237 @@
from __future__ import annotations
import dataclasses as dc
import copy
import enum
import functools
import inspect
from typing import Final, Any, TYPE_CHECKING
if TYPE_CHECKING:
from clinic import Clinic, CConverter, CReturnConverter, self_converter
from libclinic import VersionTuple, unspecified
ClassDict = dict[str, "Class"]
ModuleDict = dict[str, "Module"]
ParamDict = dict[str, "Parameter"]
@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)) + ">"
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
@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
@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()

View file

@ -1,9 +1,10 @@
import collections
import enum
import hashlib
import os
import re
import string
from typing import Literal
from typing import Literal, Final
def write_file(filename: str, new_contents: str) -> None:
@ -66,3 +67,18 @@ def get_value(
) -> Literal[""]:
self.counts[key] += 1
return ""
VersionTuple = tuple[int, int]
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