| from __future__ import annotations |
| import dataclasses as dc |
| import copy |
| import enum |
| import functools |
| import inspect |
| from collections.abc import Iterable, Iterator, Sequence |
| from typing import Final, Any, TYPE_CHECKING |
| if TYPE_CHECKING: |
| from libclinic.converter import CConverter |
| from libclinic.converters import self_converter |
| from libclinic.return_converters import CReturnConverter |
| from libclinic.app import Clinic |
| |
| 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 |
| forced_text_signature: str | None = None |
| 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() |
| |
| |
| 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) |