From 20e59c322d5cab1785090c0b9095dd265bf653ec Mon Sep 17 00:00:00 2001 From: OpenVMP Date: Sun, 18 Aug 2024 19:14:55 -0700 Subject: [PATCH] Render packages recursively (#175) --- partcad-cli/src/partcad_cli/cli_render.py | 90 ++++++++++++++--------- partcad/src/partcad/context.py | 60 ++++++++++----- partcad/src/partcad/project.py | 87 +++++++++++++++++++++- partcad/src/partcad/shape.py | 11 ++- partcad/src/partcad/utils.py | 2 + 5 files changed, 190 insertions(+), 60 deletions(-) diff --git a/partcad-cli/src/partcad_cli/cli_render.py b/partcad-cli/src/partcad_cli/cli_render.py index 6518864..4300c90 100644 --- a/partcad-cli/src/partcad_cli/cli_render.py +++ b/partcad-cli/src/partcad_cli/cli_render.py @@ -58,6 +58,12 @@ def cli_help_render(subparsers: argparse.ArgumentParser): dest="package", default="", ) + parser_render.add_argument( + "-r", + help="Recursively render all imported packages", + dest="recursive", + action="store_true", + ) group_type = parser_render.add_mutually_exclusive_group(required=False) group_type.add_argument( @@ -96,43 +102,57 @@ def cli_help_render(subparsers: argparse.ArgumentParser): def cli_render(args, ctx): ctx.option_create_dirs = args.create_dirs - if args.package is None: - args.package = "" - if not args.object is None: - if not ":" in args.object: - args.object = ":" + args.object - args.package, args.object = pc_utils.resolve_resource_path( - ctx.get_current_project_path(), args.object + package = args.package if args.package is not None else "" + if args.recursive: + start_package = pc_utils.get_child_project_path( + ctx.get_current_project_path(), package ) - - if args.object is None: - # Render all parts and assemblies configured to be auto-rendered in this project - ctx.render( - project_path=args.package, - format=args.format, - output_dir=args.output_dir, + all_packages = ctx.get_all_packages(start_package) + packages = list( + map( + lambda p: p["name"], + list(all_packages), + ) ) else: - # Render the requested part or assembly - sketches = [] - interfaces = [] - parts = [] - assemblies = [] - if args.sketch: - sketches.append(args.object) - elif args.interface: - interfaces.append(args.object) - elif args.assembly: - assemblies.append(args.object) + packages = [package] + + for package in packages: + if not args.object is None: + if not ":" in args.object: + args.object = ":" + args.object + args.package, args.object = pc_utils.resolve_resource_path( + ctx.get_current_project_path(), args.object + ) + + if args.object is None: + # Render all parts and assemblies configured to be auto-rendered in this project + ctx.render( + project_path=package, + format=args.format, + output_dir=args.output_dir, + ) else: - parts.append(args.object) + # Render the requested part or assembly + sketches = [] + interfaces = [] + parts = [] + assemblies = [] + if args.sketch: + sketches.append(args.object) + elif args.interface: + interfaces.append(args.object) + elif args.assembly: + assemblies.append(args.object) + else: + parts.append(args.object) - prj = ctx.get_project(args.package) - prj.render( - sketches=sketches, - interfaces=interfaces, - parts=parts, - assemblies=assemblies, - format=args.format, - output_dir=args.output_dir, - ) + prj = ctx.get_project(package) + prj.render( + sketches=sketches, + interfaces=interfaces, + parts=parts, + assemblies=assemblies, + format=args.format, + output_dir=args.output_dir, + ) diff --git a/partcad/src/partcad/context.py b/partcad/src/partcad/context.py index d366148..f14dc93 100644 --- a/partcad/src/partcad/context.py +++ b/partcad/src/partcad/context.py @@ -241,10 +241,13 @@ def get_project(self, rel_project_path: str): # assume that the root package has an 'onlyInRoot' dependency # present to facilitate such a reference in a standalone # development environment. - project_path = self.name + project_path # Strip the first '/' (absolute path always starts with a '/'``) - len_to_skip = len(self.name) + 1 if self.name != "/" else 1 + if self.name != "/" and project_path.startswith(self.name): + # The root package is not '/, need to skip the root package name + len_to_skip = len(self.name) + 1 + else: + len_to_skip = 1 project_path = project_path[len_to_skip:] project = self.projects[self.name] @@ -292,7 +295,7 @@ def _get_project_recursive(self, project, import_list: list[str]): ) ): pc_logging.debug( - "Importing a subfolder: %s..." % next_project_path + "Importing a subfolder (get): %s..." % next_project_path ) prj_conf = { "name": next_project_path, @@ -320,11 +323,16 @@ def _get_project_recursive(self, project, import_list: list[str]): ) imports = list(filtered) for prj_name in imports: - pc_logging.debug("Checking the import: %s..." % prj_name) + pc_logging.debug( + "Checking the import: %s vs %s..." + % (prj_name, next_import) + ) if prj_name != next_import: continue - pc_logging.debug("Importing: %s..." % next_project_path) prj_conf = project.config_obj["import"][prj_name] + if prj_conf.get("onlyInRoot", False): + next_project_path = "/" + prj_name + pc_logging.debug("Importing: %s..." % next_project_path) if "name" in prj_conf: prj_conf["orig_name"] = prj_conf["name"] prj_conf["name"] = next_project_path @@ -338,8 +346,10 @@ def _get_project_recursive(self, project, import_list: list[str]): return next_project - def import_all(self): - asyncio.run(self._import_all_wrapper(self.projects[self.name])) + def import_all(self, parent_name=None): + if parent_name is None: + parent_name = self.name + asyncio.run(self._import_all_wrapper(self.projects[parent_name])) async def _import_all_wrapper(self, project): iterate_tasks = [] @@ -394,13 +404,18 @@ async def _import_all_recursive(self, project): imports, ) imports = list(filtered) + for prj_name in imports: - next_project_path = get_child_project_path( - project.name, prj_name - ) + prj_conf = project.config_obj["import"][prj_name] + if prj_conf.get("onlyInRoot", False): + next_project_path = "/" + prj_name + else: + next_project_path = get_child_project_path( + project.name, prj_name + ) pc_logging.debug("Importing: %s..." % next_project_path) - prj_conf = project.config_obj["import"][prj_name] + if "name" in prj_conf: prj_conf["orig_name"] = prj_conf["name"] prj_conf["name"] = next_project_path @@ -423,9 +438,11 @@ async def _import_all_recursive(self, project): consts.DEFAULT_PACKAGE_CONFIG, ) ): + # TODO(clairbee): check if this subdir is already imported next_project_path = get_child_project_path(project.name, subdir) pc_logging.debug( - "Importing a subfolder: %s..." % next_project_path + "Importing a subfolder (import all): %s..." + % next_project_path ) prj_conf = { "name": next_project_path, @@ -441,18 +458,25 @@ async def _import_all_recursive(self, project): return tasks - def get_all_packages(self): + def get_all_packages(self, parent_name=None): # TODO(clairbee): leverage root_project.get_child_project_names() - self.import_all() - return self.get_packages() - - def get_packages(self): + self.import_all(parent_name) + return self.get_packages(parent_name) + + def get_packages(self, parent_name=None): + projects = self.projects.values() + if parent_name is not None: + projects = filter( + lambda x: x.name.startswith(parent_name), projects + ) return map( lambda pkg: {"name": pkg.name, "desc": pkg.desc}, filter( + # FIXME(clairbee): parameterize interfaces before displaying them + # lambda x: len(x.interfaces) + len(x.sketches) + len(x.parts) + len(x.assemblies) lambda x: len(x.sketches) + len(x.parts) + len(x.assemblies) > 0, - self.projects.values(), + projects, ), ) diff --git a/partcad/src/partcad/project.py b/partcad/src/partcad/project.py index fe20ae4..df617cb 100644 --- a/partcad/src/partcad/project.py +++ b/partcad/src/partcad/project.py @@ -1223,7 +1223,7 @@ async def render_async( # Render tasks = [] for shape in shapes: - shape_render = render + shape_render = copy.copy(render) if ( "render" in shape.config and not shape.config["render"] is None @@ -1406,6 +1406,8 @@ def render_readme_async(self, render_cfg, output_dir): cfg = {} path = os.path.join(output_dir, cfg.get("path", "README.md")) + dir_path = os.path.dirname(path) + return_path = os.path.relpath(output_dir, dir_path) exclude = cfg.get("exclude", []) if exclude is None: @@ -1436,10 +1438,70 @@ def render_readme_async(self, render_cfg, output_dir): lines += [usage] lines += [""] + if ( + self.config_obj.get("import", None) is not None + and not "packages" in exclude + ): + imports = self.config_obj["import"] + display_imports = [] + for alias in imports: + if imports[alias].get("onlyInRoot", False): + continue + display_imports.append(alias) + + if display_imports: + lines += ["## Sub-Packages"] + lines += [""] + for alias in display_imports: + import_config = imports[alias] + columns = [] + + if ( + "type" not in import_config + or import_config["type"] == "local" + ): + lines += [ + "### [%s](./%s)" + % ( + import_config["name"], + os.path.join( + return_path, + import_config["path"], + "README.md", + ), + ) + ] + elif import_config["type"] == "git": + lines += [ + "### [%s](%s)" + % (import_config["name"], import_config["url"]) + ] + else: + lines += ["### %s" % import_config["name"]] + + if "desc" in import_config: + columns += [import_config["desc"]] + elif not columns: + columns += ["***Not documented yet.***"] + + if len(columns) > 1: + lines += [""] + lines += map( + lambda c: "", columns + ) + lines += ["
" + c + "
"] + else: + lines += columns + lines += [""] + def add_section(name, shape, render_cfg): config = shape.config - if "type" in config and config["type"] == "alias": + if ( + "type" in config + and config["type"] == "alias" + and "aliases" in exclude + ): return [] columns = [] @@ -1447,8 +1509,15 @@ def add_section(name, shape, render_cfg): "type" in config and config["type"] == "svg" ): svg_cfg = render_cfg["svg"] if "svg" in render_cfg else {} + if isinstance(svg_cfg, str): + svg_cfg = {"prefix": svg_cfg} svg_cfg = svg_cfg if svg_cfg is not None else {} image_path = os.path.join( + return_path, + svg_cfg.get("prefix", "."), + name + ".svg", + ) + test_image_path = os.path.join( svg_cfg.get("prefix", "."), name + ".svg", ) @@ -1459,6 +1528,11 @@ def add_section(name, shape, render_cfg): png_cfg = render_cfg["png"] png_cfg = png_cfg if png_cfg is not None else {} image_path = os.path.join( + return_path, + png_cfg.get("prefix", "."), + name + ".png", + ) + test_image_path = os.path.join( png_cfg.get("prefix", "."), name + ".png", ) @@ -1466,9 +1540,12 @@ def add_section(name, shape, render_cfg): else: image_path = None - if image_path is None or not os.path.exists(image_path): + if image_path is None or not os.path.exists( + os.path.join(output_dir, test_image_path) + ): pc_logging.warn( - "Skipping rendering of %s: no image found" % name + "Skipping rendering of %s: no image found at %s" + % (name, test_image_path) ) return [] @@ -1540,6 +1617,8 @@ def add_section(name, shape, render_cfg): lines += add_section(name, shape, render_cfg) lines += [ + "

", + "", "*Generated by [PartCAD](https://partcad.org/)*", ] diff --git a/partcad/src/partcad/shape.py b/partcad/src/partcad/shape.py index 3276183..208f7b4 100644 --- a/partcad/src/partcad/shape.py +++ b/partcad/src/partcad/shape.py @@ -11,6 +11,7 @@ import asyncio import base64 +import copy import os import pickle import shutil @@ -268,7 +269,7 @@ def render_getopts( filepath=None, ): if not project is None and "render" in project.config_obj: - render_opts = project.config_obj["render"] + render_opts = copy.copy(project.config_obj["render"]) else: render_opts = {} @@ -276,7 +277,7 @@ def render_getopts( if isinstance(render_opts[kind], str): opts = {"prefix": render_opts[kind]} else: - opts = render_opts[kind] + opts = copy.copy(render_opts[kind]) else: opts = {} @@ -286,7 +287,7 @@ def render_getopts( and kind in self.config["render"] and not self.config["render"][kind] is None ): - shape_opts = self.config["render"][kind] + shape_opts = copy.copy(self.config["render"][kind]) if isinstance(shape_opts, str): shape_opts = {"prefix": shape_opts} opts = render_cfg_merge(opts, shape_opts) @@ -305,6 +306,10 @@ def render_getopts( # the generic section of rendering options in the config. if not os.path.isabs(filepath): if "output_dir" in render_opts: + # TODO(clairbee): consider using project.config_dir + # filepath = os.path.join( + # project.config_dir, render_opts["output_dir"], filepath + # ) filepath = os.path.join(render_opts["output_dir"], filepath) elif not project is None: filepath = os.path.join(project.config_dir, filepath) diff --git a/partcad/src/partcad/utils.py b/partcad/src/partcad/utils.py index 3bb658a..7f74770 100644 --- a/partcad/src/partcad/utils.py +++ b/partcad/src/partcad/utils.py @@ -27,6 +27,8 @@ def get_child_project_path(parent_path, child_name): result = parent_path + "/" + child_name result = re.sub(r"/[^/]*/\.\.", "", result) + if result != "/": + result = re.sub(r"/$", "", result) return result