diff --git a/src/blenderbim/blenderbim/bim/module/patch/helper.py b/src/blenderbim/blenderbim/bim/module/patch/helper.py deleted file mode 100644 index 296fa167c9..0000000000 --- a/src/blenderbim/blenderbim/bim/module/patch/helper.py +++ /dev/null @@ -1,101 +0,0 @@ - -# BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult -# -# This file is part of BlenderBIM Add-on. -# -# BlenderBIM Add-on is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# BlenderBIM Add-on is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with BlenderBIM Add-on. If not, see . - -import typing -import inspect -import collections -import importlib -from types import ModuleType - -def extract_docs( - module: ModuleType, - submodule_name: str, - cls_name: str, - method_name: str, - boilerplate_args : typing.Iterable[str]=None): - """Extract class docstrings and method arguments - - :param module: Parent module from which to extract the submodule class - :param submodule_name: Submodule from which to extract the class - :param cls_name: Class from which to extract the docstring and method arguments - :param method_name: Class Method name from which to extract arguments - :param boilerplate_args: String iterable containing arguments that shall not be parsed - """ - spec = importlib.util.spec_from_file_location(submodule_name, f"{module.__path__[0]}/recipes/{submodule_name}.py") - submodule = importlib.util.module_from_spec(spec) - try: - spec.loader.exec_module(submodule) - try: - return _extract_docs(getattr(submodule, cls_name), method_name, boilerplate_args) - except AttributeError as e: - print(e) - except ModuleNotFoundError as e: - print(f"Error : IFCPatch {str(submodule)} could not complete because : {str(e)}") - except: - print(f"Error : IFCPatch {str(submodule)} could not load") - -def _extract_docs(cls, method_name, boilerplate_args): - inputs = collections.OrderedDict() - method = getattr(cls, method_name) - node_data = {"class": cls} - - signature = inspect.signature(method) - for name, parameter in signature.parameters.items(): - if name == "self": - continue - inputs[name] = {"name": name} - if isinstance(parameter.default, (str, float, int, bool)): - inputs[name]["default"] = parameter.default - - type_hints = typing.get_type_hints(method) - for name, socket_data in inputs.items(): - type_hint = type_hints.get(name, None) - if type_hint is None: # The argument is not type-hinted. (Or hinted to None ??) - continue - if isinstance(type_hint, typing._UnionGenericAlias): - inputs[name]["type"] = [t.__name__ for t in typing.get_args(type_hint)] - else: - inputs[name]["type"] = type_hint.__name__ - - description = "" - doc = method.__doc__ - if doc is not None: - for i, line in enumerate(doc.split("\n")): - line = line.strip() - if i == 0: - node_data["name"] = line - elif line.startswith(":return:"): - node_data["output"] = {"name": line.split(":")[2].strip(), "description": line.split(":")[3].strip()} - elif line.startswith(":param"): - param_name = line.split(":")[1].strip().replace("param ", "") - if param_name in inputs: - inputs[param_name]["description"] = line.split(":")[2].strip() - elif i == 2: - description += line - elif i > 2: - description += "\n" + line - - node_data["description"] = description.strip() - node_data["inputs"] = inputs - - if boilerplate_args is not None: - for arg in boilerplate_args: # Remove boilerplate arguments - node_data["inputs"].pop(arg, None) - return node_data - diff --git a/src/blenderbim/blenderbim/bim/module/patch/operator.py b/src/blenderbim/blenderbim/bim/module/patch/operator.py index 518bd381b2..ea76cd51c3 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/operator.py +++ b/src/blenderbim/blenderbim/bim/module/patch/operator.py @@ -26,8 +26,6 @@ try: except: print("IfcPatch not available") -from .helper import extract_docs - class SelectIfcPatchInput(bpy.types.Operator): bl_idname = "bim.select_ifc_patch_input" @@ -102,7 +100,7 @@ class UpdateIfcPatchArguments(bpy.types.Operator): return {"FINISHED"} patch_args = context.scene.BIMPatchProperties.ifc_patch_args_attr patch_args.clear() - docs = extract_docs(ifcpatch, self.recipe, "Patcher", "__init__", ("src", "file", "logger", "args")) + docs = ifcpatch.extract_docs(self.recipe, "Patcher", "__init__", ("src", "file", "logger", "args")) if docs and "inputs" in docs: inputs = docs["inputs"] for arg_name in inputs: diff --git a/src/blenderbim/blenderbim/bim/module/patch/prop.py b/src/blenderbim/blenderbim/bim/module/patch/prop.py index eb34c1f65d..ac9e09faa9 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/prop.py +++ b/src/blenderbim/blenderbim/bim/module/patch/prop.py @@ -33,8 +33,6 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -from .helper import extract_docs -from .operator import UpdateIfcPatchArguments ifcpatchrecipes_enum = [] @@ -54,7 +52,7 @@ def getIfcPatchRecipes(self, context): f = str(filename.stem) if f == "__init__": continue - docs = extract_docs(ifcpatch, f, "Patcher", "__init__", ("src", "file", "logger", "args")) + docs = ifcpatch.extract_docs(f, "Patcher", "__init__", ("src", "file", "logger", "args")) ifcpatchrecipes_enum.append((f, f, docs.get("description","") if docs else "")) return ifcpatchrecipes_enum diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index 6c2b60a369..79b9027592 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -22,6 +22,11 @@ import ifcopenshell import logging +import os +import typing +import inspect +import collections +import importlib def execute(args, is_library=None): @@ -51,3 +56,78 @@ def execute(args, is_library=None): else: ifc_file.write(args["output"]) print("# All tasks are complete :-)") + + +def extract_docs( + submodule_name: str, + cls_name: str, + method_name: str="__init__", + boilerplate_args : typing.Iterable[str]=None): + """Extract class docstrings and method arguments + + :param module: Parent module from which to extract the submodule class + :param submodule_name: Submodule from which to extract the class + :param cls_name: Class from which to extract the docstring and method arguments + :param method_name: Class Method name from which to extract arguments + :param boilerplate_args: String iterable containing arguments that shall not be parsed + """ + spec = importlib.util.spec_from_file_location( + submodule_name, + f"{os.path.dirname(inspect.getabsfile(inspect.currentframe()))}/recipes/{submodule_name}.py") + submodule = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(submodule) + try: + return _extract_docs(getattr(submodule, cls_name), method_name, boilerplate_args) + except AttributeError as e: + print(e) + except ModuleNotFoundError as e: + print(f"Error : IFCPatch {str(submodule)} could not load because : {str(e)}") + + +def _extract_docs(cls, method_name, boilerplate_args): + inputs = collections.OrderedDict() + method = getattr(cls, method_name) + docs = {"class": cls} + if boilerplate_args is None: + boilerplate_args = [] + + signature = inspect.signature(method) + for name, parameter in signature.parameters.items(): + if name == "self" or name in boilerplate_args: + continue + inputs[name] = {"name": name} + if isinstance(parameter.default, (str, float, int, bool)): + inputs[name]["default"] = parameter.default + + type_hints = typing.get_type_hints(method) + for input_name in inputs.keys(): + type_hint = type_hints.get(input_name, None) + if type_hint is None: # The argument is not type-hinted. (Or hinted to None ??) + continue + if isinstance(type_hint, typing._UnionGenericAlias): + inputs[input_name]["type"] = [t.__name__ for t in typing.get_args(type_hint)] + else: + inputs[input_name]["type"] = type_hint.__name__ + + description = "" + doc = method.__doc__ + if doc is not None: + for i, line in enumerate(doc.split("\n")): + line = line.strip() + if i == 0: + docs["name"] = line + elif line.startswith(":return:"): + docs["output"] = {"name": line.split(":")[2].strip(), "description": line.split(":")[3].strip()} + elif line.startswith(":param"): + param_name = line.split(":")[1].strip().replace("param ", "") + if param_name in inputs: + inputs[param_name]["description"] = line.split(":")[2].strip() + elif i == 2: + description += line + elif i > 2: + description += "\n" + line + + docs["description"] = description.strip() + docs["inputs"] = inputs + return docs