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 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.
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 module

Import 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 args

Parses 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.