|
@@ -4,6 +4,8 @@
|
|
|
|
|
|
import argparse
|
|
|
import os
|
|
|
+import re
|
|
|
+import textwrap
|
|
|
import typing as t
|
|
|
from io import StringIO
|
|
|
|
|
@@ -15,6 +17,9 @@ def error(message):
|
|
|
exit(1)
|
|
|
|
|
|
|
|
|
+I = " " # noqa: E741 - Indentation is 4 spaces
|
|
|
+
|
|
|
+
|
|
|
def generate_doc(library: ElementLibrary) -> str: # noqa: C901F
|
|
|
stream = StringIO()
|
|
|
|
|
@@ -22,8 +27,12 @@ def generate_doc(library: ElementLibrary) -> str: # noqa: C901F
|
|
|
if not doc_string:
|
|
|
return None
|
|
|
lines = doc_string.splitlines()
|
|
|
- min_indent = min((len(line) - len(line.lstrip())) for line in lines if line.strip())
|
|
|
- lines = [line[min_indent:] if line.strip() else "" for line in lines]
|
|
|
+ first_line = lines.pop(0) if len(lines[0]) == len(lines[0].lstrip()) else None
|
|
|
+ if lines:
|
|
|
+ min_indent = min((len(line) - len(line.lstrip())) for line in lines if line.strip())
|
|
|
+ lines = [line[min_indent:] if line.strip() else "" for line in lines]
|
|
|
+ if first_line:
|
|
|
+ lines.insert(0, first_line)
|
|
|
while lines and not lines[0].strip():
|
|
|
lines.pop(0)
|
|
|
while lines and not lines[-1].strip():
|
|
@@ -33,90 +42,119 @@ def generate_doc(library: ElementLibrary) -> str: # noqa: C901F
|
|
|
print("# ----------------------------------------------------------------------", file=stream)
|
|
|
print("# Generated by taipy.gui.extension module", file=stream)
|
|
|
print("# ----------------------------------------------------------------------", file=stream)
|
|
|
+ print("import typing as t", file=stream)
|
|
|
+ print("", file=stream)
|
|
|
+ first_element = True
|
|
|
for element_name, element in library.get_elements().items():
|
|
|
- properties: list[str] = []
|
|
|
+ if first_element:
|
|
|
+ first_element = False
|
|
|
+ else:
|
|
|
+ print("\n", file=stream)
|
|
|
+ property_names: list[str] = []
|
|
|
+ parameters: list[str] = []
|
|
|
property_doc = {}
|
|
|
default_property_found = False
|
|
|
for property_name, property in element.attributes.items():
|
|
|
- desc = property_name
|
|
|
- # Could use 'match' with Python >= 3.10
|
|
|
- if property.property_type in [PropertyType.boolean, PropertyType.dynamic_boolean]:
|
|
|
- desc = desc + ": bool"
|
|
|
- elif property.property_type in [PropertyType.string, PropertyType.dynamic_string]:
|
|
|
- desc = desc + ": str"
|
|
|
- elif property.property_type in [PropertyType.dict, PropertyType.dynamic_dict]:
|
|
|
- desc = desc + ": dict"
|
|
|
+ property_names.append(property_name)
|
|
|
+ prop_def_value = property.default_value
|
|
|
+ prop_type = property.type_hint
|
|
|
+ if prop_type:
|
|
|
+ prop_type = re.sub(r"\b(?<!t\.)\b(Optional|Union)\[", r"t.\1[", prop_type)
|
|
|
+ else:
|
|
|
+ # Could use 'match' with Python >= 3.10
|
|
|
+ prop_type = "t.Union[str, any]"
|
|
|
+ if property.property_type in [PropertyType.boolean, PropertyType.dynamic_boolean]:
|
|
|
+ prop_type = "t.Union[bool, str]"
|
|
|
+ elif property.property_type in [PropertyType.string, PropertyType.dynamic_string]:
|
|
|
+ prop_type = "str"
|
|
|
+ if prop_def_value:
|
|
|
+ prop_def_value = f'"{str(prop_def_value)}"'
|
|
|
+ elif property.property_type in [PropertyType.dict, PropertyType.dynamic_dict]:
|
|
|
+ prop_type = "t.Union[dict, str]"
|
|
|
+ if prop_def_value:
|
|
|
+ prop_def_value = f'"{str(prop_def_value)}"'
|
|
|
+ if prop_def_value is None:
|
|
|
+ prop_def_value = "None"
|
|
|
+ prop_type = f"t.Optional[{prop_type}]"
|
|
|
+ desc = f"{property_name}: {prop_type} = {prop_def_value}"
|
|
|
if property_name == element.default_attribute:
|
|
|
- properties.insert(0, desc)
|
|
|
+ parameters.insert(0, desc)
|
|
|
default_property_found = True
|
|
|
else:
|
|
|
- properties.append(desc)
|
|
|
+ parameters.append(desc)
|
|
|
if doc_string := clean_doc_string(property.doc_string):
|
|
|
property_doc[property_name] = doc_string
|
|
|
- if default_property_found and len(properties) > 1:
|
|
|
- properties.insert(1, "*")
|
|
|
+ if default_property_found and len(parameters) > 1:
|
|
|
+ parameters.insert(1, "*")
|
|
|
doc_string = clean_doc_string(element.doc_string)
|
|
|
documentation = ""
|
|
|
if doc_string:
|
|
|
lines = doc_string.splitlines()
|
|
|
- documentation = f' """{lines.pop(0)}\n'
|
|
|
+ documentation = f'{I}"""{lines.pop(0)}\n'
|
|
|
while lines:
|
|
|
line = lines.pop(0)
|
|
|
- documentation += f" {line}\n" if line else "\n"
|
|
|
+ documentation += f"{I}{line}\n" if line else "\n"
|
|
|
if property_doc:
|
|
|
- documentation += "\n Arguments:\n"
|
|
|
- for property_name, doc_string in property_doc.items():
|
|
|
- lines = doc_string.splitlines()
|
|
|
- documentation += f" {property_name}: {lines.pop(0)}\n"
|
|
|
- while lines:
|
|
|
- line = lines.pop(0)
|
|
|
- if line:
|
|
|
- documentation += f" {line}\n"
|
|
|
+ documentation += f"\n{I}### Parameters:\n"
|
|
|
+ for property_name in property_names:
|
|
|
+ doc_string = property_doc.get(property_name)
|
|
|
+ if doc_string:
|
|
|
+ lines = doc_string.splitlines()
|
|
|
+ documentation += f"{I}`{property_name}`: {lines.pop(0)}\n"
|
|
|
+ for line in lines:
|
|
|
+ documentation += f"{I * 2}{line}\n" if line else "\n"
|
|
|
+ else:
|
|
|
+ documentation += f"{I}`{property_name}`: ...\n"
|
|
|
+ documentation += "\n"
|
|
|
if documentation:
|
|
|
- documentation += ' """\n'
|
|
|
- print(f"def {element_name}({', '.join(properties)}):\n{documentation} ...\n\n", file=stream)
|
|
|
+ documentation += f'{I}"""\n'
|
|
|
+ parameters_list = ",\n".join([f"{I}{p}" for p in parameters])
|
|
|
+ print(f"def {element_name}(\n{parameters_list},\n):\n{documentation}{I}...", file=stream)
|
|
|
|
|
|
return stream.getvalue()
|
|
|
|
|
|
|
|
|
-def generate_tgb(args) -> None:
|
|
|
+def generate_tgb(args) -> int:
|
|
|
from importlib import import_module
|
|
|
from inspect import getmembers, isclass
|
|
|
|
|
|
package_root_dir = args.package_root_dir[0]
|
|
|
# Remove potential directory separator at the end of the package root dir
|
|
|
- if package_root_dir[-1] == "/" or package_root_dir[-1] == "\\":
|
|
|
+ if package_root_dir[-1] == "/" or package_root_dir[-1] == "\\": # pragma: no cover
|
|
|
package_root_dir = package_root_dir[:-1]
|
|
|
module = None
|
|
|
try:
|
|
|
module = import_module(package_root_dir)
|
|
|
- except Exception as e:
|
|
|
+ except Exception as e: # pragma: no cover
|
|
|
error(f"Couldn't open module '{package_root_dir}' ({e})")
|
|
|
library: t.Optional[ElementLibrary] = None
|
|
|
for _, member in getmembers(module, lambda o: isclass(o) and issubclass(o, ElementLibrary)):
|
|
|
- if library:
|
|
|
+ if library: # pragma: no cover
|
|
|
error("Extension contains more than one ElementLibrary")
|
|
|
library = member()
|
|
|
- if library is None:
|
|
|
+ if library is None: # pragma: no cover
|
|
|
error("Extension does not contain any ElementLibrary")
|
|
|
- return # To avoid having to deal with this case in the following code
|
|
|
- pyi_path = os.path.join(package_root_dir, "__init__.pyi")
|
|
|
+ return 1 # To avoid having to deal with this case in the following code
|
|
|
+
|
|
|
+ if (pyi_path := args.output_path) is None: # pragma: no cover
|
|
|
+ pyi_path = os.path.join(package_root_dir, "__init__.pyi")
|
|
|
pyi_file = None
|
|
|
try:
|
|
|
pyi_file = open(pyi_path, "w")
|
|
|
- except Exception as e:
|
|
|
- error(f"Couldn't open Python Interface Definition file '{pyi_file}' ({e})")
|
|
|
+ except Exception as e: # pragma: no cover
|
|
|
+ error(f"Couldn't open Python Interface Definition file '{pyi_path}' ({e})")
|
|
|
|
|
|
print(f"Inspecting extension library '{library.get_name()}'") # noqa: T201
|
|
|
content = generate_doc(library)
|
|
|
|
|
|
if pyi_file:
|
|
|
- print(content, file=pyi_file)
|
|
|
+ print(content, file=pyi_file, end="")
|
|
|
pyi_file.close()
|
|
|
print(f"File '{pyi_path}' was updated.") # noqa: T201
|
|
|
+ return 0
|
|
|
|
|
|
|
|
|
-def main(arg_strings=None) -> None:
|
|
|
+def main(argv=None) -> int:
|
|
|
parser = argparse.ArgumentParser(description="taipy.gui.extensions entry point.")
|
|
|
sub_parser = parser.add_subparsers(dest="command", help="Commands to run", required=True)
|
|
|
|
|
@@ -126,13 +164,22 @@ def main(arg_strings=None) -> None:
|
|
|
tgb_generation.add_argument(
|
|
|
dest="package_root_dir",
|
|
|
nargs=1,
|
|
|
- help="The root dir of the extension package." + " This directory must contain a __init__.py file.",
|
|
|
+ help=textwrap.dedent("""\
|
|
|
+ The root dir of the extension package.
|
|
|
+ This directory must contain a __init__.py file."""),
|
|
|
+ )
|
|
|
+ tgb_generation.add_argument(
|
|
|
+ dest="output_path",
|
|
|
+ nargs="?",
|
|
|
+ help=textwrap.dedent("""\
|
|
|
+ The output path for the Python Interface Definition file.
|
|
|
+ The default is a file called '__init__.pyi' in the module's root directory."""),
|
|
|
)
|
|
|
tgb_generation.set_defaults(func=generate_tgb)
|
|
|
|
|
|
- args = parser.parse_args(arg_strings)
|
|
|
- args.func(args)
|
|
|
+ args = parser.parse_args(argv)
|
|
|
+ return args.func(args)
|
|
|
|
|
|
|
|
|
-if __name__ == "__main__":
|
|
|
- main()
|
|
|
+if __name__ == "__main__": # pragma: no cover
|
|
|
+ exit(main())
|