Module psdi_data_conversion.main_create_plugin
psdi_data_conversion/main_create_plugin.py
Created 2026-07-01 by Bryan Gillis.
Entry-point file for the script to create a new converter plugin.
Functions
def get_argument_parser()-
Expand source code
def get_argument_parser(): """Get an argument parser for this script. Returns ------- parser : ArgumentParser An argument parser set up with the allowed command-line arguments for this script. """ parser = ArgumentParser(formatter_class=CustomHelpFormatter) parser.add_argument("plugin_name", type=str, nargs="+", help=f"The name of the plugin to be created, e.g. {tc.MESSAGE}'Open Babel'{tc.OFF}") parser.add_argument("--label", type=str, default=None, help="The label for the package (i.e. Python-compatible package name). By default, will " f"convert <NAME> to snake_case (e.g. {tc.MESSAGE}'Open Babel'{tc.OFF} -> " f"{tc.MESSAGE}'open_babel'{tc.OFF})") parser.add_argument("--script", action="store_true", help=f"If set, will create the plugin using the {tc.CODE}`ScriptFileConverter`{tc.OFF} base " "class, which uses a script to run the conversion. The script will by default be named " f"{tc.PATH}'<LABEL>.sh'{tc.OFF}") return parserGet an argument parser for this script.
Returns
parser:ArgumentParser- An argument parser set up with the allowed command-line arguments for this script.
def import_from_path(module_name, file_path)-
Expand source code
def import_from_path(module_name, file_path): """Import a Python module from a path within code""" spec = importlib.util.spec_from_file_location(module_name, file_path) module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) return moduleImport a Python module from a path within code
def main()-
Expand source code
def main(): """Standard entry-point function for this script. """ args = parse_args() run_from_args(args)Standard entry-point function for this script.
def parse_args()-
Expand source code
def parse_args(): """Parses arguments for this executable. Returns ------- args : Namespace The parsed arguments. """ parser = get_argument_parser() args = parser.parse_args() return argsParses arguments for this executable.
Returns
args:Namespace- The parsed arguments.
def run_from_args(args)-
Expand source code
def run_from_args(args): """Workhorse function to perform primary execution of this script, using the provided parsed arguments. Parameters ---------- args : Namespace The parsed arguments for this script. """ # Check if the package is installed in editable mode confirm_editable_mode() # Get the name and label from input arguments name: str = " ".join(args.plugin_name) name = name.strip("'\"").strip() label: str | None = args.label if label: # Check that the label appears to be properly in snake_case if label != label.lower().replace(" ", "_") or NON_SNAKE_CASE_CHAR_RE.search(label): print_wrap(f"{tc.ERROR}ERROR:{tc.OFF} Label {tc.MESSAGE}'{label}'{tc.OFF} is invalid. The label should be " "in snake_case (all lower-case with underscores in place of spaces), containing only letters, " "digits, and underscores", err=True) exit(1) else: # Create the label by converting the name to snake_case and stripping invalid characters label = NON_SNAKE_CASE_CHAR_RE.sub("", name.lower().replace(" ", "_")) if not label: print_wrap(f"{tc.ERROR}ERROR:{tc.OFF} A valid label could not be generated from converter name '{name}'. " f"Please specify a label directly with {tc.CODE}`--label LABEL`{tc.OFF}. The label should " "be in snake_case (all lower-case with underscores in place of spaces), containing only " "letters, digits, and underscores", err=True) exit(1) # Determine the PascalCase name of the converter (to be used for classes) l_name_words = name.split(" ") pascal_name = "".join(map(lambda x: x.capitalize(), l_name_words)) # Get the project path to use based on if we're using a test path or not if os.environ.get(TEST_PATH_KEY): project_path: Path = Path(os.environ[TEST_PATH_KEY]).resolve() if not project_path.is_dir(): print_wrap(f"{tc.ERROR}ERROR:{tc.OFF} When running this script with {tc.CODE}`TEST_PATH=<TEST_PATH>" + f"`{tc.OFF}, the provided path ({tc.CYAN}'{project_path}'{tc.OFF}) must already exist.", err=True) exit(1) else: project_path = get_project_path() # Get the directory containing converter plugins conv_path = project_path / "psdi_data_conversion/converters" # Check for any name clashes with existing converters for qual_dir in conv_path.iterdir(): dir = qual_dir.parts[-1] if not qual_dir.is_dir() or not (qual_dir / "__init__.py").is_file(): continue if label == dir: if os.environ.get(TEST_DATA_KEY): # In test mode, don't worry if it already exists, just delete it so it doesn't clash shutil.rmtree(qual_dir) break else: print_wrap(f"{tc.ERROR}ERROR:{tc.OFF} Label '{label}' clashes with the label of an " "existing converter plugin. Please choose a different label (or different name if this was " "determined from the name)", err=True) exit(1) conv_module = import_from_path(label, qual_dir / PLUGIN_PYFILE) if name == conv_module.converter.meta.name: print_wrap(f"{tc.ERROR}ERROR:{tc.OFF} Name '{name}' clashes with the name of an existing " "converter plugin. Please choose a different name", err=True) exit(1) # Determine which template directory to use and other related info if os.environ.get(TEST_DATA_KEY): test_path = os.environ.get(TEST_PATH_KEY) test_path = Path(test_path) if test_path else None template_path = get_test_data_loc(test_path) / name template_str: str = "Template" elif args.script: template_path = conv_path / PLUGIN_SCRIPT_TEMPLATEDIR template_str: str = "ScriptTemplate" else: template_path = conv_path / PLUGIN_TEMPLATEDIR template_str: str = "Template" # Create a new folder for the plugin by copying/modifying from the template appropriately plugin_path = conv_path / label plugin_path.mkdir(exist_ok=True) shutil.copy(template_path / "__init__.py", plugin_path) for filename in (PLUGIN_PYFILE, PLUGIN_DATAFILE): text = open(template_path / filename).read() if not os.environ.get(TEST_DATA_KEY): # Replace template info as appropriate if filename == PLUGIN_DATAFILE: text = text.replace(template_str, name.replace(r'"', r'\"')) else: # Where the template string appears alone as part of a word, replace it with the name, otherwise # replace it with the Pascal-case name text = re.sub(f"\b{template_str}\b", name, text) text = text.replace(template_str, pascal_name) open(plugin_path / filename, "w").write(text) print(f"{tc.SUCCESS}Success!{tc.OFF} The plugin has been created at " f"{tc.PATH}'{plugin_path}'{tc.OFF}\nNext steps:\n") print_wrap(f"- Edit the {tc.PATH}'{PLUGIN_PYFILE}'{tc.OFF} and {tc.PATH}'{PLUGIN_DATAFILE}'{tc.OFF} files in this " "directory to contain all necessary information about this converter and how to run it\n", initial_indent="", subsequent_indent=" "*2) print_wrap(f"- Run the script {tc.CODE}`psdi-data-convert-install-plugins`{tc.OFF} to install it\n", initial_indent="", subsequent_indent=" "*2) print_wrap("- If this script highlights that formats provided by this plugin may already be in the database," "follow the provided instructions to resolve this and then run it again", initial_indent="", subsequent_indent=" "*2)Workhorse function to perform primary execution of this script, using the provided parsed arguments.
Parameters
args:Namespace- The parsed arguments for this script.