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 "" @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 "" 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"" 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'' 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'' 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)