├── .gitignore ├── LICENSE ├── README.md ├── docify.py ├── pyproject.toml └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | __pycache__/ 4 | .venv/ 5 | 6 | .pdm-build/ 7 | 8 | dist/ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Andre Toerien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docify 2 | 3 | A script to add docstrings to Python type stubs using reflection 4 | 5 | ## Features 6 | 7 | - Uses [LibCST](https://github.com/Instagram/LibCST) to parse and modify the stub file 8 | - Dynamically imports the actual module to get the runtime docstring 9 | - Handles most `sys.version` and `sys.platform` conditional blocks, will only add docstrings to the correct branch 10 | - Able to modify files in-place with `-i` or `--in-place` 11 | - Won't overwrite existing docstrings 12 | - With `-b` or `--builtins-only`, will only add docstrings for modules found in `sys.builtin_module_names` (stdlib modules written in C). 13 | - With `--if-needed`, will only add docstrings if the object's (Python) source code is unavailable. Useful for language servers like [basedpyright](https://github.com/DetachHead/basedpyright) that are able to extract docstrings from source code. 14 | 15 | ## Installation 16 | 17 | Install from [PyPI](https://pypi.org/project/docify/): 18 | 19 | ```sh 20 | pip install docify 21 | 22 | docify 23 | # or 24 | python -m docify 25 | ``` 26 | 27 | Or from [conda-forge](https://anaconda.org/conda-forge/docify): 28 | 29 | ```sh 30 | conda install conda-forge::docify 31 | 32 | docify 33 | # or 34 | python -m docify 35 | ``` 36 | 37 | Or just download and run the script directly: 38 | 39 | ```sh 40 | # Install dependencies 41 | pip install libcst tqdm # tqdm is optional, and is only used if not running with -q 42 | 43 | python docify.py 44 | # or 45 | python -m docify 46 | # or 47 | chmod +x docify.py 48 | ./docify.py 49 | ``` 50 | 51 | ## Usage 52 | 53 | ``` 54 | docify [-h] [-V] [-v] [-q] [-b] [--if-needed] (-i | -o OUTPUT_DIR) INPUT_DIR [INPUT_DIR ...] 55 | 56 | A script to add docstrings to Python type stubs using reflection 57 | 58 | positional arguments: 59 | INPUT_DIR directory to read stubs from 60 | 61 | options: 62 | -h, --help show this help message and exit 63 | -V, --version show program's version number and exit 64 | -v, --verbose increase verbosity 65 | -q, --quiet decrease verbosity 66 | -b, --builtins-only only add docstrings to modules found in `sys.builtin_module_names` 67 | --if-needed only add a docstring if the object's source code cannot be found 68 | -i, --in-place modify stubs in-place 69 | -o, --output OUTPUT_DIR 70 | directory to write modified stubs to 71 | ``` 72 | -------------------------------------------------------------------------------- /docify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import annotations 4 | 5 | import functools 6 | import importlib 7 | import inspect 8 | import logging 9 | import os 10 | import shutil 11 | import sys 12 | import textwrap 13 | import warnings 14 | from argparse import ArgumentParser 15 | from pathlib import Path 16 | from tempfile import NamedTemporaryFile 17 | from types import ModuleType 18 | from typing import Any, Literal, Sequence, cast 19 | 20 | import libcst as cst 21 | import libcst.matchers as m 22 | import libcst.metadata as meta 23 | 24 | tqdm = None 25 | 26 | IGNORE_MODULES = ("antigravity", "this") 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | TRACE = 5 31 | 32 | logger_trace = functools.partial(logger.log, TRACE) 33 | 34 | 35 | _default_sentinel = object() 36 | 37 | 38 | def getattr_safe(o: object, name: str, default=_default_sentinel) -> Any: 39 | try: 40 | return getattr(o, name) 41 | except AttributeError: 42 | if default is _default_sentinel: 43 | raise 44 | return default 45 | except Exception: 46 | logger.warning(f"getattr({o!r}, {name!r}) raised an exception", exc_info=True) 47 | 48 | if default is _default_sentinel: 49 | raise AttributeError 50 | return default 51 | 52 | 53 | def queue_iter(queue): 54 | if tqdm is not None: 55 | return tqdm(queue, dynamic_ncols=True) 56 | else: 57 | return queue 58 | 59 | 60 | def get_obj(mod: ModuleType, qualname: str) -> tuple[object, object] | None: 61 | scope_obj = None 62 | obj = mod 63 | try: 64 | for part in qualname.split("."): 65 | scope_obj = obj 66 | obj = getattr_safe(scope_obj, part) 67 | except AttributeError: 68 | return None 69 | return scope_obj, obj 70 | 71 | 72 | def get_qualname(scope: meta.Scope, name: str): 73 | qualname = name 74 | while True: 75 | if isinstance(scope, meta.GlobalScope): 76 | return qualname 77 | elif isinstance(scope, meta.ClassScope): 78 | if not scope.name: 79 | raise ValueError 80 | qualname = f"{scope.name}.{qualname}" 81 | else: 82 | raise TypeError 83 | scope = scope.parent 84 | 85 | 86 | def get_doc_class(obj: object, qualname: str): 87 | doc = getattr_safe(obj, "__doc__", None) 88 | 89 | # ignore if __doc__ is a data descriptor (property) 90 | # e.g. types.BuiltinFunctionType, aka builtin_function_or_method, 91 | # or typing._SpecialForm 92 | if inspect.isdatadescriptor(doc): 93 | logger.debug(f"ignoring __doc__ descriptor for {qualname}") 94 | return None 95 | 96 | return doc 97 | 98 | 99 | def get_doc_def(scope_obj: object, obj: object, qualname: str, name: str): 100 | if inspect.isroutine(obj) or inspect.isdatadescriptor(obj): 101 | # for functions, methods and data descriptors, get __doc__ directly 102 | doc = getattr_safe(obj, "__doc__", None) 103 | 104 | # ignore __init__ and __new__ if they are inherited from object 105 | if inspect.isclass(scope_obj) and scope_obj is not object: 106 | if name == "__init__" and doc == object.__init__.__doc__: 107 | logger_trace(f"ignoring __doc__ for {qualname}") 108 | return None 109 | elif name == "__new__" and doc == object.__new__.__doc__: 110 | logger_trace(f"ignoring __doc__ for {qualname}") 111 | return None 112 | 113 | return doc 114 | 115 | # try to get the descriptor for the object, and get __doc__ from that 116 | # this allows to get the docstring for e.g. object.__class__ 117 | raw_obj = scope_obj.__dict__.get(name) 118 | if inspect.isdatadescriptor(raw_obj): 119 | doc = getattr_safe(raw_obj, "__doc__", None) 120 | if doc: 121 | logger.debug(f"using __doc__ from descriptor for {qualname}") 122 | return doc 123 | 124 | if not inspect.isclass(obj): 125 | # obj is an object (instance of a class) 126 | # only get __doc__ if it is an attribute of the instance 127 | # rather than the class, or if it is a data descriptor (property) 128 | raw_doc = type(obj).__dict__.get("__doc__") 129 | if raw_doc is None or inspect.isdatadescriptor(raw_doc): 130 | doc = getattr_safe(obj, "__doc__", None) 131 | if doc: 132 | logger.debug(f"using __doc__ from class instance {qualname}") 133 | return doc 134 | 135 | return None 136 | 137 | 138 | def docquote_str(doc: str, indent: str = ""): 139 | # if unprintable while ignoring newlines, just use repr() 140 | if not doc.replace("\n", "").isprintable(): 141 | return repr(doc) 142 | 143 | raw = "\\" in doc 144 | 145 | if "\n" in doc: 146 | doc = textwrap.indent(doc, indent) 147 | doc = "\n" + doc + "\n" + indent 148 | elif doc[-1:] == '"': 149 | if raw: 150 | # raw strings cannot end in a ", so just add a space 151 | doc = doc + " " 152 | else: 153 | # escape the " 154 | doc = doc[:-1] + '\\"' 155 | 156 | # no docstring should really have """, but let's be safe 157 | if raw: 158 | # escapes don't work in raw strings, replace with ''' 159 | doc = doc.replace('"""', "'''") 160 | else: 161 | doc = doc.replace('"""', '\\"\\"\\"') 162 | 163 | return ('r"""' if raw else '"""') + doc + '"""' 164 | 165 | 166 | def get_version(elements: Sequence[cst.BaseElement]): 167 | return tuple(int(cast(cst.Integer, element.value).value) for element in elements) 168 | 169 | 170 | class ConditionProvider(meta.BatchableMetadataProvider[bool]): 171 | def leave_Comparison(self, original_node): 172 | if m.matches( 173 | original_node, 174 | m.Comparison( 175 | left=m.Attribute( 176 | m.Name("sys"), 177 | m.Name("version_info"), 178 | ), 179 | comparisons=[ 180 | m.ComparisonTarget( 181 | m.GreaterThanEqual() 182 | | m.GreaterThan() 183 | | m.Equal() 184 | | m.NotEqual() 185 | | m.LessThan() 186 | | m.LessThanEqual(), 187 | comparator=m.Tuple( 188 | [ 189 | m.Element(m.Integer()), 190 | m.AtMostN(m.Element(m.Integer()), n=2), 191 | ] 192 | ), 193 | ) 194 | ], 195 | ), 196 | ): 197 | matches = m.matches( 198 | original_node, 199 | m.Comparison( 200 | comparisons=[ 201 | m.ComparisonTarget( 202 | m.GreaterThanEqual(), 203 | comparator=m.Tuple( 204 | m.MatchIfTrue( 205 | lambda els: sys.version_info >= get_version(els) 206 | ), 207 | ), 208 | ) 209 | | m.ComparisonTarget( 210 | m.GreaterThan(), 211 | comparator=m.Tuple( 212 | m.MatchIfTrue( 213 | lambda els: sys.version_info > get_version(els) 214 | ), 215 | ), 216 | ) 217 | | m.ComparisonTarget( 218 | m.Equal(), 219 | comparator=m.Tuple( 220 | m.MatchIfTrue( 221 | lambda els: sys.version_info == get_version(els) 222 | ), 223 | ), 224 | ) 225 | | m.ComparisonTarget( 226 | m.NotEqual(), 227 | comparator=m.Tuple( 228 | m.MatchIfTrue( 229 | lambda els: sys.version_info != get_version(els) 230 | ), 231 | ), 232 | ) 233 | | m.ComparisonTarget( 234 | m.LessThan(), 235 | comparator=m.Tuple( 236 | m.MatchIfTrue( 237 | lambda els: sys.version_info < get_version(els) 238 | ), 239 | ), 240 | ) 241 | | m.ComparisonTarget( 242 | m.LessThanEqual(), 243 | comparator=m.Tuple( 244 | m.MatchIfTrue( 245 | lambda els: sys.version_info <= get_version(els) 246 | ), 247 | ), 248 | ) 249 | ] 250 | ), 251 | ) 252 | self.set_metadata(original_node, matches) 253 | 254 | if m.matches( 255 | original_node, 256 | m.Comparison( 257 | left=m.Attribute( 258 | m.Name("sys"), 259 | m.Name("platform"), 260 | ), 261 | comparisons=[ 262 | m.ComparisonTarget( 263 | m.Equal() | m.NotEqual(), 264 | comparator=m.SimpleString(), 265 | ) 266 | ], 267 | ), 268 | ): 269 | matches = m.matches( 270 | original_node, 271 | m.Comparison( 272 | comparisons=[ 273 | m.ComparisonTarget( 274 | m.Equal(), 275 | comparator=m.MatchIfTrue(lambda val: sys.platform == val), 276 | ) 277 | | m.ComparisonTarget( 278 | m.NotEqual(), 279 | comparator=m.MatchIfTrue(lambda val: sys.platform != val), 280 | ) 281 | ] 282 | ), 283 | ) 284 | self.set_metadata(original_node, matches) 285 | 286 | def leave_UnaryOperation(self, original_node): 287 | val = self.get_metadata(type(self), original_node.expression, None) 288 | if val is None: 289 | return 290 | 291 | if isinstance(original_node.operator, cst.Not): 292 | self.set_metadata(original_node, not val) 293 | 294 | def leave_BooleanOperation(self, original_node): 295 | left = self.get_metadata(type(self), original_node.left, None) 296 | if left is None: 297 | return 298 | 299 | right = self.get_metadata(type(self), original_node.right, None) 300 | if right is None: 301 | return 302 | 303 | if isinstance(original_node.operator, cst.And): 304 | self.set_metadata(original_node, left and right) 305 | elif isinstance(original_node.operator, cst.Or): 306 | self.set_metadata(original_node, left or right) 307 | 308 | 309 | class UnreachableProvider(meta.BatchableMetadataProvider[Literal[True]]): 310 | METADATA_DEPENDENCIES = [ConditionProvider] 311 | 312 | class SetMetadataVisitor(cst.CSTVisitor): 313 | def __init__(self, provider: "UnreachableProvider"): 314 | super().__init__() 315 | self.provider = provider 316 | 317 | def on_leave(self, original_node): 318 | self.provider.set_metadata(original_node, True) 319 | super().on_leave(original_node) 320 | 321 | def mark_unreachable(self, node: cst.If | cst.Else): 322 | self.set_metadata(node, True) 323 | node.body.visit(self.SetMetadataVisitor(self)) 324 | 325 | def visit_If(self, node): 326 | cond = self.get_metadata(type(self), node, None) 327 | if cond is not None: 328 | return 329 | 330 | cond = self.get_metadata(ConditionProvider, node.test, None) 331 | if cond is None: 332 | logger.warning(f"encountered unsupported condition:\n{node.test}") 333 | return 334 | 335 | if cond: 336 | # condition is true - subsequent branches are unreachable 337 | while True: 338 | node = node.orelse 339 | if node is None: 340 | break 341 | elif isinstance(node, cst.If): 342 | self.mark_unreachable(node) 343 | elif isinstance(node, cst.Else): 344 | self.mark_unreachable(node) 345 | break 346 | else: 347 | # condition is false - this branch is unreachable 348 | self.mark_unreachable(node) 349 | 350 | 351 | # TODO: somehow add module attribute docstrings? e.g. typing.Union 352 | # TODO: infer for renamed classes, e.g. types._Cell is CellType at runtime, and CellType = _Cell exists in stub 353 | 354 | 355 | class Transformer(cst.CSTTransformer): 356 | METADATA_DEPENDENCIES = [ 357 | meta.ScopeProvider, 358 | meta.ParentNodeProvider, 359 | UnreachableProvider, 360 | ] 361 | 362 | def __init__(self, import_path: str, mod: ModuleType, if_needed: bool): 363 | super().__init__() 364 | self.import_path = import_path 365 | self.mod = mod 366 | self.if_needed = if_needed 367 | 368 | def check_if_needed(self, obj): 369 | if not self.if_needed: 370 | return True 371 | try: 372 | return not inspect.getsourcefile(obj) 373 | except TypeError: 374 | return True 375 | 376 | def leave_ClassFunctionDef( 377 | self, 378 | original_node: cst.ClassDef | cst.FunctionDef, 379 | updated_node: cst.ClassDef | cst.FunctionDef, 380 | ): 381 | scope = self.get_metadata(meta.ScopeProvider, original_node, None) 382 | if scope is None: 383 | return updated_node 384 | 385 | if self.get_metadata(UnreachableProvider, original_node, False): 386 | return updated_node 387 | 388 | name = original_node.name.value 389 | qualname = get_qualname(scope, name) 390 | 391 | if m.matches( 392 | updated_node.body, 393 | m.SimpleStatementSuite( 394 | [ 395 | m.Expr(m.SimpleString()), 396 | m.ZeroOrMore(), 397 | ] 398 | ) 399 | | m.IndentedBlock( 400 | [ 401 | m.SimpleStatementLine( 402 | [ 403 | m.Expr(m.SimpleString()), 404 | m.ZeroOrMore(), 405 | ] 406 | ), 407 | m.ZeroOrMore(), 408 | ] 409 | ), 410 | ): 411 | logger_trace(f"docstring for {qualname} already exists, skipping") 412 | return updated_node 413 | 414 | r = get_obj(self.mod, qualname) 415 | if r is None: 416 | logger_trace(f"cannot find {qualname}") 417 | return updated_node 418 | 419 | scope_obj, obj = r 420 | 421 | if not self.check_if_needed(obj): 422 | return updated_node 423 | 424 | if isinstance(original_node, cst.FunctionDef): 425 | doc = get_doc_def(scope_obj, obj, qualname, name) 426 | elif isinstance(original_node, cst.ClassDef): 427 | doc = get_doc_class(obj, qualname) 428 | else: 429 | doc = None 430 | 431 | if doc is not None: 432 | if not isinstance(doc, str): 433 | logger.warning(f"__doc__ for {qualname} is {type(doc)!r}, not str") 434 | doc = None 435 | else: 436 | doc = inspect.cleandoc(doc) 437 | 438 | if not doc: 439 | logger_trace(f"could not find __doc__ for {qualname}") 440 | return updated_node 441 | 442 | indent = "" 443 | if "\n" in doc: 444 | n = original_node.body 445 | while n is not None: 446 | if isinstance(n, cst.SimpleStatementSuite): 447 | indent += self.module.default_indent 448 | elif isinstance(n, cst.IndentedBlock): 449 | block_indent = n.indent 450 | if block_indent is None: 451 | block_indent = self.module.default_indent 452 | indent += block_indent 453 | 454 | n = self.get_metadata(meta.ParentNodeProvider, n, None) 455 | 456 | doc = docquote_str(doc, indent) 457 | logger_trace(f"__doc__ for {qualname}:\n{doc}") 458 | 459 | docstring_node = cst.SimpleStatementLine([cst.Expr(cst.SimpleString(doc))]) 460 | 461 | node_body = updated_node.body 462 | if isinstance(node_body, cst.SimpleStatementSuite): 463 | lines = (cst.SimpleStatementLine([x]) for x in node_body.body) 464 | node_body = cst.IndentedBlock([docstring_node, *lines]) 465 | elif isinstance(node_body, cst.IndentedBlock): 466 | node_body = node_body.with_changes(body=[docstring_node, *node_body.body]) 467 | else: 468 | return updated_node 469 | 470 | return updated_node.with_changes(body=node_body) 471 | 472 | def leave_ClassDef(self, original_node, updated_node): 473 | return self.leave_ClassFunctionDef(original_node, updated_node) 474 | 475 | def leave_FunctionDef(self, original_node, updated_node): 476 | return self.leave_ClassFunctionDef(original_node, updated_node) 477 | 478 | def visit_Module(self, node): 479 | self.module = node 480 | 481 | def leave_Module(self, original_node, updated_node): 482 | if updated_node.body and m.matches( 483 | updated_node.body[0], 484 | m.SimpleStatementLine( 485 | [ 486 | m.Expr(m.SimpleString()), 487 | m.ZeroOrMore(), 488 | ] 489 | ), 490 | ): 491 | logger_trace(f"docstring for {self.import_path} already exists, skipping") 492 | return updated_node 493 | 494 | if not self.check_if_needed(self.mod): 495 | return updated_node 496 | 497 | doc = getattr_safe(self.mod, "__doc__", None) 498 | if not doc: 499 | logger_trace(f"could not find __doc__ for {self.import_path}") 500 | return updated_node 501 | 502 | doc = inspect.cleandoc(doc) 503 | doc = docquote_str(doc) 504 | logger_trace(f"__doc__ for {self.import_path}:\n{doc}") 505 | 506 | node_body = updated_node.body 507 | if len(node_body) != 0: 508 | node_body = ( 509 | node_body[0].with_changes( 510 | leading_lines=[ 511 | cst.EmptyLine(), 512 | *node_body[0].leading_lines, 513 | ] 514 | ), 515 | *node_body[1:], 516 | ) 517 | else: 518 | updated_node = updated_node.with_changes( 519 | footer=[cst.EmptyLine(), *updated_node.footer] 520 | ) 521 | 522 | if len(updated_node.header) != 0: 523 | updated_node = updated_node.with_changes( 524 | header=[*updated_node.header, cst.EmptyLine()] 525 | ) 526 | 527 | node_body = ( 528 | cst.SimpleStatementLine( 529 | [cst.Expr(cst.SimpleString(doc))], 530 | ), 531 | *node_body, 532 | ) 533 | return updated_node.with_changes(body=node_body) 534 | 535 | 536 | def run( 537 | *, 538 | input_dirs: list[str] | None = None, 539 | input_dir: str | None = None, 540 | builtins_only: bool = False, 541 | if_needed: bool = False, 542 | in_place: bool = True, 543 | output_dir: str = "", 544 | ): 545 | queue: list[tuple[str, Path, Path]] = [] 546 | 547 | if input_dirs is None: 548 | input_dirs = [] 549 | 550 | if input_dir: 551 | input_dirs.append(input_dir) 552 | 553 | for input_dir in input_dirs: 554 | input_path = Path(input_dir) 555 | 556 | if not input_path.is_dir(): 557 | raise ValueError(f"Input path '{input_dir}' is not a directory") 558 | 559 | include_root = (input_path / "__init__.py").exists() 560 | include_root = include_root or (input_path / "__init__.pyi").exists() 561 | 562 | for base_dir, _, filenames in os.walk(input_path, followlinks=True): 563 | for filename in filenames: 564 | file_path = Path(base_dir, filename) 565 | file_relpath = file_path.relative_to(input_path) 566 | 567 | if file_relpath.suffix != ".pyi": 568 | continue 569 | 570 | import_path = file_relpath.with_suffix("") 571 | 572 | if include_root: 573 | root = input_path.name 574 | if root == "" or root == "..": 575 | # resolve the path to get the actual name of the parent dir 576 | root = input_path.resolve().name 577 | 578 | import_path = root / import_path 579 | file_relpath = root / file_relpath 580 | 581 | if import_path.name == "__init__": 582 | import_path = import_path.parent 583 | 584 | import_path = str(import_path).replace(os.path.sep, ".") 585 | 586 | if import_path in IGNORE_MODULES: 587 | continue 588 | if builtins_only and import_path not in sys.builtin_module_names: 589 | continue 590 | 591 | queue.append((import_path, file_path, file_relpath)) 592 | 593 | with warnings.catch_warnings(): 594 | # ignore all warnings, mostly get DeprecationWarnings and a few SyntaxWarnings 595 | warnings.simplefilter("ignore") 596 | 597 | for import_path, file_path, file_relpath in queue_iter(queue): 598 | try: 599 | mod = importlib.import_module(import_path) 600 | except ImportError as e: 601 | logger.warning(f"could not import {import_path}: {e}") 602 | continue 603 | except Exception: 604 | logger.warning(f"could not import {import_path}", exc_info=True) 605 | continue 606 | 607 | with open(file_path, "r", encoding="utf-8") as f: 608 | stub_source = f.read() 609 | 610 | try: 611 | stub_cst = cst.parse_module(stub_source) 612 | except Exception: 613 | logger.exception(f"could not parse {file_path}") 614 | continue 615 | 616 | logger.info(f"processing {file_path}") 617 | 618 | wrapper = cst.MetadataWrapper(stub_cst) 619 | visitor = Transformer(import_path, mod, if_needed) 620 | 621 | new_stub_cst = wrapper.visit(visitor) 622 | 623 | if in_place: 624 | f = None 625 | try: 626 | with NamedTemporaryFile( 627 | dir=(file_path / "..").resolve(), 628 | prefix=f"{file_path.name}.", 629 | mode="w", 630 | delete=False, 631 | encoding="utf-8", 632 | ) as f: 633 | f.write(new_stub_cst.code) 634 | except: 635 | if f: 636 | os.remove(f.name) 637 | raise 638 | 639 | shutil.copystat(file_path, f.name) 640 | os.replace(f.name, file_path) 641 | else: 642 | output_path = Path(output_dir) 643 | output_file = output_path / file_relpath 644 | os.makedirs(output_file.parent, exist_ok=True) 645 | 646 | with open(output_file, "w", encoding="utf-8") as f: 647 | f.write(new_stub_cst.code) 648 | 649 | 650 | def main(args: Sequence[str] | None = None): 651 | arg_parser = ArgumentParser( 652 | description="A script to add docstrings to Python type stubs using reflection" 653 | ) 654 | 655 | arg_parser.add_argument( 656 | "-V", 657 | "--version", 658 | action="version", 659 | version="%(prog)s 1.1.0", 660 | ) 661 | arg_parser.add_argument( 662 | "-v", 663 | "--verbose", 664 | action="count", 665 | default=0, 666 | help="increase verbosity", 667 | ) 668 | arg_parser.add_argument( 669 | "-q", 670 | "--quiet", 671 | action="count", 672 | default=0, 673 | help="decrease verbosity", 674 | ) 675 | arg_parser.add_argument( 676 | "-b", 677 | "--builtins-only", 678 | action="store_true", 679 | help="only add docstrings to modules found in `sys.builtin_module_names`", 680 | ) 681 | arg_parser.add_argument( 682 | "--if-needed", 683 | action="store_true", 684 | help="only add a docstring if the object's source code cannot be found", 685 | ) 686 | arg_parser.add_argument( 687 | "input_dirs", 688 | metavar="INPUT_DIR", 689 | nargs="+", 690 | help="directory to read stubs from", 691 | ) 692 | output_group = arg_parser.add_mutually_exclusive_group(required=True) 693 | output_group.add_argument( 694 | "-i", 695 | "--in-place", 696 | action="store_true", 697 | help="modify stubs in-place", 698 | ) 699 | output_group.add_argument( 700 | "-o", 701 | "--output", 702 | metavar="OUTPUT_DIR", 703 | dest="output_dir", 704 | help="directory to write modified stubs to", 705 | ) 706 | 707 | parsed_args = arg_parser.parse_args(args) 708 | 709 | logging.addLevelName(5, "TRACE") 710 | 711 | verbosity = 2 + parsed_args.verbose - parsed_args.quiet 712 | levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG, TRACE] 713 | if verbosity < 0: 714 | level = logging.ERROR 715 | elif verbosity >= len(levels): 716 | level = TRACE 717 | else: 718 | level = levels[verbosity] 719 | 720 | stream = None 721 | if level <= logging.INFO and sys.stderr.isatty(): 722 | try: 723 | global tqdm 724 | from tqdm import tqdm 725 | from tqdm.contrib import DummyTqdmFile 726 | 727 | stream = DummyTqdmFile(sys.stderr) 728 | except Exception: 729 | pass 730 | 731 | handler = logging.StreamHandler(stream) 732 | # only print docify messages 733 | handler.addFilter(logging.Filter(__name__)) 734 | logging.basicConfig( 735 | format="%(levelname)s: %(message)s", 736 | level=level, 737 | handlers=[handler], 738 | ) 739 | 740 | run_args = vars(parsed_args) 741 | del run_args["verbose"] 742 | del run_args["quiet"] 743 | run(**run_args) 744 | 745 | 746 | if __name__ == "__main__": 747 | main() 748 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "docify" 3 | version = "1.1.0" 4 | dependencies = [ 5 | "libcst >= 1.1.0", 6 | "tqdm >= 4.66.4", 7 | ] 8 | requires-python = ">= 3.8" 9 | authors = [ 10 | { name = "AThePeanut4", email = "andre.toerien8@gmail.com" }, 11 | ] 12 | description = "A script to add docstrings to Python type stubs using reflection" 13 | readme = "README.md" 14 | license = { text = "MIT" } 15 | classifiers = [ 16 | "Development Status :: 4 - Beta", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: MIT License", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Programming Language :: Python :: 3.13", 26 | "Topic :: Software Development", 27 | "Topic :: Utilities", 28 | ] 29 | 30 | [project.urls] 31 | Repository = "https://github.com/AThePeanut4/docify" 32 | 33 | [project.scripts] 34 | docify = "docify:main" 35 | 36 | [build-system] 37 | requires = ["pdm-backend"] 38 | build-backend = "pdm.backend" 39 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.8" 3 | 4 | [[package]] 5 | name = "colorama" 6 | version = "0.4.6" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 11 | ] 12 | 13 | [[package]] 14 | name = "docify" 15 | version = "1.1.0" 16 | source = { editable = "." } 17 | dependencies = [ 18 | { name = "libcst" }, 19 | { name = "tqdm" }, 20 | ] 21 | 22 | [package.metadata] 23 | requires-dist = [ 24 | { name = "libcst", specifier = ">=1.1.0" }, 25 | { name = "tqdm", specifier = ">=4.66.4" }, 26 | ] 27 | 28 | [[package]] 29 | name = "libcst" 30 | version = "1.1.0" 31 | source = { registry = "https://pypi.org/simple" } 32 | dependencies = [ 33 | { name = "pyyaml" }, 34 | { name = "typing-extensions" }, 35 | { name = "typing-inspect" }, 36 | ] 37 | sdist = { url = "https://files.pythonhosted.org/packages/81/ef/610498b5e982d9dd64f2af8422ece1be44a946a8dbda15d08087e0e1ff08/libcst-1.1.0.tar.gz", hash = "sha256:0acbacb9a170455701845b7e940e2d7b9519db35a86768d86330a0b0deae1086", size = 764691 } 38 | wheels = [ 39 | { url = "https://files.pythonhosted.org/packages/5c/0a/bdd31ed5c8ad1978aa5d0350e64769ea91c7710a4d34e159c696e6c145e7/libcst-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:63f75656fd733dc20354c46253fde3cf155613e37643c3eaf6f8818e95b7a3d1", size = 2112700 }, 40 | { url = "https://files.pythonhosted.org/packages/fb/27/889bc60abece5f5c998560c9d61898e548a48e6a72c75490582cde878e8d/libcst-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ae11eb1ea55a16dc0cdc61b41b29ac347da70fec14cc4381248e141ee2fbe6c", size = 2070222 }, 41 | { url = "https://files.pythonhosted.org/packages/20/23/d5b4e03fdec2275bd2f96b19a1317fed736f68411dfa6913e206888930ed/libcst-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bc745d0c06420fe2644c28d6ddccea9474fb68a2135904043676deb4fa1e6bc", size = 3154843 }, 42 | { url = "https://files.pythonhosted.org/packages/ac/0d/723754c5a50fd57a3fddc17960e98f809a46a898419a39a5da5795ca6983/libcst-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c1f2da45f1c45634090fd8672c15e0159fdc46853336686959b2d093b6e10fa", size = 3194222 }, 43 | { url = "https://files.pythonhosted.org/packages/b3/d8/9d598a272f2d7687f1936836bd67bf8b13623fbf43210e8745157732a87e/libcst-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:003e5e83a12eed23542c4ea20fdc8de830887cc03662432bb36f84f8c4841b81", size = 3300452 }, 44 | { url = "https://files.pythonhosted.org/packages/82/41/bed99411d679318116673bbd7d96f65ccf382770898719109f5927250b74/libcst-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:3ebbb9732ae3cc4ae7a0e97890bed0a57c11d6df28790c2b9c869f7da653c7c7", size = 2031202 }, 45 | { url = "https://files.pythonhosted.org/packages/9d/01/20eef81a259a7555def181917ac21180b0ccd694b62851d251c69e55b431/libcst-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d68c34e3038d3d1d6324eb47744cbf13f2c65e1214cf49db6ff2a6603c1cd838", size = 2112699 }, 46 | { url = "https://files.pythonhosted.org/packages/b8/e3/f9b7528ebd96e5b507c401830f81274806fe7f76e65668dee9884fdb8465/libcst-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9dffa1795c2804d183efb01c0f1efd20a7831db6a21a0311edf90b4100d67436", size = 2070226 }, 47 | { url = "https://files.pythonhosted.org/packages/3e/88/1a0dd13408be61a5fcd83ecf0ee85c2b4795dab7d3c5544a8ae35f495265/libcst-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc9b6ac36d7ec9db2f053014ea488086ca2ed9c322be104fbe2c71ca759da4bb", size = 3154844 }, 48 | { url = "https://files.pythonhosted.org/packages/13/a6/3414494d9767eb937d2261f070d5edf12dac26cbf8df0f4a9619a119e033/libcst-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b7a38ec4c1c009ac39027d51558b52851fb9234669ba5ba62283185963a31c", size = 3194221 }, 49 | { url = "https://files.pythonhosted.org/packages/74/a6/3085ab81c6effe43590827cd4748d44621cd94ef6bf9f70a301985e4b566/libcst-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5297a16e575be8173185e936b7765c89a3ca69d4ae217a4af161814a0f9745a7", size = 3300454 }, 50 | { url = "https://files.pythonhosted.org/packages/01/3b/9f1b0f4401e439bbf42162468c3583799fa80fa441b47ef103f79d0fd61a/libcst-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:7ccaf53925f81118aeaadb068a911fac8abaff608817d7343da280616a5ca9c1", size = 2031205 }, 51 | { url = "https://files.pythonhosted.org/packages/04/bd/0143bc80fee8544d4c3bf7a4ba098b8a86d7a08df2c8cbce1e04300c5f47/libcst-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:75816647736f7e09c6120bdbf408456f99b248d6272277eed9a58cf50fb8bc7d", size = 2112700 }, 52 | { url = "https://files.pythonhosted.org/packages/93/b8/d144476de088d6b2ed40b7c6a02f89df91f0163cab1315ff5944bf1796a6/libcst-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8f26250f87ca849a7303ed7a4fd6b2c7ac4dec16b7d7e68ca6a476d7c9bfcdb", size = 2070223 }, 53 | { url = "https://files.pythonhosted.org/packages/b6/98/a30992fa79669e10cf0f7ae8749d460f713f968c66cd48c569c6ed87f705/libcst-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d37326bd6f379c64190a28947a586b949de3a76be00176b0732c8ee87d67ebe", size = 3155322 }, 54 | { url = "https://files.pythonhosted.org/packages/94/d9/2a2af5d477ea98af05534f76260647da308d20c0cdd10bc05107d2755b0f/libcst-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d8cf974cfa2487b28f23f56c4bff90d550ef16505e58b0dca0493d5293784b", size = 3194986 }, 55 | { url = "https://files.pythonhosted.org/packages/d6/76/c2867a61f185c1a21cf5e10c8020b763250f5523a042535c609215800389/libcst-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d1271403509b0a4ee6ff7917c2d33b5a015f44d1e208abb1da06ba93b2a378", size = 3300603 }, 56 | { url = "https://files.pythonhosted.org/packages/b8/83/1504eb1d4c2cf7f251e1c719734d6f1560dd5532c25871983ef36d9cab03/libcst-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bca1841693941fdd18371824bb19a9702d5784cd347cb8231317dbdc7062c5bc", size = 2031201 }, 57 | { url = "https://files.pythonhosted.org/packages/df/76/ef29f8f26e9ca89a75c138b1cc4de936bc648607c2fe4e2ee2201201a1b0/libcst-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f36f592e035ef84f312a12b75989dde6a5f6767fe99146cdae6a9ee9aff40dd0", size = 2112679 }, 58 | { url = "https://files.pythonhosted.org/packages/4c/29/4cf0e0bdc5c9045ba13c1ef86e75af6db1d55e67b798c9d7f0d2631ff85a/libcst-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f561c9a84eca18be92f4ad90aa9bd873111efbea995449301719a1a7805dbc5c", size = 2070261 }, 59 | { url = "https://files.pythonhosted.org/packages/df/72/b1a969b0a7f9184f1a7181b30a09d88479aaf9af861bae10d7207780aa88/libcst-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97fbc73c87e9040e148881041fd5ffa2a6ebf11f64b4ccb5b52e574b95df1a15", size = 3154652 }, 60 | { url = "https://files.pythonhosted.org/packages/4e/9f/9b4278b8e6e2cfd56caab73e69388fc6da1aa42961cbc777a45888db8e11/libcst-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99fdc1929703fd9e7408aed2e03f58701c5280b05c8911753a8d8619f7dfdda5", size = 3194182 }, 61 | { url = "https://files.pythonhosted.org/packages/be/fa/7b8abb0c5e6f30953dd82fba70cbf9c36d3832fc36d329cb408241ac25c2/libcst-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bf69cbbab5016d938aac4d3ae70ba9ccb3f90363c588b3b97be434e6ba95403", size = 3300688 }, 62 | { url = "https://files.pythonhosted.org/packages/7f/71/13e5a9f9831aebffced7f42e8304ce9f09ca85f7f713b21d22fb7b571140/libcst-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fe41b33aa73635b1651f64633f429f7aa21f86d2db5748659a99d9b7b1ed2a90", size = 2031062 }, 63 | { url = "https://files.pythonhosted.org/packages/b7/4f/e989c0b07749143f3608814f712bc27ad48a72f5ba890c4bea7df4258c23/libcst-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73c086705ed34dbad16c62c9adca4249a556c1b022993d511da70ea85feaf669", size = 2112658 }, 64 | { url = "https://files.pythonhosted.org/packages/f8/c3/c7e56011c477df755d78ee734dd1caa5e6ff6cc51a07071adbc452df998f/libcst-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a07ecfabbbb8b93209f952a365549e65e658831e9231649f4f4e4263cad24b1", size = 2070233 }, 65 | { url = "https://files.pythonhosted.org/packages/30/f3/49bdd4be2a9a89834e89adf3c0377c1c6387e00634efa0d0c078a49ee034/libcst-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c653d9121d6572d8b7f8abf20f88b0a41aab77ff5a6a36e5a0ec0f19af0072e8", size = 3155034 }, 66 | { url = "https://files.pythonhosted.org/packages/bd/d2/544d481b33dbc35381d976221ab1358fab0db4588c2d53214500454e9fd5/libcst-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f1cd308a4c2f71d5e4eec6ee693819933a03b78edb2e4cc5e3ad1afd5fb3f07", size = 3194521 }, 67 | { url = "https://files.pythonhosted.org/packages/60/1c/c2d4e2b6a69fc8fd52ae6c0662dfa4550486712d5e77cf754348e4c85a2c/libcst-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8afb6101b8b3c86c5f9cec6b90ab4da16c3c236fe7396f88e8b93542bb341f7c", size = 3300712 }, 68 | { url = "https://files.pythonhosted.org/packages/0a/76/b31dba704487daf0845fc2d246ddb1a141a1dd12ef695d30eb4b0aac51c5/libcst-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:d22d1abfe49aa60fc61fa867e10875a9b3024ba5a801112f4d7ba42d8d53242e", size = 2031198 }, 69 | ] 70 | 71 | [[package]] 72 | name = "mypy-extensions" 73 | version = "1.0.0" 74 | source = { registry = "https://pypi.org/simple" } 75 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } 76 | wheels = [ 77 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, 78 | ] 79 | 80 | [[package]] 81 | name = "pyyaml" 82 | version = "6.0.2" 83 | source = { registry = "https://pypi.org/simple" } 84 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 85 | wheels = [ 86 | { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, 87 | { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, 88 | { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, 89 | { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, 90 | { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, 91 | { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, 92 | { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, 93 | { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, 94 | { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, 95 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, 96 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, 97 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, 98 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, 99 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, 100 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, 101 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, 102 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, 103 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, 104 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, 105 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, 106 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, 107 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, 108 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, 109 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, 110 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, 111 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, 112 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, 113 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, 114 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, 115 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, 116 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, 117 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, 118 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, 119 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, 120 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, 121 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, 122 | { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218 }, 123 | { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067 }, 124 | { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812 }, 125 | { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531 }, 126 | { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820 }, 127 | { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514 }, 128 | { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702 }, 129 | { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, 130 | { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, 131 | { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, 132 | { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, 133 | { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, 134 | { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, 135 | { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, 136 | { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, 137 | { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, 138 | ] 139 | 140 | [[package]] 141 | name = "tqdm" 142 | version = "4.67.1" 143 | source = { registry = "https://pypi.org/simple" } 144 | dependencies = [ 145 | { name = "colorama", marker = "platform_system == 'Windows'" }, 146 | ] 147 | sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } 148 | wheels = [ 149 | { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, 150 | ] 151 | 152 | [[package]] 153 | name = "typing-extensions" 154 | version = "4.12.2" 155 | source = { registry = "https://pypi.org/simple" } 156 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 157 | wheels = [ 158 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 159 | ] 160 | 161 | [[package]] 162 | name = "typing-inspect" 163 | version = "0.9.0" 164 | source = { registry = "https://pypi.org/simple" } 165 | dependencies = [ 166 | { name = "mypy-extensions" }, 167 | { name = "typing-extensions" }, 168 | ] 169 | sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825 } 170 | wheels = [ 171 | { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827 }, 172 | ] 173 | --------------------------------------------------------------------------------