Source code for pynamer.builder

#!/usr/bin/env python3
"""Collection of functions to build a minimal package and publish on PyPI."""

# Core Library modules
import os
import pickle
import re
import shutil
import subprocess
import sys
from importlib.resources import as_file
from pathlib import Path
from typing import Any, Union

# Third party modules
import build
from colorama import Back, Fore, Style
from jinja2 import Template

# Local modules
from . import (
    logger,
    meta,
    meta_file_trv,
    project_path,
    setup_base_file_trv,
    setup_file_py_trv,
    setup_file_trv,
    setup_text,
)
from .config import config
from .utils import feedback


[docs] def create_setup(new_project_name: str, new_meta: bool = False) -> None: """Utility script to create a setup.py file. The object being to create a setup.py file from a 'template' file for the purpose of creating a minimalist package for upload to PyPI. Args: new_project_name: name used to render the template. new_meta: generate new package metadata. """ author = meta["author"] if meta else "" email = meta["email"] if meta else "" description = meta["description"] if meta else config.description version = meta["version"] if meta else config.package_version _email_pattern = ( r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:" r"[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?" ) if "AUTHOR" in setup_text or "EMAIL" in setup_text or not meta or new_meta: try: while True: author = ( input( Fore.YELLOW + Back.BLACK + Style.BRIGHT + f"please enter your author name ({author}): " + Style.RESET_ALL ) or author ) if author != "": break except KeyboardInterrupt as e: # pragma: no cover feedback("...bye!", "warning") raise SystemExit() from e try: while True: email = ( input( Fore.YELLOW + Back.BLACK + Style.BRIGHT + f"please enter your email address ({email}): " + Style.RESET_ALL ) or email ) if re.search(_email_pattern, email): break else: print( Fore.YELLOW + Back.BLACK + Style.BRIGHT + "does not appear to be a valid email address" + Style.RESET_ALL ) # pragma: no cover except KeyboardInterrupt as e: # pragma: no cover feedback("...bye!", "warning") raise SystemExit() from e try: while True: version = ( input( Fore.YELLOW + Back.BLACK + Style.BRIGHT + f"version number: ({version}): " + Style.RESET_ALL ) or version ) if version != "": break except KeyboardInterrupt as e: # pragma: no cover raise SystemExit("Bye!") from e try: while True: description = ( input( Fore.YELLOW + Back.BLACK + Style.BRIGHT + f"description: ({description}): " + Style.RESET_ALL ) or description ) if description != "": break except KeyboardInterrupt as e: # pragma: no cover raise SystemExit("Bye!") from e with as_file(setup_file_trv) as setup_file: if setup_file.exists(): setup_file.unlink(missing_ok=True) setup_text_base = setup_base_file_trv.read_text(encoding="utf-8") template = Template(setup_text_base) content = template.render( PROJECT_NAME="{{ PROJECT_NAME }}", PACKAGE_VERSION=version, DESCRIPTION=description, AUTHOR=author, EMAIL=email, ) with ( as_file(setup_file_trv) as setup_file, setup_file.open("w", encoding="utf-8") as f, ): f.write(content) meta_save = { "author": author, "email": email, "description": description, "version": version, } with as_file(meta_file_trv) as meta_file, meta_file.open("wb") as f: pickle.dump(meta_save, f) # type: ignore[arg-type] setup_text_file = setup_file_trv.read_text(encoding="utf-8") template = Template(setup_text_file) content = template.render(PROJECT_NAME=new_project_name) with ( as_file(setup_file_py_trv) as setup_file_py, setup_file_py.open("w", encoding="utf-8") as message, ): logger.debug("creating new setup.py with the following: \n %s", content) message.write(content)
[docs] def rename_project_dir(old_name: str, new_name: str) -> None: """Utility script to rename a directory. The object being to rename a 'template' directory for the purpose of creating a minimalist package for upload to PyPI. Args: old_name: source name. new_name: dst name. Raises: FileNotFoundError """ old_directory_path = Path(old_name) new_directory_path = Path(new_name) logger.debug("renaming project directory from %s to %s", old_name, new_name) try: old_directory_path.rename(new_directory_path) except FileNotFoundError as e: logger.error("directory %s cannot be found:", old_directory_path) raise FileNotFoundError from e
[docs] def delete_director(items_to_delete: Any) -> None: """Utility function to delete files and directories. Args: items_to_delete: A list of Path like objects to delete. """ for item in items_to_delete: if not item.exists(): logger.debug("trying to delete %s but it does not exist", item) continue if item.is_dir(): logger.debug("Deleting Directory: %s", item) shutil.rmtree(item, ignore_errors=True) else: logger.debug("Deleting File: %s", item) Path.unlink(item, missing_ok=True)
[docs] def cleanup(project_name: str) -> None: """Builds a manifest of artifacts to delete into a list of Path objects. Args: project_name: the name of the project currently under test. """ if config.no_cleanup is True: # pragma: no cover return rename_project_dir( str(project_path.joinpath(project_name)), str(project_path.joinpath(config.original_project_name)), ) build_artifacts = [ project_path / "build", project_path / "dist", project_path / "".join([project_name, ".egg-info"]), project_path / "setup.py", ] logger.debug("cleaning build artifacts %s", build_artifacts) delete_director(build_artifacts)
[docs] def run_command( *arguments: str, shell: bool = True, working_dir: Union[Path, str, None] = None, project: Union[None, str] = None, ) -> None: # pragma: no cover """Utility designed to execute a command line utility. Args: arguments: Comma separated strings- "utility", "arg1", "arg2", etc. shell: command executed by the shell or directly by the operating system. working_dir: specifies the current working directory to use when starting the subprocess. e.g. "/home/user/mydir" project: the name of the project currently being tested """ working_dir = os.getcwd() if working_dir is None else working_dir try: process = subprocess.Popen( arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, text=True, cwd=working_dir, ) stdout, stderr = process.communicate() if process.returncode != 0: logger.error("Error running command: %s", arguments) logger.error("stderr: %s", stderr) if project is not None: cleanup(project) return logger.debug("%s", stdout) return except Exception as e: logger.error("Exception running command: %s", arguments) logger.error(e) if project is not None: cleanup(project) return
[docs] def upload_dist(project_name: str) -> None: """Builds the twine command line to upload the minimalist project to PyPI. Args: project_name: the name of the project currently under test. Notes: twine expects a filesystem path not Path object so use os.fspath() """ logger.debug("Uploading the distribution... ") project_build = str(project_path / "dist" / "*") dir_path = os.fspath(project_build) pypirc_path = os.fspath(str(config.pypirc)) run_command( sys.executable, "-m", "twine", "upload", "--config-file", pypirc_path, dir_path, project=project_name, )
[docs] def build_dist() -> None: """Builds the sdist and wheel of the minimalist project to upload to PyPI.""" logger.debug("Building the distribution... ") builder = build.ProjectBuilder(project_path) builder.build("wheel", project_path / "dist") builder.build("sdist", project_path / "dist")