Skip to content

Commit

Permalink
[stubgen] Introduce an object-oriented design for C extension stub ge…
Browse files Browse the repository at this point in the history
…neration
  • Loading branch information
chadrik committed Aug 3, 2023
1 parent 54bc37c commit 893d428
Show file tree
Hide file tree
Showing 12 changed files with 1,974 additions and 1,398 deletions.
14 changes: 12 additions & 2 deletions docs/source/stubgen.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,22 @@ alter the default behavior:
unwanted side effects, such as the running of tests. Stubgen tries to skip test
modules even without this option, but this does not always work.

.. option:: --parse-only
.. option:: --no-analysis

Don't perform semantic analysis of source files. This may generate
worse stubs -- in particular, some module, class, and function aliases may
be represented as variables with the ``Any`` type. This is generally only
useful if semantic analysis causes a critical mypy error.
useful if semantic analysis causes a critical mypy error. Does not apply to
C extension modules. Incompatible with :option:`--inspect-mode`.

.. option:: --inspect-mode

Import and inspect modules instead of parsing source code. This is the default
behavior for c modules and pyc-only packages. The flag is useful to force
inspection for pure python modules that make use of dynamically generated
members that would otherwiswe be omitted when using the default behavior of
code parsing. Implies :option:`--no-analysis` as analysis requires source
code.

.. option:: --doc-dir PATH

Expand Down
4 changes: 4 additions & 0 deletions mypy/moduleinspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def is_c_module(module: ModuleType) -> bool:
return os.path.splitext(module.__dict__["__file__"])[-1] in [".so", ".pyd", ".dll"]


def is_pyc_only(file: str | None) -> bool:
return bool(file and file.endswith(".pyc") and not os.path.exists(file[:-1]))


class InspectError(Exception):
pass

Expand Down
86 changes: 76 additions & 10 deletions mypy/stubdoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import contextlib
import io
import keyword
import re
import tokenize
from typing import Any, Final, MutableMapping, MutableSequence, NamedTuple, Sequence, Tuple
Expand Down Expand Up @@ -35,12 +36,16 @@ class ArgSig:

def __init__(self, name: str, type: str | None = None, default: bool = False):
self.name = name
if type and not is_valid_type(type):
raise ValueError("Invalid type: " + type)
self.type = type
# Does this argument have a default value?
self.default = default

def is_star_arg(self) -> bool:
return self.name.startswith("*") and not self.name.startswith("**")

def is_star_kwarg(self) -> bool:
return self.name.startswith("**")

def __repr__(self) -> str:
return "ArgSig(name={}, type={}, default={})".format(
repr(self.name), repr(self.type), repr(self.default)
Expand All @@ -59,7 +64,68 @@ def __eq__(self, other: Any) -> bool:
class FunctionSig(NamedTuple):
name: str
args: list[ArgSig]
ret_type: str
ret_type: str | None

def is_special_method(self) -> bool:
return bool(
self.name.startswith("__")
and self.name.endswith("__")
and self.args
and self.args[0].name in ("self", "cls")
)

def has_catchall_args(self) -> bool:
"""Return if this signature has catchall args: (*args, **kwargs)"""
if self.args and self.args[0].name in ("self", "cls"):
args = self.args[1:]
else:
args = self.args
return (
len(args) == 2
and all(a.type in (None, "Any", "typing.Any") for a in args)
and args[0].is_star_arg()
and args[1].is_star_kwarg()
)

def is_identity(self) -> bool:
"""Return if this signature is the catchall identity: (*args, **kwargs) -> Any"""
return self.has_catchall_args() and self.ret_type in (None, "Any", "typing.Any")

def format_sig(self, any_val: str | None = None, suffix: str = ": ...") -> str:
args: list[str] = []
for arg in self.args:
arg_def = arg.name

if arg_def in keyword.kwlist:
arg_def = "_" + arg_def

if (
arg.type is None
and any_val is not None
and arg.name not in ("self", "cls")
and not arg.name.startswith("*")
):
arg_type: str | None = any_val
else:
arg_type = arg.type
if arg_type:
arg_def += ": " + arg_type
if arg.default:
arg_def += " = ..."

elif arg.default:
arg_def += "=..."

args.append(arg_def)

retfield = ""
ret_type = self.ret_type if self.ret_type else any_val
if ret_type is not None:
retfield = " -> " + ret_type

return "def {name}({args}){ret}{suffix}".format(
name=self.name, args=", ".join(args), ret=retfield, suffix=suffix
)


# States of the docstring parser.
Expand Down Expand Up @@ -176,17 +242,17 @@ def add_token(self, token: tokenize.TokenInfo) -> None:

# arg_name is empty when there are no args. e.g. func()
if self.arg_name:
try:
if self.arg_type and not is_valid_type(self.arg_type):
# wrong type, use Any
self.args.append(
ArgSig(name=self.arg_name, type=None, default=bool(self.arg_default))
)
else:
self.args.append(
ArgSig(
name=self.arg_name, type=self.arg_type, default=bool(self.arg_default)
)
)
except ValueError:
# wrong type, use Any
self.args.append(
ArgSig(name=self.arg_name, type=None, default=bool(self.arg_default))
)
self.arg_name = ""
self.arg_type = None
self.arg_default = None
Expand Down Expand Up @@ -240,7 +306,7 @@ def args_kwargs(signature: FunctionSig) -> bool:


def infer_sig_from_docstring(docstr: str | None, name: str) -> list[FunctionSig] | None:
"""Convert function signature to list of TypedFunctionSig
"""Convert function signature to list of FunctionSig
Look for function signatures of function in docstring. Signature is a string of
the format <function_name>(<signature>) -> <return type> or perhaps without
Expand Down
Loading

0 comments on commit 893d428

Please sign in to comment.