From ae61ab2fa38c6ae54581cfb885803934f0dc5479 Mon Sep 17 00:00:00 2001 From: Nicolai Dagestad Date: Tue, 20 Jan 2026 12:20:16 +0100 Subject: [PATCH 01/11] Make generic module retrieval functions for projects/data_models/targets --- src/fuddly/libs/fmk_services.py | 66 +++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/src/fuddly/libs/fmk_services.py b/src/fuddly/libs/fmk_services.py index f31911e8..f79f0de7 100644 --- a/src/fuddly/libs/fmk_services.py +++ b/src/fuddly/libs/fmk_services.py @@ -6,25 +6,68 @@ import fuddly.framework.global_resources as gr from fuddly.framework.plumbing import _populate_projects as populate_projects -def get_each_project_module() -> []: - - project_modules = [] +# Get all modules (from FS and entry_points) +def get_module_of_type(group_name: str, prefix: str): + path = { + "targets": gr.user_targets_folder, + "projects": gr.user_projects_folder, + "data_models": gr.user_data_models_folder, + }[group_name] + modules = [] # Project from user (FS) - projects = populate_projects(gr.user_projects_folder, prefix="fuddly/projects", projects=None) - for dname, (_, file_list) in projects.items(): + modules.extend(find_modules_in_dir(path=path, prefix=prefix)) + # Projects from modules + modules.extend(find_modules_from_ep_group(group_name=gr.ep_group_names[group_name])) + return modules + + +def get_each_project_module() -> list(): + return get_module_of_type( + group_name="projects", + prefix="fuddly/projects" + ) + + +def get_each_data_model_module() -> list(): + return get_module_of_type( + group_name="data_models", + prefix="fuddly/data_models" + ) + + +def get_each_target_module() -> list(): + return get_module_of_type( + group_name="targets", + prefix="fuddly/data_models" + ) + + +# Find python modules in a specific path, prepend "prefix" to the modules' names +def find_modules_in_dir(path: str, prefix: str) -> list(): + res = [] + fullpath = os.path.join(gr.fuddly_data_folder, path) + # TODO this is the project specific detector which checks if it's a python + # module. The data model import should be using the same behavior but has + # not been converted yet + modules = populate_projects(fullpath, prefix=prefix) + for dname, (_, file_list) in modules.items(): prefix = dname.replace(os.sep, ".") + "." for name in file_list: m = find_spec(prefix+name) # This should never happen if m is None or m.origin is None: - print(f"{prefix+name} detected as a module in {gr.fuddly_data_folder}/user_projects," + print(f"{prefix+name} detected as a module in "f"{fullpath}," " but could not be imported") continue - project_modules.append(m) + res.append(m) + return res - # Projects from modules - for ep in entry_points(group=gr.ep_group_names["projects"]): + +# Get all the python modules corresponding to a certain entry_point name group +def find_modules_from_ep_group(group_name: str) -> list(): + res = [] + for ep in entry_points(group=group_name): if ep.name.endswith("__root__"): continue m = find_spec(ep.module) @@ -34,9 +77,8 @@ def get_each_project_module() -> []: # the entry point is not a module, let's just ignore it print(f"*** {ep.module} is not a python module, check your installed modules ***") continue - project_modules.append(m) - - return project_modules + res.append(m) + return res def get_project_from_name(name): From 2b12306698a31132bf6d0f440b8d4c1eb642e9b0 Mon Sep 17 00:00:00 2001 From: Nicolai Dagestad Date: Tue, 20 Jan 2026 16:16:59 +0100 Subject: [PATCH 02/11] Regroup stuff from various cli/*.py files in cli/utils.py --- src/fuddly/cli/__init__.py | 20 +++--- src/fuddly/cli/run.py | 31 +--------- src/fuddly/cli/show.py | 39 ++---------- src/fuddly/cli/tool.py | 20 +++--- src/fuddly/cli/utils.py | 122 +++++++++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 85 deletions(-) create mode 100644 src/fuddly/cli/utils.py diff --git a/src/fuddly/cli/__init__.py b/src/fuddly/cli/__init__.py index b26d2822..0fe5b726 100644 --- a/src/fuddly/cli/__init__.py +++ b/src/fuddly/cli/__init__.py @@ -22,11 +22,19 @@ # ################################################################################ -import sys +import argcomplete import fuddly.cli.argparse_wrapper as argparse import importlib +import sys +from typing import List + +# TODO script_argument_completer will be used once a sub-script argument completion logic is developped +from fuddly.cli.run import script_argument_completer +# TODO tool_argument_completer will be used once a sub-script argument completion logic is developped +from fuddly.cli.tool import tool_argument_completer +from fuddly.cli.utils import get_projects, get_tools, get_scripts, get_all_objects +from fuddly.cli.error import CliException -import argcomplete # Import magic # import fuddly.{obj_type} will find targets, data_models, projects or info # automagically wethere they are define in an entry point, as part of fuddly's @@ -34,14 +42,6 @@ from fuddly.libs.importer import fuddly_importer_hook fuddly_importer_hook.setup() -# TODO script_argument_completer will be used once a sub-script argument completion logic is developped -from .run import get_scripts, script_argument_completer -# TODO tool_argument_completer will be used once a sub-script argument completion logic is developped -from .tool import get_tools, tool_argument_completer -from .show import get_projects - -from typing import List -from fuddly.cli.error import CliException def main(argv: List[str] = None): # This is done so you can call it from python shell if you want to diff --git a/src/fuddly/cli/run.py b/src/fuddly/cli/run.py index 0cbfdac1..c3f98dba 100644 --- a/src/fuddly/cli/run.py +++ b/src/fuddly/cli/run.py @@ -2,40 +2,13 @@ from fuddly.cli.error import CliException from importlib.util import find_spec from importlib.metadata import entry_points -import fuddly.framework.global_resources as gr import sys import os.path import os import argcomplete -from fuddly.libs.fmk_services import get_each_project_module - -def get_scripts() -> []: - # The function is called for when the CLI is called and if we use the list option - # having paths be an attribute to the functions means we will not run it twice - if get_scripts.paths is not None: - return get_scripts.paths - else: - get_scripts.paths = [] - - project_modules = get_each_project_module() - - for m in project_modules: - p = m.origin - if os.path.basename(p) == "__init__.py": - p = os.path.dirname(p) - else: - # Ignoring old single-files projects - continue - if os.path.isdir(os.path.join(p, "scripts")): - for f in next(os.walk(os.path.join(p, "scripts")))[2]: - if f.endswith(".py") and f != "__init__.py": - get_scripts.paths.append(m.name + ".scripts." + f.removesuffix(".py")) - - return get_scripts.paths - - -get_scripts.paths = None +import fuddly.framework.global_resources as gr +from fuddly.cli.utils import get_scripts def script_from_pkg_name(name) -> str: diff --git a/src/fuddly/cli/show.py b/src/fuddly/cli/show.py index b1ebfced..c6f51a17 100644 --- a/src/fuddly/cli/show.py +++ b/src/fuddly/cli/show.py @@ -1,38 +1,9 @@ -import fuddly.cli.argparse_wrapper as argparse -from fuddly.cli.error import CliException -from importlib.util import find_spec -from importlib.metadata import entry_points - -import sys import os -import argcomplete import importlib -from fuddly.libs.fmk_services import get_each_project_module - -def get_projects() -> []: - - if get_projects.modules is not None: - return get_projects.modules - else: - get_projects.modules = [] - - project_modules = get_each_project_module() - - for m in project_modules: - path = m.origin - if os.path.basename(path) == "__init__.py": - path = os.path.dirname(path) - else: - # Ignoring old single-files projects - continue - *prefix, prj_name = path.split("/") - get_projects.modules.append((prj_name, path, m)) - - - return get_projects.modules - -get_projects.modules = None +import fuddly.cli.argparse_wrapper as argparse +from fuddly.cli.error import CliException +from fuddly.cli.utils import get_projects def info_from_project_name(name) -> str | None: @@ -49,7 +20,8 @@ def info_from_project_name(name) -> str | None: else: return None -def readme_from_project_name(name) -> (str|None, bool): + +def readme_from_project_name(name) -> (str | None, bool): for prj in get_projects(): prj_name, path, m = prj if prj_name == name: @@ -83,4 +55,3 @@ def start(args: argparse.Namespace) -> int: print(info) return 0 - diff --git a/src/fuddly/cli/tool.py b/src/fuddly/cli/tool.py index 3803b83c..8c14ce58 100644 --- a/src/fuddly/cli/tool.py +++ b/src/fuddly/cli/tool.py @@ -1,24 +1,20 @@ -import fuddly.cli.argparse_wrapper as argparse - -from fuddly.cli.error import CliException - import importlib import sys import argcomplete - -def get_tools() -> list(): - import pkgutil - tools = importlib.import_module("fuddly.tools") - return list(map(lambda x: x.name, pkgutil.walk_packages(tools.__path__))) +import fuddly.cli.argparse_wrapper as argparse +from fuddly.cli.utils import get_tools +from fuddly.cli.error import CliException def tool_argument_completer(prefix, parsed_args, **kwargs): - # Set _ARC_DEBUG in the shell fro _DEBUG to be true + # Set _ARC_DEBUG in the shell for _DEBUG to be true from argcomplete.io import _DEBUG if parsed_args.tool is not None: if _DEBUG: - argcomplete.warn(f"Prefix: {prefix}\nparsed_args: {parsed_args}\nkwargs: {kwargs}") + argcomplete.warn(f"Prefix: {prefix}\n" + f"parsed_args: {parsed_args}\n" + f"kwargs: {kwargs}") # TODO For now we always return [], it should eventually be the tool's arguments return [] else: @@ -40,4 +36,4 @@ def start(args: argparse.Namespace) -> int: print(f"{args.tool} is not a valid fuddly tool") return 1 mod = pkg.loader.load_module() - return mod.main() \ No newline at end of file + return mod.main() diff --git a/src/fuddly/cli/utils.py b/src/fuddly/cli/utils.py new file mode 100644 index 00000000..6fed4237 --- /dev/null +++ b/src/fuddly/cli/utils.py @@ -0,0 +1,122 @@ +import os + +from fuddly.libs.fmk_services import ( + get_each_project_module, + get_each_data_model_module, + get_each_target_module, +) +from importlib import import_module + + +# Return the type a module is of +def get_module_type(name: str) -> str: + if name in [x.name for x in get_each_project_module()]: + return "projects" + if name in [x.name for x in get_each_data_model_module()]: + return "data_models" + if name in [x.name for x in get_each_target_module()]: + return "targets" + return None + + +# These functions uses a trick to have a variable with cached info +# On first run, a member of the object the function (Everything is an object +# in python) is populated with a list of values so that it can be reused if +# the function is called multiple times + +# Return a list of all projects, dms, and targets fuddly knows about +def get_all_objects() -> list(): + if get_all_objects.modules is not None: + return get_all_objects.modules + else: + get_all_objects.modules = [] + + # Projects + project_modules = get_each_project_module() + # Data models + data_model_modules = get_each_data_model_module() + # Targets + target_modules = get_each_target_module() + # Infos + + for m in project_modules + data_model_modules + target_modules: + path = m.origin + if os.path.basename(path) == "__init__.py": + path = os.path.dirname(path) + else: + # Ignoring old single-files projects + continue + get_all_objects.modules.append(m.name) + + return get_all_objects.modules + + +get_all_objects.modules = None + + +# Return a list of scripts from all the projects fuddly knows about +def get_scripts() -> list(): + if get_scripts.paths is not None: + return get_scripts.paths + else: + get_scripts.paths = [] + + project_modules = get_each_project_module() + + for m in project_modules: + p = m.origin + if os.path.basename(p) == "__init__.py": + p = os.path.dirname(p) + else: + # Ignoring old single-files projects + continue + if os.path.isdir(os.path.join(p, "scripts")): + for f in next(os.walk(os.path.join(p, "scripts")))[2]: + if f.endswith(".py") and f != "__init__.py": + get_scripts.paths.append(m.name + ".scripts." + f.removesuffix(".py")) + + return get_scripts.paths + + +get_scripts.paths = None + + +# Return a list of projects fuddly knows about +def get_projects() -> list(): + if get_projects.modules is not None: + return get_projects.modules + else: + get_projects.modules = [] + + project_modules = get_each_project_module() + + for m in project_modules: + path = m.origin + if os.path.basename(path) == "__init__.py": + path = os.path.dirname(path) + else: + # Ignoring old single-files projects + continue + *prefix, prj_name = path.split("/") + get_projects.modules.append((prj_name, path, m)) + + return get_projects.modules + + +get_projects.modules = None + + +# Return a list of all the fuddly tools +def get_tools() -> list(): + import pkgutil + + if get_tools.modules is not None: + return get_tools.modules + else: + get_tools.modules = [] + tools = import_module("fuddly.tools") + get_tools.modules = list(map(lambda x: x.name, pkgutil.walk_packages(tools.__path__))) + return get_tools.modules + + +get_tools.modules = None From 071756e060ab4dcfa725736651b79b37b61dedbe Mon Sep 17 00:00:00 2001 From: Nicolai Dagestad Date: Thu, 22 Jan 2026 13:35:24 +0100 Subject: [PATCH 03/11] Replace printf by sys.stderr.write in fm_services --- src/fuddly/libs/fmk_services.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/fuddly/libs/fmk_services.py b/src/fuddly/libs/fmk_services.py index f79f0de7..5fd62530 100644 --- a/src/fuddly/libs/fmk_services.py +++ b/src/fuddly/libs/fmk_services.py @@ -1,4 +1,5 @@ import os +import sys import importlib from importlib.util import find_spec from importlib.metadata import entry_points @@ -57,8 +58,8 @@ def find_modules_in_dir(path: str, prefix: str) -> list(): m = find_spec(prefix+name) # This should never happen if m is None or m.origin is None: - print(f"{prefix+name} detected as a module in "f"{fullpath}," - " but could not be imported") + sys.stderr.write(f"{prefix+name} detected as a module in "f"{fullpath}," + " but could not be imported\n") continue res.append(m) return res @@ -75,7 +76,7 @@ def find_modules_from_ep_group(group_name: str) -> list(): # i.e. somebody broke their package if m is None or m.origin is None: # the entry point is not a module, let's just ignore it - print(f"*** {ep.module} is not a python module, check your installed modules ***") + sys.stderr.write(f"*** {ep.module} is not a python module, check your installed modules ***\n") continue res.append(m) return res @@ -90,8 +91,8 @@ def get_project_from_name(name): try: prj_obj = mod.project except AttributeError: - print(f'[ERROR] the project "{name}" does not contain a global variable ' - f'named "project"') + sys.stderr.write(f'[ERROR] the project "{name}" does not contain a global variable ' + f'named "project"\n') return None else: if os.path.basename(m.origin) == "__init__.py": From 18eb3a4e0afd27848a4267188209e04f43c3ad91 Mon Sep 17 00:00:00 2001 From: Nicolai Dagestad Date: Thu, 22 Jan 2026 13:35:46 +0100 Subject: [PATCH 04/11] Create a _populate_datamodels function analogous to _populate_projects --- src/fuddly/framework/plumbing.py | 84 +++++++++++++++++--------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/src/fuddly/framework/plumbing.py b/src/fuddly/framework/plumbing.py index 1766efbb..06f48fda 100644 --- a/src/fuddly/framework/plumbing.py +++ b/src/fuddly/framework/plumbing.py @@ -38,6 +38,9 @@ import time import signal +from pathlib import Path +from os.path import dirname, basename + from functools import wraps, partial from tabnanny import verbose from typing import Sequence @@ -262,12 +265,12 @@ def _populate_projects(search_path, prefix="", projects=None): rel_path = path.removeprefix(search_path).removeprefix(os.sep) if "__init__.py" in files: # normapth make sure the path does not end in a '/' - key = os.path.normpath(os.path.join(prefix, os.path.dirname(rel_path))) - basename = os.path.basename(path.removeprefix(search_path).removeprefix(os.sep)) - if basename != "": + key = os.path.normpath(os.path.join(prefix, dirname(rel_path))) + basename_ = basename(path.removeprefix(search_path).removeprefix(os.sep)) + if basename_ != "": if projects.get(key) is None: - projects[key] = (path.removesuffix(basename), []) - projects[key][1].append(basename) + projects[key] = (path.removesuffix(basename_), []) + projects[key][1].append(basename_) dirs.clear() continue if "__pycache__" in dirs: @@ -288,6 +291,38 @@ def _populate_projects(search_path, prefix="", projects=None): return projects + +def _populate_data_models(searchpath, prefix="", data_models=None): + + if data_models is None: + data_models = collections.OrderedDict() + + searchpath = os.path.normpath(searchpath) + + if searchpath[-1] == '/': + searchpath = searchpath[:-1] + if prefix != "" and prefix[-1] != '/': + prefix = prefix + '/' + + base_path = dirname(searchpath) + _, dirs, _ = next(os.walk(searchpath)) + for dirpath in dirs: + p = Path(os.path.join(searchpath, dirpath)) + # We only load modules that have a __init__.py, dm.py and strategy.py + inits = [dirname(x) for x in p.glob('**/__init__.py')] + dms = [dirname(x) for x in p.glob('**/dm.py')] + strats = [dirname(x) for x in p.glob('**/strategy.py')] + modules = list(set(inits) & set(strats) & set(dms)) + for m in modules: + relpath = dirname(m)[len(base_path)+1:] + key = prefix+relpath + if data_models.get(key) is None: + data_models[key] = [] + # print(f'***DBG {key} {basename(m)}') + data_models[key].append(basename(m)) + + return data_models + class FmkPlumbing(object): """ Defines the methods to operate every sub-systems of fuddly @@ -829,38 +864,11 @@ def _get_data_models_from_fs(self, fmkDB_update=True): self.print(colorize(FontStyle.BOLD + "=" * 67 + "[ Data Models (filesystem) ]==", rgb=Color.FMKINFOGROUP)) data_models = collections.OrderedDict() - - def populate_data_models(path, prefix=""): - from pathlib import Path - from os.path import dirname,basename - - if path[-1] == '/': - path = path[:-1] - if prefix != "" and prefix[-1] != '/': - prefix = prefix + '/' - - base_path = dirname(path) - _, dirs, _ = next(os.walk(path)) - for dirpath in dirs: - p = Path(os.path.join(path, dirpath)) - # We only load modules that have a __init__.py, dm.py and strategy.py - inits = [dirname(x) for x in p.glob('**/__init__.py')] - dms = [dirname(x) for x in p.glob('**/dm.py')] - strats = [dirname(x) for x in p.glob('**/strategy.py')] - modules = list(set(inits) & set(strats) & set(dms)) - for m in modules: - relpath = dirname(m)[len(base_path)+1:] - key = prefix+relpath - if data_models.get(key) is None: - data_models[key] = [] - # print(f'***DBG {key} {basename(m)}') - data_models[key].append(basename(m)) - if gr.is_running_from_fs: if not self._quiet: self.print(colorize("*** Running directly from sources, loading internal data_models ***", rgb=Color.WARNING)) - populate_data_models(gr.data_models_folder, prefix="fuddly") - populate_data_models(gr.user_data_models_folder, prefix='') + _populate_data_models(gr.data_models_folder, prefix="fuddly", data_models=data_models) + _populate_data_models(gr.user_data_models_folder, prefix='', data_models=data_models) for dname, names in data_models.items(): if not self._quiet: @@ -949,8 +957,8 @@ def _import_dm(self, prefix, name, dm_path=None, reload_dm=False): if dm_path is None: m = module.__spec__ - if os.path.basename(m.origin) == "__init__.py": - dm_path = os.path.dirname(m.origin) + if basename(m.origin) == "__init__.py": + dm_path = dirname(m.origin) if dm_path is not None: dm_params["dm"].set_fs_path(dm_path) @@ -1119,8 +1127,8 @@ def _import_project(self, prefix, name, prj_path=None, reload_prj=False): if prj_path is None: m = module.__spec__ - if os.path.basename(m.origin) == "__init__.py": - prj_path = os.path.dirname(m.origin) + if basename(m.origin) == "__init__.py": + prj_path = dirname(m.origin) if prj_path is not None: prj_params["project"].set_fs_path(prj_path) From 5f6f9112258597fd8afc523e9ee58efb2157b253 Mon Sep 17 00:00:00 2001 From: Nicolai Dagestad Date: Thu, 22 Jan 2026 14:49:26 +0100 Subject: [PATCH 05/11] Add a --clone option to the cli 'new' function (and make the object-type be an option as well) --- src/fuddly/cli/__init__.py | 28 ++++++--- src/fuddly/cli/new.py | 103 ++++++++++++++++++++++++-------- src/fuddly/cli/utils.py | 14 ++--- src/fuddly/libs/fmk_services.py | 2 +- 4 files changed, 106 insertions(+), 41 deletions(-) diff --git a/src/fuddly/cli/__init__.py b/src/fuddly/cli/__init__.py index 0fe5b726..1a4e7243 100644 --- a/src/fuddly/cli/__init__.py +++ b/src/fuddly/cli/__init__.py @@ -32,7 +32,12 @@ from fuddly.cli.run import script_argument_completer # TODO tool_argument_completer will be used once a sub-script argument completion logic is developped from fuddly.cli.tool import tool_argument_completer -from fuddly.cli.utils import get_projects, get_tools, get_scripts, get_all_objects +from fuddly.cli.utils import ( + get_projects, + get_tools, + get_scripts, + get_all_object_names, +) from fuddly.cli.error import CliException # Import magic @@ -135,13 +140,20 @@ def main(argv: List[str] = None): action="store_true", help="create a python package project structure" ) - p.add_argument( - "object", - choices=["dm", "data-model", "project:bare"], - # This one has not yet been create: "project:example" - metavar="object", - help="type of object to create. [dm, data-model, project]", - ) + with p.add_mutually_exclusive_group() as g: + g.add_argument( + "--clone", + metavar="object_name", + help="name of the object to clone.", + choices=["list", *get_all_object_names()], + ) + g.add_argument( + "--type", + choices=["dm", "data-model", "project:bare"], + # This one has not yet been create: "project:example" + metavar="object", + help="type of object to create. [dm, data-model, project:bare]", + ) p.add_argument( "name", help="name to give the create object.", diff --git a/src/fuddly/cli/new.py b/src/fuddly/cli/new.py index 53239b77..345b8f23 100644 --- a/src/fuddly/cli/new.py +++ b/src/fuddly/cli/new.py @@ -1,9 +1,12 @@ -import fuddly.cli.argparse_wrapper as argparse from pathlib import Path from importlib import util from fuddly.framework import global_resources as gr import string +import os + +import fuddly.cli.argparse_wrapper as argparse from fuddly.cli.error import CliException +from fuddly.cli.utils import get_module_type, get_all_object_names conf = {} @@ -57,18 +60,31 @@ def __eq__(self, str_b): def start(args: argparse.Namespace): _conf = dict() + clone = False # TODO should the template dir be in fuddly_folder so users can define their own templates? # origin is the __init__.py file of the module so taking "parent" gives us the module folder src_dir = Path(util.find_spec("fuddly.cli").origin).parent.joinpath("templates") module_name = args.name + if args.clone is not None and args.pyproject: + print("--pyproject and --clone cannot be used together") + return 1 + dest_dir = Path(gr.fuddly_data_folder).absolute() if args.dest is not None: dest_dir = Path(args.dest).absolute() elif args.pyproject: dest_dir = Path(".").absolute() else: - if args.object.startswith("project"): + if args.clone is not None: + # This id not ideal, a better solution would be having a list command to show + # all the modules (and scripts for that matter) + if args.clone == "list": + for n in get_all_object_names(): + print(n) + return 0 + dest_dir = dest_dir / get_module_type(args.clone) + elif args.type.startswith("project"): dest_dir = dest_dir/"projects" else: dest_dir = dest_dir/"data_models" @@ -83,30 +99,56 @@ def start(args: argparse.Namespace): dest_dir = dest_dir/args.name dest_dir.mkdir(parents=True) - match PartialMatchString(args.object): - case "dm" | "data-model": - create_msg = f"Creating new data-model \"{module_name}\"" - _src_dir = src_dir/"data_model" - _conf = conf["dm"] - object_name = "data_model" - case "project:": - args.object, template = args.object.split(':') - create_msg = f"Creating new project \"{args.name}\" based on the \"{template}\" template" - _src_dir = src_dir/template - if not _src_dir.exists(): - print(f"The '{template}' project template does not exist.") - return 1 - _conf = conf["project"][template] - object_name = args.object - case _: - dest_dir.rmdir() - raise CliException(f"{args.object} is not a valide object name.") + elif args.type is not None: + match PartialMatchString(args.type): + case "dm" | "data-model": + create_msg = f"Creating new data-model \"{module_name}\"" + _src_dir = src_dir/"data_model" + _conf = conf["dm"] + object_name = "data_model" + case "project:": + args.type, template = args.type.split(':') + create_msg = f"Creating new project \"{args.name}\" based on the \"{template}\" template" + _src_dir = src_dir/template + if not _src_dir.exists(): + print(f"The '{template}' project template does not exist.") + return 1 + _conf = conf["project"][template] + object_name = args.type + case _: + dest_dir.rmdir() + raise CliException(f"{args.type} is not a valide object name.") + elif args.clone is not None: + # Retrieve the files + # Copy them to the src of the new object + create_msg = f'Copying "{args.clone}" to "{module_name}"' + _src_dir = Path(util.find_spec(args.clone).origin).parent + if not _src_dir.exists(): + print(f"The '{template}' module does not exist. Check your python install") + return 1 + clone = True + _conf = [] + for (path, dirs, files) in os.walk(_src_dir): + # Removing an elem from dirs will not go down the directory + if "__pycache__" in dirs: + dirs.remove("__pycache__") + for f in files: + p = str(path).removeprefix(str(_src_dir)).removeprefix("/") + _conf.append({ + "name": f, + "path": p, + }) + + # Find what type the object is off to default to a correct folder + object_name = "" + #module_name = args.clone if args.pyproject: _create_conf( dest_dir, src_dir/"module", conf["module"], + clone=False, name=args.name, object_name=object_name, module_name=module_name @@ -125,6 +167,7 @@ def start(args: argparse.Namespace): dest_dir, _src_dir, conf=_conf, + clone=clone, # kwargs modules_name=module_name, name=args.name, @@ -132,18 +175,28 @@ def start(args: argparse.Namespace): ) -def _create_conf(dstPath: Path, srcPath: Path, conf: dict, **kwargs): +def _create_conf(dstPath: Path, srcPath: Path, conf: dict, clone: bool, **kwargs): for e in conf: _srcPath = srcPath _dstPath = dstPath - if e.get("path") is not None: + if e.get("path") is not None and e["path"] != "": _dstPath = _dstPath/e["path"] _srcPath = _srcPath/e["path"] - _dstPath.mkdir(parents=True) + try: + _dstPath.mkdir(parents=True) + # If the directory exist, we don't care to much + except FileExistsError: + pass _srcPath = (_srcPath/e["name"]) - if ".py" == _srcPath.suffix: + # When cloning a template, the python files are suffixed with _ for VCS + # reasons, when cloning this is not the case, we copy everything as they are + if ".py" == _srcPath.suffix and not clone: _srcPath = _srcPath.with_suffix(".py_") data = _srcPath.read_text() f = _dstPath/e["name"] f.touch() - f.write_text(string.Template(data).substitute(**kwargs)) + if clone: + f.write_text(data) + else: + f.write_text(string.Template(data).substitute(**kwargs)) + diff --git a/src/fuddly/cli/utils.py b/src/fuddly/cli/utils.py index 6fed4237..b2495770 100644 --- a/src/fuddly/cli/utils.py +++ b/src/fuddly/cli/utils.py @@ -25,11 +25,11 @@ def get_module_type(name: str) -> str: # the function is called multiple times # Return a list of all projects, dms, and targets fuddly knows about -def get_all_objects() -> list(): - if get_all_objects.modules is not None: - return get_all_objects.modules +def get_all_object_names() -> list(): + if get_all_object_names.modules is not None: + return get_all_object_names.modules else: - get_all_objects.modules = [] + get_all_object_names.modules = [] # Projects project_modules = get_each_project_module() @@ -46,12 +46,12 @@ def get_all_objects() -> list(): else: # Ignoring old single-files projects continue - get_all_objects.modules.append(m.name) + get_all_object_names.modules.append(m.name) - return get_all_objects.modules + return get_all_object_names.modules -get_all_objects.modules = None +get_all_object_names.modules = None # Return a list of scripts from all the projects fuddly knows about diff --git a/src/fuddly/libs/fmk_services.py b/src/fuddly/libs/fmk_services.py index 5fd62530..af714c75 100644 --- a/src/fuddly/libs/fmk_services.py +++ b/src/fuddly/libs/fmk_services.py @@ -40,7 +40,7 @@ def get_each_data_model_module() -> list(): def get_each_target_module() -> list(): return get_module_of_type( group_name="targets", - prefix="fuddly/data_models" + prefix="fuddly/targets" ) From 826f26de2c1b8ea10c5663342ed92393ee21dcfe Mon Sep 17 00:00:00 2001 From: Nicolai Dagestad Date: Tue, 20 Jan 2026 16:23:30 +0100 Subject: [PATCH 06/11] Small code cleanup in importer --- src/fuddly/libs/importer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fuddly/libs/importer.py b/src/fuddly/libs/importer.py index 78384042..83c01806 100644 --- a/src/fuddly/libs/importer.py +++ b/src/fuddly/libs/importer.py @@ -2,9 +2,11 @@ from importlib.metadata import entry_points, EntryPoint from importlib.abc import MetaPathFinder from importlib.util import spec_from_file_location, module_from_spec -from importlib.machinery import ModuleSpec, PathFinder +from importlib.machinery import ModuleSpec from importlib.util import find_spec +# This import is important for some of the magic this module does +# At least I think it is import fuddly from fuddly.framework.global_resources import ( ep_group_names, @@ -14,7 +16,6 @@ from fuddly.libs.external_modules import colorize, Color import os.path -import code import sys From ae23632fc9df0fd32413518e4b00738db48b6af0 Mon Sep 17 00:00:00 2001 From: Nicolai Dagestad Date: Thu, 29 Jan 2026 17:08:00 +0100 Subject: [PATCH 07/11] Document the new --clone option for the 'new' command --- docs/fuddly.1.scd | 51 +++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/docs/fuddly.1.scd b/docs/fuddly.1.scd index 73a279ad..3caffc92 100644 --- a/docs/fuddly.1.scd +++ b/docs/fuddly.1.scd @@ -64,32 +64,31 @@ a *-h* and *--help* options to describe their use from the command line ## NEW -*fuddly* new [OPTIONS] object name +*fuddly* new [OPTIONS] name - *object* - Type of object to create. [dm, data-model, project: