mirror of https://github.com/KLayout/klayout.git
Added python stubs with type hinting and documentation. (#1125)
* Added python stubs with type hinting and documentation. The documentation was extracted by inspecting the docstrings within each class and methods. This should enable type hinting and checking by IDEs like VSCode. The stubs were automatically generated, and have not been manually curated. (TODO) * created tl ArgType to python type translator * Fixing a pyaModule.cc bug * almost finished stubgen * fixing little bug in classes defined within classes * Release version of the stubgen script. Good enough for release. * Adding notes * Including stubs when installing from source * typechecking bugfix: missed checking is_iter() * Cleaning up unused code for merge review * Including stubs when installing from source (part deux) * Putting the GenericDeviceExtractor name setting into the right place. * Updating python stubs Co-authored-by: Matthias Koefferlein <matthias@koefferlein.de>
This commit is contained in:
parent
cb3369e9be
commit
4d4f4b643b
|
|
@ -12,6 +12,7 @@ recursive-include src/pymod *.cc *.h
|
|||
recursive-include src/rbastub *.cc *.h
|
||||
recursive-include src/rdb/rdb *.cc *.h
|
||||
recursive-include src/tl/tl *.cc *.h
|
||||
recursive-include src/pymod *.pyi
|
||||
include src/plugins/*/db_plugin/*.cc
|
||||
include src/plugins/*/*/db_plugin/*.cc
|
||||
include src/plugins/*/db_plugin/*.h
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -759,4 +759,6 @@ if __name__ == '__main__':
|
|||
url='https://github.com/klayout/klayout',
|
||||
packages=find_packages('src/pymod/distutils_src'),
|
||||
package_dir={'': 'src/pymod/distutils_src'}, # https://github.com/pypa/setuptools/issues/230
|
||||
package_data={config.root: ["src/pymod/distutils_src/klayout/*.pyi"]},
|
||||
include_package_data=True,
|
||||
ext_modules=[_tl, _gsi, _pya, _rba, _db, _lib, _rdb, _lym, _laybasic, _layview, _ant, _edt, _img] + db_plugins + [tl, db, lib, rdb, lay])
|
||||
|
|
|
|||
|
|
@ -220,14 +220,6 @@ public:
|
|||
*/
|
||||
~NetlistDeviceExtractor ();
|
||||
|
||||
/**
|
||||
* @brief Gets the name of the extractor and the device class
|
||||
*/
|
||||
const std::string &name ()
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the property name for the device terminal annotation
|
||||
* This name is used to attach the terminal ID to terminal shapes.
|
||||
|
|
@ -321,6 +313,14 @@ public:
|
|||
m_name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the name of the device class and the device extractor
|
||||
*/
|
||||
const std::string &name () const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets up the extractor
|
||||
*
|
||||
|
|
|
|||
|
|
@ -711,7 +711,7 @@ Class<db::CompoundRegionOperationNode> decl_CompoundRegionOperationNode ("db", "
|
|||
"This node renders the input if the specified bounding box parameter of the input shape is between pmin and pmax (exclusively). If 'inverse' is set to true, the "
|
||||
"input shape is returned if the parameter is less than pmin (exclusively) or larger than pmax (inclusively)."
|
||||
) +
|
||||
gsi::constructor ("new_ratio_filter", &new_ratio_filter, gsi::arg ("input"), gsi::arg ("parameter"), gsi::arg ("inverse", false), gsi::arg ("pmin", 0.0), gsi::arg ("pmin_included"), gsi::arg ("pmax", std::numeric_limits<double>::max (), "max"), gsi::arg ("pmax_included", true),
|
||||
gsi::constructor ("new_ratio_filter", &new_ratio_filter, gsi::arg ("input"), gsi::arg ("parameter"), gsi::arg ("inverse", false), gsi::arg ("pmin", 0.0), gsi::arg ("pmin_included", true), gsi::arg ("pmax", std::numeric_limits<double>::max (), "max"), gsi::arg ("pmax_included", true),
|
||||
"@brief Creates a node filtering the input by ratio parameters.\n"
|
||||
"This node renders the input if the specified ratio parameter of the input shape is between pmin and pmax. If 'pmin_included' is true, the range will include pmin. Same for 'pmax_included' and pmax. "
|
||||
"If 'inverse' is set to true, the input shape is returned if the parameter is not within the specified range."
|
||||
|
|
@ -732,7 +732,7 @@ Class<db::CompoundRegionOperationNode> decl_CompoundRegionOperationNode ("db", "
|
|||
gsi::constructor ("new_edge_length_sum_filter", &new_edge_length_sum_filter, gsi::arg ("input"), gsi::arg ("inverse", false), gsi::arg ("lmin", 0), gsi::arg ("lmax", std::numeric_limits<db::Edge::distance_type>::max (), "max"),
|
||||
"@brief Creates a node filtering edges by their length sum (over the local set).\n"
|
||||
) +
|
||||
gsi::constructor ("new_edge_orientation_filter", &new_edge_orientation_filter, gsi::arg ("input"), gsi::arg ("inverse", false), gsi::arg ("amin"), gsi::arg ("include_amin"), gsi::arg ("amax"), gsi::arg ("include_amax"),
|
||||
gsi::constructor ("new_edge_orientation_filter", &new_edge_orientation_filter, gsi::arg ("input"), gsi::arg ("inverse"), gsi::arg ("amin"), gsi::arg ("include_amin"), gsi::arg ("amax"), gsi::arg ("include_amax"),
|
||||
"@brief Creates a node filtering edges by their orientation.\n"
|
||||
) +
|
||||
gsi::constructor ("new_polygons", &new_polygons, gsi::arg ("input"), gsi::arg ("e", 0),
|
||||
|
|
@ -923,4 +923,3 @@ gsi::EnumIn<db::CompoundRegionOperationNode, db::RegionRatioFilter::parameter_ty
|
|||
);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ struct edge_pair_defs
|
|||
return c.release ();
|
||||
}
|
||||
|
||||
static C *new_v ()
|
||||
static C *new_v ()
|
||||
{
|
||||
return new C ();
|
||||
}
|
||||
|
|
@ -67,29 +67,29 @@ struct edge_pair_defs
|
|||
static gsi::Methods methods ()
|
||||
{
|
||||
return
|
||||
constructor ("new", &new_v,
|
||||
constructor ("new", &new_v,
|
||||
"@brief Default constructor\n"
|
||||
"\n"
|
||||
"This constructor creates an default edge pair.\n"
|
||||
) +
|
||||
) +
|
||||
constructor ("new", &new_ee, gsi::arg ("first"), gsi::arg ("second"), gsi::arg ("symmetric", false),
|
||||
"@brief Constructor from two edges\n"
|
||||
"\n"
|
||||
"This constructor creates an edge pair from the two edges given.\n"
|
||||
"See \\symmetric? for a description of this attribute."
|
||||
) +
|
||||
method ("first", (const edge_type &(C::*) () const) &C::first,
|
||||
) +
|
||||
method ("first", (const edge_type &(C::*) () const) &C::first,
|
||||
"@brief Gets the first edge\n"
|
||||
) +
|
||||
) +
|
||||
method ("first=", &C::set_first, gsi::arg ("edge"),
|
||||
"@brief Sets the first edge\n"
|
||||
) +
|
||||
method ("second", (const edge_type &(C::*) () const) &C::second,
|
||||
) +
|
||||
method ("second", (const edge_type &(C::*) () const) &C::second,
|
||||
"@brief Gets the second edge\n"
|
||||
) +
|
||||
) +
|
||||
method ("second=", &C::set_second, gsi::arg ("edge"),
|
||||
"@brief Sets the second edge\n"
|
||||
) +
|
||||
) +
|
||||
method ("symmetric?", &C::is_symmetric,
|
||||
"@brief Returns a value indicating whether the edge pair is symmetric\n"
|
||||
"For symmetric edge pairs, the edges are commutable. Specifically, a symmetric edge pair with (e1,e2) is identical to (e2,e1). "
|
||||
|
|
@ -125,8 +125,8 @@ struct edge_pair_defs
|
|||
"Normalization is a first step recommended before converting an edge pair to a polygon, "
|
||||
"because that way the polygons won't be self-overlapping and the enlargement parameter "
|
||||
"is applied properly."
|
||||
) +
|
||||
method ("polygon", &C::to_polygon, gsi::arg ("e The enlargement (set to zero for exact representation)"),
|
||||
) +
|
||||
method ("polygon", &C::to_polygon, gsi::arg ("e"),
|
||||
"@brief Convert an edge pair to a polygon\n"
|
||||
"The polygon is formed by connecting the end and start points of the edges. It is recommended to "
|
||||
"use \\normalized before converting the edge pair to a polygon.\n"
|
||||
|
|
@ -137,8 +137,10 @@ struct edge_pair_defs
|
|||
"edge pairs consisting of two point-like edges.\n"
|
||||
"\n"
|
||||
"Another version for converting edge pairs to simple polygons is \\simple_polygon which renders a \\SimplePolygon object."
|
||||
) +
|
||||
method ("simple_polygon", &C::to_simple_polygon, gsi::arg ("e The enlargement (set to zero for exact representation)"),
|
||||
"\n"
|
||||
"@param e The enlargement (set to zero for exact representation)"
|
||||
) +
|
||||
method ("simple_polygon", &C::to_simple_polygon, gsi::arg ("e"),
|
||||
"@brief Convert an edge pair to a simple polygon\n"
|
||||
"The polygon is formed by connecting the end and start points of the edges. It is recommended to "
|
||||
"use \\normalized before converting the edge pair to a polygon.\n"
|
||||
|
|
@ -149,7 +151,9 @@ struct edge_pair_defs
|
|||
"edge pairs consisting of two point-like edges.\n"
|
||||
"\n"
|
||||
"Another version for converting edge pairs to polygons is \\polygon which renders a \\Polygon object."
|
||||
) +
|
||||
"\n"
|
||||
"@param e The enlargement (set to zero for exact representation)"
|
||||
) +
|
||||
constructor ("from_s", &from_string, gsi::arg ("s"),
|
||||
"@brief Creates an object from a string\n"
|
||||
"Creates the object from a string representation (as returned by \\to_s)\n"
|
||||
|
|
@ -162,7 +166,7 @@ struct edge_pair_defs
|
|||
"\n"
|
||||
"The DBU argument has been added in version 0.27.6.\n"
|
||||
) +
|
||||
method ("bbox", &C::bbox,
|
||||
method ("bbox", &C::bbox,
|
||||
"@brief Gets the bounding box of the edge pair\n"
|
||||
) +
|
||||
method ("<", &C::less, gsi::arg ("box"),
|
||||
|
|
|
|||
|
|
@ -266,6 +266,9 @@ Class<db::NetlistDeviceExtractor> decl_dbNetlistDeviceExtractor ("db", "DeviceEx
|
|||
gsi::method ("name", &db::NetlistDeviceExtractor::name,
|
||||
"@brief Gets the name of the device extractor and the device class."
|
||||
) +
|
||||
gsi::method ("name=", &db::NetlistDeviceExtractor::set_name, gsi::arg ("name"),
|
||||
"@brief Sets the name of the device extractor and the device class."
|
||||
) +
|
||||
gsi::method ("device_class", &db::NetlistDeviceExtractor::device_class,
|
||||
"@brief Gets the device class used during extraction\n"
|
||||
"The attribute will hold the actual device class used in the device extraction. It "
|
||||
|
|
@ -293,9 +296,6 @@ Class<db::NetlistDeviceExtractor> decl_dbNetlistDeviceExtractor ("db", "DeviceEx
|
|||
);
|
||||
|
||||
Class<GenericDeviceExtractor> decl_GenericDeviceExtractor (decl_dbNetlistDeviceExtractor, "db", "GenericDeviceExtractor",
|
||||
gsi::method ("name=", &GenericDeviceExtractor::set_name,
|
||||
"@brief Sets the name of the device extractor and the device class."
|
||||
) +
|
||||
gsi::callback ("setup", &GenericDeviceExtractor::setup, &GenericDeviceExtractor::cb_setup,
|
||||
"@brief Sets up the extractor.\n"
|
||||
"This method is supposed to set up the device extractor. This involves three basic steps:\n"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ static int t_object () { return T_object; }
|
|||
static int t_vector () { return T_vector; }
|
||||
static int t_map () { return T_map; }
|
||||
|
||||
static int type (const ArgType *t)
|
||||
static int type (const ArgType *t)
|
||||
{
|
||||
return t->type ();
|
||||
}
|
||||
|
|
@ -105,9 +105,13 @@ Class<ArgType> decl_ArgType ("tl", "ArgType",
|
|||
"@brief Return the basic type (see t_.. constants)\n"
|
||||
) +
|
||||
gsi::method ("inner", &ArgType::inner,
|
||||
"@brief Returns the inner ArgType object (i.e. value of a vector)\n"
|
||||
"@brief Returns the inner ArgType object (i.e. value of a vector/map)\n"
|
||||
"Starting with version 0.22, this method replaces the is_vector method.\n"
|
||||
) +
|
||||
gsi::method ("inner_k", &ArgType::inner_k,
|
||||
"@brief Returns the inner ArgType object (i.e. key of a map)\n"
|
||||
"This method has been introduced in version 0.27."
|
||||
) +
|
||||
gsi::method ("pass_obj?", &ArgType::pass_obj,
|
||||
"@brief True, if the ownership over an object represented by this type is passed to the receiver\n"
|
||||
"In case of the return type, a value of true indicates, that the object is a freshly created one and "
|
||||
|
|
@ -283,6 +287,12 @@ Class<ClassBase> decl_Class ("tl", "Class",
|
|||
gsi::iterator ("each_method", &ClassBase::begin_methods, &ClassBase::end_methods,
|
||||
"@brief Iterate over all methods of this class\n"
|
||||
) +
|
||||
gsi::iterator ("each_child_class", &ClassBase::begin_child_classes, &ClassBase::end_child_classes,
|
||||
"@brief Iterate over all child classes defined within this class\n"
|
||||
) +
|
||||
gsi::method ("parent", &ClassBase::parent,
|
||||
"@brief The parent of the class\n"
|
||||
) +
|
||||
gsi::method ("name", &ClassBase::name,
|
||||
"@brief The name of the class\n"
|
||||
) +
|
||||
|
|
@ -309,5 +319,3 @@ Class<ClassBase> decl_Class ("tl", "Class",
|
|||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -589,6 +589,8 @@ static std::string extract_python_name (const std::string &name)
|
|||
return "__ixor__";
|
||||
} else if (name == "[]") {
|
||||
return "__getitem__";
|
||||
} else if (name == "[]=") {
|
||||
return "__setitem__";
|
||||
} else {
|
||||
|
||||
const char *c = name.c_str ();
|
||||
|
|
@ -2684,7 +2686,7 @@ public:
|
|||
const gsi::MethodBase *m_first = *mt->begin (mid);
|
||||
|
||||
tl_assert (mid < sizeof (method_adaptors) / sizeof (method_adaptors[0]));
|
||||
if (! mt->is_static (mid)) {
|
||||
if (! mt->is_static (mid)) { // Bound methods
|
||||
|
||||
if (! as_static) {
|
||||
|
||||
|
|
@ -2781,7 +2783,7 @@ public:
|
|||
mp_module->add_python_doc (*cls, mt, int (mid), tl::to_string (tr ("This attribute is not available for Python")));
|
||||
}
|
||||
|
||||
} else if (! as_static) {
|
||||
} else if (! as_static) { // Class methods
|
||||
|
||||
if (m_first->ret_type ().type () == gsi::T_object && m_first->ret_type ().pass_obj () && name == "new") {
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,700 @@
|
|||
""" Stub file generation routines.
|
||||
|
||||
This module contains routines to generate stub files from klayout's python API.
|
||||
This uses the `tl` module of the API, which offers an introspection layer to
|
||||
the C-extension modules.
|
||||
"""
|
||||
|
||||
from collections import Counter
|
||||
from copy import copy
|
||||
from dataclasses import dataclass, field
|
||||
from functools import wraps
|
||||
import functools
|
||||
from sys import argv
|
||||
from textwrap import indent
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
import pya # initialize all modules
|
||||
import klayout.tl as ktl
|
||||
|
||||
|
||||
def qualified_name(_class: ktl.Class) -> str:
|
||||
name = _class.name()
|
||||
if _class.parent():
|
||||
return f"{qualified_name(_class.parent())}.{name}"
|
||||
else:
|
||||
return name
|
||||
|
||||
|
||||
def superclass(_class: ktl.Class) -> str:
|
||||
if _class.base():
|
||||
return superclass(_class.base())
|
||||
else:
|
||||
return _class.name()
|
||||
|
||||
|
||||
def is_reserved_word(name: str) -> bool:
|
||||
wordlist = [
|
||||
"and",
|
||||
"del",
|
||||
"from",
|
||||
"not",
|
||||
"while",
|
||||
"as",
|
||||
"elif",
|
||||
"global",
|
||||
"or",
|
||||
"with",
|
||||
"assert",
|
||||
"else",
|
||||
"if",
|
||||
"pass",
|
||||
"yield",
|
||||
"break",
|
||||
"except",
|
||||
"import",
|
||||
"print",
|
||||
"class",
|
||||
"exec",
|
||||
"in",
|
||||
"raise",
|
||||
"continue",
|
||||
"finally",
|
||||
"is",
|
||||
"return",
|
||||
"def",
|
||||
"for",
|
||||
"lambda",
|
||||
"try",
|
||||
"None",
|
||||
]
|
||||
return name in wordlist
|
||||
|
||||
|
||||
def translate_methodname(name: str) -> str:
|
||||
"""
|
||||
Should be the same as pyaModule.cc:extract_python_name function
|
||||
* The name string encodes some additional information, specifically:
|
||||
* "*..." The method is protected
|
||||
* "x|y" Aliases (synonyms)
|
||||
* "x|#y" y is deprecated
|
||||
* "x=" x is a setter
|
||||
* ":x" x is a getter
|
||||
* "x?" x is a predicate
|
||||
* Backslashes can be used to escape the special characters, like "*" and "|".
|
||||
"""
|
||||
if name == "new":
|
||||
new_name = "__init__"
|
||||
elif name == "++":
|
||||
new_name = "inc"
|
||||
elif name == "--":
|
||||
new_name = "dec"
|
||||
elif name == "()":
|
||||
new_name = "call"
|
||||
elif name == "!":
|
||||
new_name = "not"
|
||||
elif name == "==":
|
||||
new_name = "__eq__"
|
||||
elif name == "!=":
|
||||
new_name = "__ne__"
|
||||
elif name == "<":
|
||||
new_name = "__lt__"
|
||||
elif name == "<=":
|
||||
new_name = "__le__"
|
||||
elif name == ">":
|
||||
new_name = "__gt__"
|
||||
elif name == ">=":
|
||||
new_name = "__ge__"
|
||||
elif name == "<=>":
|
||||
new_name = "__cmp__"
|
||||
elif name == "+":
|
||||
new_name = "__add__"
|
||||
elif name == "+@":
|
||||
new_name = "__pos__"
|
||||
elif name == "-":
|
||||
new_name = "__sub__"
|
||||
elif name == "-@":
|
||||
new_name = "__neg__"
|
||||
elif name == "/":
|
||||
new_name = "__truediv__"
|
||||
elif name == "*":
|
||||
new_name = "__mul__"
|
||||
elif name == "%":
|
||||
new_name = "__mod__"
|
||||
elif name == "<<":
|
||||
new_name = "__lshift__"
|
||||
elif name == ">>":
|
||||
new_name = "__rshift__"
|
||||
elif name == "~":
|
||||
new_name = "__invert__"
|
||||
elif name == "&":
|
||||
new_name = "__and__"
|
||||
elif name == "|":
|
||||
new_name = "__or__"
|
||||
elif name == "^":
|
||||
new_name = "__xor__"
|
||||
elif name == "+=":
|
||||
new_name = "__iadd__"
|
||||
elif name == "-=":
|
||||
new_name = "__isub__"
|
||||
elif name == "/=":
|
||||
new_name = "__itruediv__"
|
||||
elif name == "*=":
|
||||
new_name = "__imul__"
|
||||
elif name == "%=":
|
||||
new_name = "__imod__"
|
||||
elif name == "<<=":
|
||||
new_name = "__ilshift__"
|
||||
elif name == ">>=":
|
||||
new_name = "__irshift__"
|
||||
elif name == "&=":
|
||||
new_name = "__iand__"
|
||||
elif name == "|=":
|
||||
new_name = "__ior__"
|
||||
elif name == "^=":
|
||||
new_name = "__ixor__"
|
||||
elif name == "[]":
|
||||
new_name = "__getitem__"
|
||||
elif name == "[]=":
|
||||
new_name = "__setitem__"
|
||||
else:
|
||||
# Ignore other conversions for now.
|
||||
if name.startswith("*"):
|
||||
print(name)
|
||||
new_name = name
|
||||
if is_reserved_word(new_name):
|
||||
new_name = new_name + "_"
|
||||
|
||||
return new_name
|
||||
|
||||
|
||||
_type_dict = dict()
|
||||
|
||||
_type_dict[ktl.ArgType.TypeBool] = "bool"
|
||||
_type_dict[ktl.ArgType.TypeChar] = "str"
|
||||
_type_dict[ktl.ArgType.TypeDouble] = "float"
|
||||
_type_dict[ktl.ArgType.TypeFloat] = "float"
|
||||
_type_dict[ktl.ArgType.TypeInt] = "int"
|
||||
_type_dict[ktl.ArgType.TypeLong] = "int"
|
||||
_type_dict[ktl.ArgType.TypeLongLong] = "int"
|
||||
# _type_dict[ktl.ArgType.TypeMap] = None
|
||||
# _type_dict[ktl.ArgType.TypeObject] = None
|
||||
_type_dict[ktl.ArgType.TypeSChar] = "str"
|
||||
_type_dict[ktl.ArgType.TypeShort] = "int"
|
||||
_type_dict[ktl.ArgType.TypeString] = "str"
|
||||
_type_dict[ktl.ArgType.TypeUChar] = "str"
|
||||
_type_dict[ktl.ArgType.TypeUInt] = "int"
|
||||
_type_dict[ktl.ArgType.TypeULong] = "int"
|
||||
_type_dict[ktl.ArgType.TypeULongLong] = "int"
|
||||
_type_dict[ktl.ArgType.TypeUShort] = "int"
|
||||
_type_dict[ktl.ArgType.TypeVar] = "Any"
|
||||
# _type_dict[ktl.ArgType.TypeVector] = None
|
||||
_type_dict[ktl.ArgType.TypeVoid] = "None"
|
||||
_type_dict[ktl.ArgType.TypeVoidPtr] = "None"
|
||||
|
||||
|
||||
def _translate_type(arg_type: ktl.ArgType, within_class: ktl.Class) -> str:
|
||||
"""Translates klayout's C-type to a type in Python.
|
||||
|
||||
This function is equivalent to the `type_to_s` in `pyaModule.cc`.
|
||||
See also `type_to_s` in `layGSIHelpProvider.cc`"""
|
||||
|
||||
py_str: str = ""
|
||||
if arg_type.type() == ktl.ArgType.TypeObject:
|
||||
if within_class.module() and arg_type.cls().module() != within_class.module():
|
||||
py_str = arg_type.cls().module() + "." + qualified_name(arg_type.cls())
|
||||
else:
|
||||
py_str = qualified_name(arg_type.cls())
|
||||
elif arg_type.type() == ktl.ArgType.TypeMap:
|
||||
inner_key = _translate_type(arg_type.inner_k(), within_class)
|
||||
inner_val = _translate_type(arg_type.inner(), within_class)
|
||||
py_str = f"Dict[{inner_key}, {inner_val}]"
|
||||
elif arg_type.type() == ktl.ArgType.TypeVector:
|
||||
py_str = f"Iterable[{_translate_type(arg_type.inner(), within_class)}]"
|
||||
else:
|
||||
py_str = _type_dict[arg_type.type()]
|
||||
|
||||
if arg_type.is_iter():
|
||||
py_str = f"Iterable[{py_str}]"
|
||||
if arg_type.has_default():
|
||||
py_str = f"Optional[{py_str}] = ..."
|
||||
return py_str
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Method:
|
||||
name: str
|
||||
is_setter: bool
|
||||
is_getter: bool
|
||||
is_classvar: bool
|
||||
is_classmethod: bool
|
||||
doc: str
|
||||
m: ktl.Method
|
||||
|
||||
|
||||
@dataclass
|
||||
class Stub:
|
||||
signature: str
|
||||
name: Any
|
||||
docstring: str
|
||||
indent_docstring: bool = True
|
||||
child_stubs: List["Stub"] = field(default_factory=list)
|
||||
decorator: str = ""
|
||||
|
||||
def __eq__(self, __o: object) -> bool:
|
||||
if not isinstance(__o, Stub):
|
||||
return False
|
||||
return (
|
||||
self.signature == __o.signature
|
||||
and self.child_stubs == __o.child_stubs
|
||||
and self.decorator == __o.decorator
|
||||
)
|
||||
|
||||
def __lt__(self, other: "Stub") -> bool:
|
||||
# mypy complains if an overload with float happens before int
|
||||
self_signature = self.signature.replace(": int", "0").replace(": float", "1")
|
||||
other_signature = other.signature.replace(": int", "0").replace(": float", "1")
|
||||
|
||||
self_sortkey = self.name, len(self.signature.split(",")), self_signature
|
||||
other_sortkey = other.name, len(other.signature.split(",")), other_signature
|
||||
return self_sortkey < other_sortkey
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.format_stub(include_docstring=False))
|
||||
|
||||
def format_stub(self, include_docstring=True):
|
||||
lines = []
|
||||
lines.extend(self.decorator.splitlines())
|
||||
if self.indent_docstring: # all but properties
|
||||
if include_docstring or len(self.child_stubs) > 0:
|
||||
lines.append(self.signature + ":")
|
||||
else:
|
||||
lines.append(self.signature + ": ...")
|
||||
else:
|
||||
lines.append(self.signature)
|
||||
|
||||
stub_str = "\n".join(lines)
|
||||
|
||||
lines = []
|
||||
lines.append('r"""')
|
||||
lines.extend(self.docstring.splitlines())
|
||||
lines.append('"""')
|
||||
doc_str = "\n".join(lines)
|
||||
|
||||
# indent only if it is required (methods, not properties)
|
||||
if self.indent_docstring:
|
||||
doc_str = indent(doc_str, " " * 4)
|
||||
|
||||
if include_docstring:
|
||||
stub_str += "\n"
|
||||
stub_str += doc_str
|
||||
|
||||
for stub in self.child_stubs:
|
||||
stub_str += "\n"
|
||||
stub_str += indent(
|
||||
stub.format_stub(include_docstring=include_docstring), " " * 4
|
||||
)
|
||||
|
||||
return stub_str
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class MethodStub(Stub):
|
||||
indent_docstring: bool = True
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class PropertyStub(Stub):
|
||||
indent_docstring: bool = False
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class ClassStub(Stub):
|
||||
indent_docstring: bool = True
|
||||
|
||||
|
||||
def get_c_methods(c: ktl.Class) -> List[_Method]:
|
||||
"""
|
||||
Iterates over all methods defined in the C API, sorting
|
||||
properties, class methods and bound methods.
|
||||
"""
|
||||
method_list: List[_Method] = list()
|
||||
setters = set()
|
||||
|
||||
def primary_synonym(m: ktl.Method) -> ktl.MethodOverload:
|
||||
for ms in m.each_overload():
|
||||
if ms.name() == m.primary_name():
|
||||
return ms
|
||||
raise ("Primary synonym not found for method " + m.name())
|
||||
|
||||
for m in c.each_method():
|
||||
if m.is_signal():
|
||||
# ignore signals as they do not have arguments and are neither setters nor getters.
|
||||
continue
|
||||
|
||||
method_def = primary_synonym(m)
|
||||
if method_def.is_setter():
|
||||
setters.add(method_def.name())
|
||||
|
||||
for m in c.each_method():
|
||||
num_args = len([True for a in m.each_argument()])
|
||||
method_def = primary_synonym(m)
|
||||
|
||||
# extended definition of "getter" for Python
|
||||
is_getter = (num_args == 0) and (
|
||||
method_def.is_getter()
|
||||
or (not method_def.is_setter() and method_def.name() in setters)
|
||||
)
|
||||
is_setter = (num_args == 1) and method_def.is_setter()
|
||||
is_classvar = (num_args == 0) and (m.is_static() and not m.is_constructor())
|
||||
method_list.append(
|
||||
_Method(
|
||||
name=method_def.name(),
|
||||
is_setter=is_setter,
|
||||
is_getter=is_getter or is_classvar,
|
||||
is_classmethod=m.is_constructor(),
|
||||
is_classvar=is_classvar,
|
||||
doc=m.doc(),
|
||||
m=m,
|
||||
)
|
||||
)
|
||||
|
||||
# print(f"{m.name()}: {m.is_static()=}, {m.is_constructor()=}, {m.is_const_object()=}")
|
||||
return method_list
|
||||
|
||||
|
||||
def get_py_child_classes(c: ktl.Class):
|
||||
for c_child in c.each_child_class():
|
||||
return c_child
|
||||
|
||||
|
||||
def get_py_methods(
|
||||
c: ktl.Class,
|
||||
) -> List[Stub]:
|
||||
c_methods = get_c_methods(c)
|
||||
|
||||
# extract properties
|
||||
_c_methods = copy(c_methods)
|
||||
|
||||
# Helper functions
|
||||
def find_setter(c_methods: List[_Method], name: str):
|
||||
"""Finds a setter method in c_methods list with a given name.f"""
|
||||
for m in c_methods:
|
||||
if m.name == name and m.is_setter:
|
||||
return m
|
||||
return None
|
||||
|
||||
def find_getter(c_methods: List[_Method], name: str):
|
||||
"""Finds a getter method in c_methods list with a given name.f"""
|
||||
for m in c_methods:
|
||||
if m.name == name and m.is_getter:
|
||||
return m
|
||||
return None
|
||||
|
||||
translate_type = functools.partial(_translate_type, within_class=c)
|
||||
|
||||
def _get_arglist(m: ktl.Method, self_str) -> List[Tuple[str, ktl.ArgType]]:
|
||||
args = [(self_str, None)]
|
||||
for i, a in enumerate(m.each_argument()):
|
||||
argname = a.name()
|
||||
if is_reserved_word(argname):
|
||||
argname += "_"
|
||||
elif not argname:
|
||||
argname = f"arg{i}"
|
||||
args.append((argname, a))
|
||||
return args
|
||||
|
||||
def _format_args(arglist: List[Tuple[str, Optional[str]]]):
|
||||
args = []
|
||||
for argname, argtype in arglist:
|
||||
if argtype:
|
||||
args.append(f"{argname}: {argtype}")
|
||||
else:
|
||||
args.append(argname)
|
||||
return ", ".join(args)
|
||||
|
||||
def format_args(m: ktl.Method, self_str: str = "self") -> str:
|
||||
arg_list = _get_arglist(m, self_str=self_str)
|
||||
new_arglist: List[Tuple[str, Optional[str]]] = []
|
||||
for argname, a in arg_list:
|
||||
if a:
|
||||
new_arglist.append((argname, translate_type(a)))
|
||||
else:
|
||||
new_arglist.append((argname, None))
|
||||
return _format_args(new_arglist)
|
||||
|
||||
# Extract all properties (methods that have getters and/or setters)
|
||||
properties: List[Stub] = list()
|
||||
for m in copy(_c_methods):
|
||||
ret_type = translate_type(m.m.ret_type())
|
||||
if m.is_getter:
|
||||
m_setter = find_setter(c_methods, m.name)
|
||||
if m_setter is not None: # full property
|
||||
doc = m.doc + m_setter.doc
|
||||
properties.append(
|
||||
PropertyStub(
|
||||
decorator="",
|
||||
signature=f"{translate_methodname(m.name)}: {ret_type}",
|
||||
name=f"{translate_methodname(m.name)}",
|
||||
docstring=doc,
|
||||
)
|
||||
)
|
||||
# _c_methods.remove(m_setter)
|
||||
elif m.is_classvar:
|
||||
properties.append(
|
||||
PropertyStub(
|
||||
decorator="",
|
||||
signature=f"{translate_methodname(m.name)}: ClassVar[{ret_type}]",
|
||||
name=f"{translate_methodname(m.name)}",
|
||||
docstring=m.doc,
|
||||
)
|
||||
)
|
||||
else: # only getter
|
||||
properties.append(
|
||||
MethodStub(
|
||||
decorator="@property",
|
||||
signature=f"def {translate_methodname(m.name)}(self) -> {ret_type}",
|
||||
name=f"{translate_methodname(m.name)}",
|
||||
docstring=m.doc,
|
||||
)
|
||||
)
|
||||
_c_methods.remove(m)
|
||||
elif m.is_setter and not find_getter(
|
||||
c_methods, m.name
|
||||
): # include setter-only properties as full properties
|
||||
doc = "WARNING: This variable can only be set, not retrieved.\n" + m.doc
|
||||
properties.append(
|
||||
PropertyStub(
|
||||
decorator="",
|
||||
signature=f"{translate_methodname(m.name)}: {ret_type}",
|
||||
name=f"{translate_methodname(m.name)}",
|
||||
docstring=doc,
|
||||
)
|
||||
)
|
||||
_c_methods.remove(m)
|
||||
|
||||
for m in copy(_c_methods):
|
||||
if m.is_setter:
|
||||
_c_methods.remove(m)
|
||||
|
||||
def get_altnames(c_name: str):
|
||||
names = [c_name]
|
||||
if c_name == "to_s":
|
||||
names.append("__str__")
|
||||
return names
|
||||
|
||||
# Extract all classmethods
|
||||
classmethods: List[Stub] = list()
|
||||
for m in copy(_c_methods):
|
||||
if m.is_classmethod:
|
||||
# Exception: if it is an __init__ constructor, ignore.
|
||||
# Will be treated by the bound method logic below.
|
||||
if translate_methodname(m.name) == "__init__":
|
||||
continue
|
||||
decorator = "@classmethod"
|
||||
ret_type = translate_type(m.m.ret_type())
|
||||
for name in get_altnames(m.name):
|
||||
classmethods.append(
|
||||
MethodStub(
|
||||
decorator=decorator,
|
||||
signature=f"def {translate_methodname(name)}({format_args(m.m, 'cls')}) -> {ret_type}",
|
||||
name=f"{translate_methodname(name)}",
|
||||
docstring=m.doc,
|
||||
)
|
||||
)
|
||||
_c_methods.remove(m)
|
||||
|
||||
# Extract bound methods
|
||||
boundmethods: List[Stub] = list()
|
||||
for m in copy(_c_methods):
|
||||
decorator = ""
|
||||
|
||||
translated_name = translate_methodname(m.name)
|
||||
if translated_name in [s.name for s in properties]:
|
||||
translated_name += "_"
|
||||
|
||||
if translated_name == "__init__":
|
||||
ret_type = "None"
|
||||
else:
|
||||
ret_type = translate_type(m.m.ret_type())
|
||||
|
||||
arg_list = _get_arglist(m.m, "self")
|
||||
# Exceptions:
|
||||
# For X.__eq__(self, a:X), treat second argument as type object instead of X
|
||||
if translated_name in ("__eq__", "__ne__"):
|
||||
arg_list[1] = arg_list[1][0], "object"
|
||||
# X._assign(self, other:X), mypy complains if _assign is defined at base class.
|
||||
# We can't specialize other in this case.
|
||||
elif translated_name in ("_assign", "assign"):
|
||||
assert arg_list[1][1].type() == ktl.ArgType.TypeObject
|
||||
arg_list[1] = arg_list[1][0], superclass(arg_list[1][1].cls())
|
||||
else:
|
||||
new_arg_list = []
|
||||
for argname, a in arg_list:
|
||||
if a:
|
||||
new_arg_list.append((argname, translate_type(a)))
|
||||
else:
|
||||
new_arg_list.append((argname, a))
|
||||
arg_list = new_arg_list
|
||||
formatted_args = _format_args(arg_list)
|
||||
|
||||
for name in get_altnames(translated_name):
|
||||
boundmethods.append(
|
||||
MethodStub(
|
||||
decorator=decorator,
|
||||
signature=f"def {name}({formatted_args}) -> {ret_type}",
|
||||
name=f"{name}",
|
||||
docstring=m.doc,
|
||||
)
|
||||
)
|
||||
_c_methods.remove(m)
|
||||
|
||||
def add_overload_decorator(stublist: List[Stub]):
|
||||
stubnames = [stub.name for stub in stublist]
|
||||
for stub in stublist:
|
||||
has_duplicate = stubnames.count(stub.name) > 1
|
||||
if has_duplicate:
|
||||
stub.decorator = "@overload\n" + stub.decorator
|
||||
return stublist
|
||||
|
||||
boundmethods = sorted(set(boundmethods)) # sometimes duplicate methods are defined.
|
||||
add_overload_decorator(boundmethods)
|
||||
properties = sorted(set(properties))
|
||||
classmethods = sorted(classmethods)
|
||||
add_overload_decorator(classmethods)
|
||||
|
||||
return_list: List[Stub] = properties + classmethods + boundmethods
|
||||
|
||||
return return_list
|
||||
|
||||
|
||||
def get_class_stub(
|
||||
c: ktl.Class,
|
||||
ignore: List[ktl.Class] = None,
|
||||
module: str = "",
|
||||
) -> ClassStub:
|
||||
base = ""
|
||||
if c.base():
|
||||
base = f"({c.base().name()})"
|
||||
if c.module() != module:
|
||||
full_name = c.module() + "." + c.name()
|
||||
else:
|
||||
full_name = c.name()
|
||||
_cstub = ClassStub(
|
||||
signature="class " + full_name + base, docstring=c.doc(), name=full_name
|
||||
)
|
||||
child_attributes = get_py_methods(c)
|
||||
for child_c in c.each_child_class():
|
||||
_cstub.child_stubs.append(
|
||||
get_class_stub(
|
||||
child_c,
|
||||
ignore=ignore,
|
||||
module=c.module(),
|
||||
)
|
||||
)
|
||||
for stub in child_attributes:
|
||||
_cstub.child_stubs.append(stub)
|
||||
return _cstub
|
||||
|
||||
def get_classes(module: str) -> List[ktl.Class]:
|
||||
_classes = []
|
||||
for c in ktl.Class.each_class():
|
||||
if c.module() != module:
|
||||
continue
|
||||
_classes.append(c)
|
||||
return _classes
|
||||
|
||||
def get_module_stubs(module:str) -> List[ClassStub]:
|
||||
_stubs = []
|
||||
_classes = get_classes(module)
|
||||
for c in _classes:
|
||||
_cstub = get_class_stub(c, ignore=_classes, module=module)
|
||||
_stubs.append(_cstub)
|
||||
return _stubs
|
||||
|
||||
|
||||
def print_db():
|
||||
print("from typing import Any, ClassVar, Dict, Iterable, Optional")
|
||||
print("from typing import overload")
|
||||
print("import klayout.rdb as rdb")
|
||||
print("import klayout.tl as tl")
|
||||
for stub in get_module_stubs("db"):
|
||||
print(stub.format_stub(include_docstring=True) + "\n")
|
||||
|
||||
|
||||
def print_rdb():
|
||||
print("from typing import Any, ClassVar, Dict, Iterable, Optional")
|
||||
print("from typing import overload")
|
||||
print("import klayout.db as db")
|
||||
for stub in get_module_stubs("rdb"):
|
||||
print(stub.format_stub(include_docstring=True) + "\n")
|
||||
|
||||
def print_tl():
|
||||
print("from typing import Any, ClassVar, Dict, Iterable, Optional")
|
||||
print("from typing import overload")
|
||||
for stub in get_module_stubs("tl"):
|
||||
print(stub.format_stub(include_docstring=True) + "\n")
|
||||
|
||||
|
||||
def test_v1():
|
||||
db_classes = get_classes("db")
|
||||
for c in db_classes:
|
||||
if c.name() != "Region":
|
||||
continue
|
||||
print(
|
||||
get_class_stub(c, ignore=db_classes, module="db").format_stub(
|
||||
include_docstring=False
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_v2():
|
||||
db_classes = get_classes("db")
|
||||
for c in db_classes:
|
||||
if c.name() != "DPoint":
|
||||
continue
|
||||
print(
|
||||
get_class_stub(c, ignore=db_classes, module="db").format_stub(
|
||||
include_docstring=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_v3():
|
||||
db_classes = get_classes("db")
|
||||
for c in db_classes:
|
||||
if c.name() != "Instance":
|
||||
continue
|
||||
print(
|
||||
get_class_stub(c, ignore=db_classes, module="db").format_stub(
|
||||
include_docstring=False
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_v4():
|
||||
db_classes = get_classes("db")
|
||||
for c in db_classes:
|
||||
if c.name() != "Region":
|
||||
continue
|
||||
for cclass in get_py_child_classes(c):
|
||||
print(
|
||||
get_class_stub(cclass, ignore=db_classes, module="db").format_stub(
|
||||
include_docstring=False
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(argv) < 2:
|
||||
print("Specity module in argument: 'db', 'rdb', 'tl'")
|
||||
exit(1)
|
||||
if argv[1] == "db":
|
||||
print_db()
|
||||
elif argv[1] == "rdb":
|
||||
print_rdb()
|
||||
elif argv[1] == "tl":
|
||||
print_tl()
|
||||
else:
|
||||
# print_rdb()
|
||||
# test_v4()
|
||||
print("Wrong arguments")
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
Author: Thomas Ferreira de Lima
|
||||
email: thomas@tlima.me
|
||||
|
||||
## Notes
|
||||
To use the stubgen script for the three main modules, run the following from the root folder of the repository:
|
||||
`$ python ./src/pymod/stubgen.py db >! src/pymod/distutils_src/klayout/dbcore.pyi`
|
||||
`$ python ./src/pymod/stubgen.py rdb >! src/pymod/distutils_src/klayout/rdbcore.pyi`
|
||||
`$ python ./src/pymod/stubgen.py tl >! src/pymod/distutils_src/klayout/tlcore.pyi`
|
||||
|
||||
To compare the generated stubs with a python self-inspection of the klayout module, try the following:
|
||||
Navigate to `./src/pymod/distutils_src`.
|
||||
Run, for example:
|
||||
`$ stubtest klayout.tlcore`
|
||||
|
||||
TODO:
|
||||
- [ ] Integrate above scripts with CI
|
||||
## Old notes
|
||||
CHECKLIST:
|
||||
- [x] 1. Use klayout.tl to inspect all classes and methods in pya.
|
||||
- [x] 2. Figure out last few bugs.
|
||||
- DPoint has a method with "=" when it should have been "*=". There must be an issue with the gsiDeclInternal algorithms.
|
||||
- Some inner classes, e.g. LogicalOp inside CompoundRegionOperationNode are not returning
|
||||
- [x] 3. Manually check and compare to mypy's output.
|
||||
- Looks good, but there are a few discrepancies between actual python module and stubs. Namely, deprecated methods were not included in the stub. The opposite is sometimes true as well, though for newer, experimental classes e.g. `klayout.dbcore.GenericDeviceCombiner.combine_devices`.
|
||||
Loading…
Reference in New Issue