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