Source code for pyscaffold.extensions.namespace

"""
Extension that adjust project file tree to include a namespace package.

This extension adds a **namespace** option to
:obj:`~pyscaffold.api.create_project` and provides correct values for the
options **root_pkg** and **namespace_pkg** to the following functions in the
action list.
"""

import os
from pathlib import Path
from typing import List, cast

from ..actions import Action, ActionParams, ScaffoldOpts, Structure
from ..exceptions import InvalidIdentifier
from ..file_system import chdir, move
from ..identification import is_valid_identifier
from ..log import logger
from ..operations import remove
from . import Extension, store_with


[docs]class Namespace(Extension): """Add a namespace (container package) to the generated package."""
[docs] def augment_cli(self, parser): """Add an option to parser that enables the namespace extension. Args: parser (argparse.ArgumentParser): CLI parser object """ parser.add_argument( self.flag, dest=self.name, default=None, action=store_with(self), metavar="NS1[.NS2]", help="put your project inside a namespace package " "(default: use no namespace)", ) return self
[docs] def activate(self, actions: List[Action]) -> List[Action]: """Register an action responsible for adding namespace to the package. Args: actions: list of actions to perform Returns: list: updated list of actions """ actions = self.register( actions, enforce_namespace_options, after="get_default_options" ) actions = self.register(actions, add_namespace, before="version_migration") return self.register(actions, move_old_package, after="create_structure")
[docs]def prepare_namespace(namespace_str: str) -> List[str]: """Check the validity of namespace_str and split it up into a list Args: namespace_str: namespace, e.g. "com.blue_yonder" Returns: list of namespaces, e.g. ["com", "com.blue_yonder"] Raises: :obj:`InvalidIdentifier` : raised if namespace is not valid """ namespaces = namespace_str.split(".") if namespace_str else list() for namespace in namespaces: if not is_valid_identifier(namespace): raise InvalidIdentifier(f"{namespace} is not a valid namespace package.") return [".".join(namespaces[: i + 1]) for i in range(len(namespaces))]
[docs]def enforce_namespace_options(struct: Structure, opts: ScaffoldOpts) -> ActionParams: """Make sure options reflect the namespace usage.""" opts.setdefault("namespace", None) if opts["namespace"]: opts["ns_list"] = prepare_namespace(opts["namespace"]) opts["root_pkg"] = opts["ns_list"][0] opts["qual_pkg"] = ".".join([opts["ns_list"][-1], opts["package"]]) return struct, opts
[docs]def add_namespace(struct: Structure, opts: ScaffoldOpts) -> ActionParams: """Prepend the namespace to a given file structure Args: struct: directory structure as dictionary of dictionaries opts: options of the project Returns: Directory structure as dictionary of dictionaries and input options """ if not opts["namespace"]: return struct, opts namespace = opts["ns_list"][-1].split(".") base_struct = struct struct = cast(Structure, base_struct["src"]) # recursive types not supported yet pkg_struct = cast(Structure, struct[opts["package"]]) del struct[opts["package"]] for sub_package in namespace: struct[sub_package] = {"__init__.py": ("", remove)} # convert to PEP420 struct = cast(Structure, struct[sub_package]) struct[opts["package"]] = pkg_struct return base_struct, opts
[docs]def move_old_package(struct: Structure, opts: ScaffoldOpts) -> ActionParams: """Move old package that may be eventually created without namespace Args: struct (dict): directory structure as dictionary of dictionaries opts (dict): options of the project Returns: tuple(dict, dict): directory structure as dictionary of dictionaries and input options """ project_path = Path(opts.get("project_path", ".")) with chdir(project_path, log=True, **opts): old_path = Path("src", opts["package"]) namespace_path = opts["qual_pkg"].replace(".", os.sep) target = Path("src", namespace_path) old_exists = opts["pretend"] or old_path.is_dir() # ^ When pretending, pretend also an old folder exists # to show a worst case scenario log to the user... if old_exists and opts["qual_pkg"] != opts["package"]: if not opts["pretend"]: logger.warning( "\nA folder %r exists in the project directory, and it " "is likely to have been generated by a PyScaffold " "extension or manually by one of the current project " "authors.\n" "Moving it to %r, since a namespace option was passed.\n" "Please make sure to edit all the files that depend on " "this package to ensure the correct location.\n", opts["package"], namespace_path, ) move(old_path, target=target, log=True, pretend=opts["pretend"]) return struct, opts