cli.py
 1  """Module that contains the command line application."""
 2  
 3  # Why does this file exist, and why not put this in `__main__`?
 4  #
 5  # You might be tempted to import things from `__main__` later,
 6  # but that will cause problems: the code will get executed twice:
 7  #
 8  # - When you run `python -m docstrings2pep727` python will execute
 9  #   `__main__.py` as a script. That means there won't be any
10  #   `docstrings2pep727.__main__` in `sys.modules`.
11  # - When you import `__main__` it will get executed again (as a module) because
12  #   there's no `docstrings2pep727.__main__` in `sys.modules`.
13  
14  from __future__ import annotations
15  
16  import argparse
17  import sys
18  from pathlib import Path
19  from typing import TYPE_CHECKING, Any
20  
21  import libcst as cst
22  from griffe.agents.visitor import visit
23  from griffe.docstrings import Parser
24  
25  from docstrings2pep727.transformer import PEP727Transformer
26  
27  if TYPE_CHECKING:
28      from griffe import Docstring, Object
29  
30  
31  from docstrings2pep727 import debug
32  
33  
34  class _DebugInfo(argparse.Action):
35      def __init__(self, nargs: int | str | None = 0, **kwargs: Any) -> None:
36          super().__init__(nargs=nargs, **kwargs)
37  
38      def __call__(self, *args: Any, **kwargs: Any) -> None:  # noqa: ARG002
39          debug.print_debug_info()
40          sys.exit(0)
41  
42  
43  def _docstrings(obj: Object, store: dict | None = None) -> dict[str, Docstring]:
44      if store is None:
45          store = {}
46      if obj.docstring:
47          store[obj.path] = obj.docstring
48      for member in obj.members.values():
49          if not member.is_alias:
50              _docstrings(member, store)  # type: ignore[arg-type]
51      return store
52  
53  
54  def get_parser() -> argparse.ArgumentParser:
55      """Return the CLI argument parser.
56  
57      Returns:
58          An argparse parser.
59      """
60      parser = argparse.ArgumentParser(prog="docstrings2pep727")
61      parser.add_argument("module", help="Module to transform.")
62      parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {debug.get_version()}")
63      parser.add_argument("--debug-info", action=_DebugInfo, help="Print debug information.")
64      return parser
65  
66  
67  def main(args: list[str] | None = None) -> int:
68      """Run the main program.
69  
70      This function is executed when you type `docstrings2pep727` or `python -m docstrings2pep727`.
71  
72      Parameters:
73          args: Arguments passed from the command line.
74  
75      Returns:
76          An exit code.
77      """
78      parser = get_parser()
79      opts = parser.parse_args(args=args)
80  
81      module_path = Path(opts.module)
82      module_code = module_path.read_text()
83      module_data = visit(module_path.stem, module_path, module_code, docstring_parser=Parser("google"))
84      docstrings = _docstrings(module_data)
85  
86      source_tree = cst.parse_module(module_code)
87      transformer = PEP727Transformer(source_tree, module_path.stem, docstrings)
88      modified_tree = source_tree.visit(transformer)
89      print(modified_tree.code)
90      return 0