Skip to content

Simplify part of, type and cleanup py2d.py #2385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 72 additions & 78 deletions AutoDuck/py2d.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,62 @@
from __future__ import annotations

import re
import sys
import types
from collections.abc import Iterable
from types import FunctionType, MethodType
from typing import Generator, Generic, TypeVar, Union

_T = TypeVar("_T")


def ad_escape(s):
def ad_escape(s: str) -> str:
return re.sub(r"([^<]*)<([^>]*)>", r"\g<1>\\<\g<2>\\>", s)


class DocInfo:
def __init__(self, name, ob):
class DocInfo(Generic[_T]):
def __init__(self, name: str, ob: _T) -> None:
docstring = (ob.__doc__ or "").strip()

self.desc = docstring
self.short_desc = docstring and docstring.splitlines()[0]
self.name = name
self.ob = ob
self.short_desc = ""
self.desc = ""


def BuildArgInfos(ob):
ret = []
vars = list(ob.__code__.co_varnames[: ob.__code__.co_argcount])
vars.reverse() # for easier default checking.
class ArgInfo(DocInfo[Union[FunctionType, MethodType]]):
def __init__(self, name: str, ob: FunctionType | MethodType, default: str) -> None:
super().__init__(name, ob)
self.desc = name
self.short_desc = name
self.default = default


def BuildArgInfos(ob: FunctionType | MethodType) -> list[ArgInfo]:
ret: list[ArgInfo] = []
# Reversed for easier default checking.
# Since arguments w/ default can only be at the end of a function.
vars = reversed(ob.__code__.co_varnames[: ob.__code__.co_argcount])
defs = list(ob.__defaults__ or [])
for i, n in enumerate(vars):
info = DocInfo(n, ob)
info.short_desc = info.desc = n
info.default = ""
for n in vars:
default = ""
if defs:
default = repr(defs.pop())
# the default may be an object, so the repr gives '<...>' - and
# the angle brackets screw autoduck.
info.default = default.replace("<", "").replace(">", "")
ret.append(info)
# the default may be an object, so the repr gives '<...>'
# and the angle brackets screw AutoDuck.
default = default.replace("<", "").replace(">", "")
ret.append(ArgInfo(n, ob, default))
ret.reverse()
return ret


def BuildInfo(name, ob):
ret = DocInfo(name, ob)
docstring = ob.__doc__ or ""
ret.desc = ret.short_desc = docstring.strip()
if ret.desc:
ret.short_desc = ret.desc.splitlines()[0]
return ret


def should_build_function(build_info):
return build_info.ob.__doc__ and not build_info.ob.__name__.startswith("_")
def should_build_function(fn: FunctionType | MethodType) -> bool:
return bool(fn.__doc__) and not fn.__name__.startswith("_")


# docstring aware paragraph generator. Isn't there something in docutils
# we can use?
def gen_paras(val):
chunks = []
# docstring aware paragraph generator.
# Isn't there something in docutils we can use?
def gen_paras(val: str) -> Generator[list[str], None, None]:
chunks: list[str] = []
in_docstring = False
for line in val.splitlines():
line = ad_escape(line.strip())
Expand All @@ -66,7 +72,7 @@ def gen_paras(val):
yield chunks or [""]


def format_desc(desc):
def format_desc(desc: str) -> str:
# A little complicated! Given the docstring for a module, we want to:
# write:
# 'first_para_of_docstring'
Expand All @@ -76,11 +82,10 @@ def format_desc(desc):
# '@iex >>> etc.'
if not desc:
return ""
g = gen_paras(desc)
first = next(g)
chunks = [first[0]]
chunks.extend(["// " + l for l in first[1:]])
for lines in g:
paragraphs = gen_paras(desc)
first_paragraph = next(paragraphs)
chunks = [first_paragraph[0], *[f"// {line}" for line in first_paragraph[1:]]]
for lines in paragraphs:
first = lines[0]
if first.startswith(">>> "):
prefix = "// @iex \n// "
Expand All @@ -91,88 +96,77 @@ def format_desc(desc):
return "\n".join(chunks)


