2024-03-14 11:37:22 -03:00
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses as dc
|
|
|
|
import copy
|
|
|
|
import enum
|
|
|
|
import functools
|
|
|
|
import inspect
|
2024-04-03 15:17:51 -03:00
|
|
|
from collections.abc import Iterable, Iterator, Sequence
|
2024-03-14 11:37:22 -03:00
|
|
|
from typing import Final, Any, TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
2024-03-14 14:59:43 -03:00
|
|
|
from libclinic.converter import CConverter
|
2024-04-02 07:09:53 -03:00
|
|
|
from libclinic.converters import self_converter
|
2024-04-02 08:29:39 -03:00
|
|
|
from libclinic.return_converters import CReturnConverter
|
2024-04-04 06:09:40 -03:00
|
|
|
from libclinic.app import Clinic
|
2024-03-14 11:37:22 -03:00
|
|
|
|
|
|
|
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):
|
|
|
|
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}>"
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2024-04-10 05:12:05 -03:00
|
|
|
forced_text_signature: str | None = None
|
2024-03-14 11:37:22 -03:00
|
|
|
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()
|
2024-04-03 15:17:51 -03:00
|
|
|
|
|
|
|
|
|
|
|
ParamTuple = tuple["Parameter", ...]
|
|
|
|
|
|
|
|
|
|
|
|
def permute_left_option_groups(
|
|
|
|
l: Sequence[Iterable[Parameter]]
|
|
|
|
) -> Iterator[ParamTuple]:
|
|
|
|
"""
|
|
|
|
Given [(1,), (2,), (3,)], should yield:
|
|
|
|
()
|
|
|
|
(3,)
|
|
|
|
(2, 3)
|
|
|
|
(1, 2, 3)
|
|
|
|
"""
|
|
|
|
yield tuple()
|
|
|
|
accumulator: list[Parameter] = []
|
|
|
|
for group in reversed(l):
|
|
|
|
accumulator = list(group) + accumulator
|
|
|
|
yield tuple(accumulator)
|
|
|
|
|
|
|
|
|
|
|
|
def permute_right_option_groups(
|
|
|
|
l: Sequence[Iterable[Parameter]]
|
|
|
|
) -> Iterator[ParamTuple]:
|
|
|
|
"""
|
|
|
|
Given [(1,), (2,), (3,)], should yield:
|
|
|
|
()
|
|
|
|
(1,)
|
|
|
|
(1, 2)
|
|
|
|
(1, 2, 3)
|
|
|
|
"""
|
|
|
|
yield tuple()
|
|
|
|
accumulator: list[Parameter] = []
|
|
|
|
for group in l:
|
|
|
|
accumulator.extend(group)
|
|
|
|
yield tuple(accumulator)
|
|
|
|
|
|
|
|
|
|
|
|
def permute_optional_groups(
|
|
|
|
left: Sequence[Iterable[Parameter]],
|
|
|
|
required: Iterable[Parameter],
|
|
|
|
right: Sequence[Iterable[Parameter]]
|
|
|
|
) -> tuple[ParamTuple, ...]:
|
|
|
|
"""
|
|
|
|
Generator function that computes the set of acceptable
|
|
|
|
argument lists for the provided iterables of
|
|
|
|
argument groups. (Actually it generates a tuple of tuples.)
|
|
|
|
|
|
|
|
Algorithm: prefer left options over right options.
|
|
|
|
|
|
|
|
If required is empty, left must also be empty.
|
|
|
|
"""
|
|
|
|
required = tuple(required)
|
|
|
|
if not required:
|
|
|
|
if left:
|
|
|
|
raise ValueError("required is empty but left is not")
|
|
|
|
|
|
|
|
accumulator: list[ParamTuple] = []
|
|
|
|
counts = set()
|
|
|
|
for r in permute_right_option_groups(right):
|
|
|
|
for l in permute_left_option_groups(left):
|
|
|
|
t = l + required + r
|
|
|
|
if len(t) in counts:
|
|
|
|
continue
|
|
|
|
counts.add(len(t))
|
|
|
|
accumulator.append(t)
|
|
|
|
|
|
|
|
accumulator.sort(key=len)
|
|
|
|
return tuple(accumulator)
|