import sys from pathlib import Path import stat import subprocess import logging import json import argparse from .responses import compile_config from .static import IS_VENV, VENV_DIRECTORY, CONFIG_DIRECTORY, COMPILED_CONFIG_FILE_NAME from ntpath import devnull logging.basicConfig( format='%(message)s', force=True, ) log_level = logging.INFO mommy_logger = logging.getLogger("mommy") mommy_logger.setLevel(logging.INFO) serious_logger = logging.getLogger("serious") serious_logger.setLevel(50) def config_logging(verbose: bool): if verbose: logging.basicConfig( format=logging.BASIC_FORMAT, force=True, ) logging.getLogger().setLevel(logging.DEBUG) mommy_logger.setLevel(50) serious_logger.setLevel(logging.DEBUG) WRAPPER_TEMPLATE = """#!{inner_bin} # -*- coding: utf-8 -*- import sys, subprocess from python_mommy_venv import get_response INTERPRETER = "{inner_bin}" result = subprocess.run([INTERPRETER] + sys.argv[1:]) code = result.returncode print() print(get_response(code)) exit(code=code) """ PIP_HOOK = """# GENERATED BY MOMMY code = main() from pathlib import Path bin_path = Path(".venv", "bin") python_interpreter_wrappers = [] for path in bin_path.iterdir(): if path.is_symlink() and path.name.startswith("inner_"): python_interpreter_wrappers.append(path.name.replace("inner_", "", 1)) for path in bin_path.iterdir(): if path.is_symlink(): continue if path.name in python_interpreter_wrappers: continue text: str with path.open("r") as f: text = f.read() first_line = text.split("\\n")[0] if not ("inner_" in first_line and first_line.startswith("#!")): continue print(f"mommifying " + str(path)) text = text.replace("inner_", "", 1) with path.open("w") as f: f.write(text) sys.exit(code)""" def assert_venv(only_warn: bool = False): if not IS_VENV: mommy_logger.error("mommy doesn't run in a virtual environment~") serious_logger.error("this should run in a virtual environment") if not only_warn: exit(1) def write_compile_config(local: bool, disable_requests: bool = False): assert_venv(only_warn=not local) compiled_base_dir = VENV_DIRECTORY if local else CONFIG_DIRECTORY compiled_config_file = compiled_base_dir / COMPILED_CONFIG_FILE_NAME compiled = compile_config(disable_requests=disable_requests) mommy_logger.info("mommy writes its moods in %s", compiled_config_file) serious_logger.info("writing compiled config file to %s", compiled_config_file) compiled_base_dir.mkdir(parents=True, exist_ok=True) with compiled_config_file.open("w") as f: json.dump(compiled, f, indent=4) if not local: (VENV_DIRECTORY / COMPILED_CONFIG_FILE_NAME).unlink(missing_ok=True) def wrap_interpreter(path: Path, symlink_target: Path): mommy_logger.info("mommy found a symlink to an interpreter~ %s", str(path)) serious_logger.info("interpreter symlink found at %s", str(path)) inner_symlink = path.parent / ("inner_" + path.name) if inner_symlink.exists(): raise Exception("inner symlink somehow already exists. This shouldn't happen because of prior checks") mommy_logger.info("mommy shows her girl where the interpreter is: %s -> %s", inner_symlink, symlink_target) serious_logger.info("creating symlink: %s -> %s", inner_symlink, symlink_target) inner_symlink.symlink_to(symlink_target) # remove original symlink mommy_logger.info("mommy deletes the original interpreter~ %s", path) serious_logger.info("deleting original symlink %s", path) path.unlink() # creating the wrapper string mommy_logger.info("mommy writes wrapper script as %s", Path) serious_logger.info("writing wrapper script at %s", path) with path.open("w") as f: f.write(WRAPPER_TEMPLATE.format(inner_bin=str(inner_symlink))) serious_logger.info("making wrapper script executable") path.chmod(path.stat().st_mode | stat.S_IEXEC) def install_pip_hook(path: Path): text: str with path.open("r") as f: text = f.read() if "# GENERATED BY MOMMY" in text: mommy_logger.info("ahhhhh mommy already watches %s", str(path)) serious_logger.info("pip hook already installed at %s", str(path)) return mommy_logger.info("mommy needs to keep an eye on this little pip~ %s", str(path)) serious_logger.info("installing pip hook at %s", str(path)) text = text.replace("sys.exit(main())", PIP_HOOK, 1) with path.open("w") as f: f.write(text) def cli_compile_config(): parser = argparse.ArgumentParser(description="only recompile the config") parser.add_argument( "-v", "--verbose", action="store_true", help="enable verbose and serious output" ) parser.add_argument( "-l", "--local", action="store_true", help="compile the config only for the current virtual environment" ) parser.add_argument( "-r", "--no-requests", action="store_true", help="by default if makes one request to GitHub to fetch the newest responses, this disables that" ) args = parser.parse_args() config_logging(args.verbose) write_compile_config(args.local, disable_requests=args.no_requests) def mommify_venv(): parser = argparse.ArgumentParser(description="patch the virtual environment to use mommy") parser.add_argument( "-v", "--verbose", action="store_true", help="enable verbose and serious output" ) parser.add_argument( "-l", "--local", action="store_true", help="compile the config only for the current virtual environment" ) parser.add_argument( "-r", "--no-requests", action="store_true", help="by default if makes one request to GitHub to fetch the newest responses, this disables that" ) args = parser.parse_args() config_logging(args.verbose) assert_venv() write_compile_config(args.local) mommy_logger.info("") bin_path = VENV_DIRECTORY / "bin" bin_path = bin_path.resolve() mommy_logger.info("mommy looks in %s to mess your system up~ <33", str(bin_path)) serious_logger.info("scanning binary directory of venv at %s", str(bin_path)) # resolving the symlinks before making edits to anything because else it will mess up the resolving # and link to the wrapper instead of the original script resolved_symlinks = { path.name: path.resolve() for path in bin_path.iterdir() if path.is_symlink() } serious_logger.debug("resolved symlinks:\n%s", "\n".join( f"\t{name} => {str(target)}" for name, target in resolved_symlinks.items() )) for path in list(bin_path.iterdir()): name = path.name if path.is_symlink(): # could be python interpreter # check for both just to be more expressive if name.startswith("inner_"): continue RANDOM_RETURNCODE = 161 if subprocess.run([str(path), '-c', f'exit({RANDOM_RETURNCODE})']).returncode != RANDOM_RETURNCODE: continue wrap_interpreter(path, resolved_symlinks[path.name]) else: # could be pip if not name.startswith("pip"): continue install_pip_hook(path)