def build_module(fp, mod_name):
def build_module(mod_name: str) -> None:
__import__(mod_name)
mod = sys.modules[mod_name]
functions = []
classes = []
constants = []
functions: list[DocInfo[FunctionType]] = []
classes: list[DocInfo[type]] = []
constants: list[tuple[str, int | str]] = []
for name, ob in mod.__dict__.items():
if name.startswith("_"):
continue
if hasattr(ob, "__module__") and ob.__module__ != mod_name:
continue
if type(ob) == type:
classes.append(BuildInfo(name, ob))
elif isinstance(ob, types.FunctionType):
functions.append(BuildInfo(name, ob))
classes.append(DocInfo(name, ob))
elif isinstance(ob, FunctionType):
if should_build_function(ob):
functions.append(DocInfo(name, ob))
elif name.upper() == name and isinstance(ob, (int, str)):
constants.append((name, ob))
info = BuildInfo(mod_name, mod)
print(f"// @module {mod_name}|{format_desc(info.desc)}", file=fp)
functions = [f for f in functions if should_build_function(f)]
module_info = DocInfo(mod_name, mod)
print(f"// @module {mod_name}|{format_desc(module_info.desc)}")
for ob in functions:
print(f"// @pymeth {ob.name}|{ob.short_desc}", file=fp)
print(f"// @pymeth {ob.name}|{ob.short_desc}")
for ob in classes:
# only classes with docstrings get printed.
if not ob.ob.__doc__:
continue
ob_name = mod_name + "." + ob.name
print(f"// @pyclass {ob.name}|{ob.short_desc}", file=fp)
print(f"// @pyclass {ob.name}|{ob.short_desc}")
for ob in functions:
print(
f"// @pymethod |{mod_name}|{ob.name}|{format_desc(ob.desc)}",
file=fp,
)
for ai in BuildArgInfos(ob.ob):
print(f"// @pyparm |{ai.name}|{ai.default}|{ai.short_desc}", file=fp)
print(f"// @pyparm |{ai.name}|{ai.default}|{ai.short_desc}")

for ob in classes:
# only classes with docstrings get printed.
if not ob.ob.__doc__:
continue
ob_name = mod_name + "." + ob.name
print(f"// @object {ob_name}|{format_desc(ob.desc)}", file=fp)
func_infos = []
print(f"// @object {ob_name}|{format_desc(ob.desc)}")
func_infos: list[DocInfo[FunctionType | MethodType]] = []
# We need to iter the keys then to a getattr() so the funky descriptor
# things work.
for n in ob.ob.__dict__:
o = getattr(ob.ob, n)
if isinstance(o, (types.FunctionType, types.MethodType)):
info = BuildInfo(n, o)
if should_build_function(info):
func_infos.append(info)
if isinstance(o, (FunctionType, MethodType)):
if should_build_function(o):
func_infos.append(DocInfo(n, o))
for fi in func_infos:
print(f"// @pymeth {fi.name}|{fi.short_desc}", file=fp)
print(f"// @pymeth {fi.name}|{fi.short_desc}")
for fi in func_infos:
print(
f"// @pymethod |{ob_name}|{fi.name}|{format_desc(fi.desc)}",
file=fp,
)
print(f"// @pymethod |{ob_name}|{fi.name}|{format_desc(fi.desc)}")
if hasattr(fi.ob, "im_self") and fi.ob.im_self is ob.ob:
print("// @comm This is a @classmethod.", file=fp)
print(
f"// @pymethod |{ob_name}|{fi.name}|{format_desc(fi.desc)}",
file=fp,
)
print("// @comm This is a @classmethod.")
print(f"// @pymethod |{ob_name}|{fi.name}|{format_desc(fi.desc)}")
for ai in BuildArgInfos(fi.ob):
print(
f"// @pyparm |{ai.name}|{ai.default}|{ai.short_desc}",
file=fp,
)
print(f"// @pyparm |{ai.name}|{ai.default}|{ai.short_desc}")

for name, val in constants:
desc = f"{name} = {val!r}"
if isinstance(val, int):
desc += f" (0x{val:x})"
print(f"// @const {mod_name}|{name}|{desc}", file=fp)
print(f"// @const {mod_name}|{name}|{desc}")


def main(fp, args):
print("// @doc", file=fp)
def main(args: Iterable[str]) -> None:
print("// @doc")
for arg in args:
build_module(sys.stdout, arg)
build_module(arg)


if __name__ == "__main__":
main(sys.stdout, sys.argv[1:])
main(sys.argv[1:])