• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from __future__ import annotations
2import dataclasses as dc
3import copy
4import enum
5import functools
6import inspect
7from collections.abc import Iterable, Iterator, Sequence
8from typing import Final, Any, TYPE_CHECKING
9if TYPE_CHECKING:
10    from libclinic.converter import CConverter
11    from libclinic.converters import self_converter
12    from libclinic.return_converters import CReturnConverter
13    from libclinic.app import Clinic
14
15from libclinic import VersionTuple, unspecified
16
17
18ClassDict = dict[str, "Class"]
19ModuleDict = dict[str, "Module"]
20ParamDict = dict[str, "Parameter"]
21
22
23@dc.dataclass(repr=False)
24class Module:
25    name: str
26    module: Module | Clinic
27
28    def __post_init__(self) -> None:
29        self.parent = self.module
30        self.modules: ModuleDict = {}
31        self.classes: ClassDict = {}
32        self.functions: list[Function] = []
33
34    def __repr__(self) -> str:
35        return "<clinic.Module " + repr(self.name) + " at " + str(id(self)) + ">"
36
37
38@dc.dataclass(repr=False)
39class Class:
40    name: str
41    module: Module | Clinic
42    cls: Class | None
43    typedef: str
44    type_object: str
45
46    def __post_init__(self) -> None:
47        self.parent = self.cls or self.module
48        self.classes: ClassDict = {}
49        self.functions: list[Function] = []
50
51    def __repr__(self) -> str:
52        return "<clinic.Class " + repr(self.name) + " at " + str(id(self)) + ">"
53
54
55class FunctionKind(enum.Enum):
56    CALLABLE        = enum.auto()
57    STATIC_METHOD   = enum.auto()
58    CLASS_METHOD    = enum.auto()
59    METHOD_INIT     = enum.auto()
60    METHOD_NEW      = enum.auto()
61    GETTER          = enum.auto()
62    SETTER          = enum.auto()
63
64    @functools.cached_property
65    def new_or_init(self) -> bool:
66        return self in {FunctionKind.METHOD_INIT, FunctionKind.METHOD_NEW}
67
68    def __repr__(self) -> str:
69        return f"<clinic.FunctionKind.{self.name}>"
70
71
72CALLABLE: Final = FunctionKind.CALLABLE
73STATIC_METHOD: Final = FunctionKind.STATIC_METHOD
74CLASS_METHOD: Final = FunctionKind.CLASS_METHOD
75METHOD_INIT: Final = FunctionKind.METHOD_INIT
76METHOD_NEW: Final = FunctionKind.METHOD_NEW
77GETTER: Final = FunctionKind.GETTER
78SETTER: Final = FunctionKind.SETTER
79
80
81@dc.dataclass(repr=False)
82class Function:
83    """
84    Mutable duck type for inspect.Function.
85
86    docstring - a str containing
87        * embedded line breaks
88        * text outdented to the left margin
89        * no trailing whitespace.
90        It will always be true that
91            (not docstring) or ((not docstring[0].isspace()) and (docstring.rstrip() == docstring))
92    """
93    parameters: ParamDict = dc.field(default_factory=dict)
94    _: dc.KW_ONLY
95    name: str
96    module: Module | Clinic
97    cls: Class | None
98    c_basename: str
99    full_name: str
100    return_converter: CReturnConverter
101    kind: FunctionKind
102    coexist: bool
103    return_annotation: object = inspect.Signature.empty
104    docstring: str = ''
105    # docstring_only means "don't generate a machine-readable
106    # signature, just a normal docstring".  it's True for
107    # functions with optional groups because we can't represent
108    # those accurately with inspect.Signature in 3.4.
109    docstring_only: bool = False
110    forced_text_signature: str | None = None
111    critical_section: bool = False
112    target_critical_section: list[str] = dc.field(default_factory=list)
113
114    def __post_init__(self) -> None:
115        self.parent = self.cls or self.module
116        self.self_converter: self_converter | None = None
117        self.__render_parameters__: list[Parameter] | None = None
118
119    @functools.cached_property
120    def displayname(self) -> str:
121        """Pretty-printable name."""
122        if self.kind.new_or_init:
123            assert isinstance(self.cls, Class)
124            return self.cls.name
125        else:
126            return self.name
127
128    @functools.cached_property
129    def fulldisplayname(self) -> str:
130        parent: Class | Module | Clinic | None
131        if self.kind.new_or_init:
132            parent = getattr(self.cls, "parent", None)
133        else:
134            parent = self.parent
135        name = self.displayname
136        while isinstance(parent, (Module, Class)):
137            name = f"{parent.name}.{name}"
138            parent = parent.parent
139        return name
140
141    @property
142    def render_parameters(self) -> list[Parameter]:
143        if not self.__render_parameters__:
144            l: list[Parameter] = []
145            self.__render_parameters__ = l
146            for p in self.parameters.values():
147                p = p.copy()
148                p.converter.pre_render()
149                l.append(p)
150        return self.__render_parameters__
151
152    @property
153    def methoddef_flags(self) -> str | None:
154        if self.kind.new_or_init:
155            return None
156        flags = []
157        match self.kind:
158            case FunctionKind.CLASS_METHOD:
159                flags.append('METH_CLASS')
160            case FunctionKind.STATIC_METHOD:
161                flags.append('METH_STATIC')
162            case _ as kind:
163                acceptable_kinds = {FunctionKind.CALLABLE, FunctionKind.GETTER, FunctionKind.SETTER}
164                assert kind in acceptable_kinds, f"unknown kind: {kind!r}"
165        if self.coexist:
166            flags.append('METH_COEXIST')
167        return '|'.join(flags)
168
169    def __repr__(self) -> str:
170        return f'<clinic.Function {self.name!r}>'
171
172    def copy(self, **overrides: Any) -> Function:
173        f = dc.replace(self, **overrides)
174        f.parameters = {
175            name: value.copy(function=f)
176            for name, value in f.parameters.items()
177        }
178        return f
179
180
181@dc.dataclass(repr=False, slots=True)
182class Parameter:
183    """
184    Mutable duck type of inspect.Parameter.
185    """
186    name: str
187    kind: inspect._ParameterKind
188    _: dc.KW_ONLY
189    default: object = inspect.Parameter.empty
190    function: Function
191    converter: CConverter
192    annotation: object = inspect.Parameter.empty
193    docstring: str = ''
194    group: int = 0
195    # (`None` signifies that there is no deprecation)
196    deprecated_positional: VersionTuple | None = None
197    deprecated_keyword: VersionTuple | None = None
198    right_bracket_count: int = dc.field(init=False, default=0)
199
200    def __repr__(self) -> str:
201        return f'<clinic.Parameter {self.name!r}>'
202
203    def is_keyword_only(self) -> bool:
204        return self.kind == inspect.Parameter.KEYWORD_ONLY
205
206    def is_positional_only(self) -> bool:
207        return self.kind == inspect.Parameter.POSITIONAL_ONLY
208
209    def is_vararg(self) -> bool:
210        return self.kind == inspect.Parameter.VAR_POSITIONAL
211
212    def is_optional(self) -> bool:
213        return not self.is_vararg() and (self.default is not unspecified)
214
215    def copy(
216        self,
217        /,
218        *,
219        converter: CConverter | None = None,
220        function: Function | None = None,
221        **overrides: Any
222    ) -> Parameter:
223        function = function or self.function
224        if not converter:
225            converter = copy.copy(self.converter)
226            converter.function = function
227        return dc.replace(self, **overrides, function=function, converter=converter)
228
229    def get_displayname(self, i: int) -> str:
230        if i == 0:
231            return 'argument'
232        if not self.is_positional_only():
233            return f'argument {self.name!r}'
234        else:
235            return f'argument {i}'
236
237    def render_docstring(self) -> str:
238        lines = [f"  {self.name}"]
239        lines.extend(f"    {line}" for line in self.docstring.split("\n"))
240        return "\n".join(lines).rstrip()
241
242
243ParamTuple = tuple["Parameter", ...]
244
245
246def permute_left_option_groups(
247    l: Sequence[Iterable[Parameter]]
248) -> Iterator[ParamTuple]:
249    """
250    Given [(1,), (2,), (3,)], should yield:
251       ()
252       (3,)
253       (2, 3)
254       (1, 2, 3)
255    """
256    yield tuple()
257    accumulator: list[Parameter] = []
258    for group in reversed(l):
259        accumulator = list(group) + accumulator
260        yield tuple(accumulator)
261
262
263def permute_right_option_groups(
264    l: Sequence[Iterable[Parameter]]
265) -> Iterator[ParamTuple]:
266    """
267    Given [(1,), (2,), (3,)], should yield:
268      ()
269      (1,)
270      (1, 2)
271      (1, 2, 3)
272    """
273    yield tuple()
274    accumulator: list[Parameter] = []
275    for group in l:
276        accumulator.extend(group)
277        yield tuple(accumulator)
278
279
280def permute_optional_groups(
281    left: Sequence[Iterable[Parameter]],
282    required: Iterable[Parameter],
283    right: Sequence[Iterable[Parameter]]
284) -> tuple[ParamTuple, ...]:
285    """
286    Generator function that computes the set of acceptable
287    argument lists for the provided iterables of
288    argument groups.  (Actually it generates a tuple of tuples.)
289
290    Algorithm: prefer left options over right options.
291
292    If required is empty, left must also be empty.
293    """
294    required = tuple(required)
295    if not required:
296        if left:
297            raise ValueError("required is empty but left is not")
298
299    accumulator: list[ParamTuple] = []
300    counts = set()
301    for r in permute_right_option_groups(right):
302        for l in permute_left_option_groups(left):
303            t = l + required + r
304            if len(t) in counts:
305                continue
306            counts.add(len(t))
307            accumulator.append(t)
308
309    accumulator.sort(key=len)
310    return tuple(accumulator)
311