diff --git a/pyproject.toml b/pyproject.toml index dd01be8..48b8a72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] description = "Mommy's here to support you when running python (in a virtual enviroment)~ ❤️" name = "python_mommy_venv" -dependencies = ["toml"] +dependencies = ["toml", "requests"] authors = [] readme = "README.md" requires-python = ">=3.9" @@ -15,7 +15,6 @@ license-files = ["LICENSE"] [project.scripts] python-mommy-dev = "python_mommy_venv.__main__:development" -python-mommy-generate-config = "python_mommy_venv.__main__:write_current_config" mommify-venv = "python_mommy_venv.__main__:mommify_venv" [build-system] diff --git a/python_mommy_venv/__init__.py b/python_mommy_venv/__init__.py index 2f18315..641a687 100644 --- a/python_mommy_venv/__init__.py +++ b/python_mommy_venv/__init__.py @@ -1,21 +1,32 @@ import random import sys from typing import Optional +import json -from .config import get_mood, get_template_values -from .static import RESPONSES, Situation, colors +from .static import colors, get_compiled_config_file -def get_response(situation: Situation, colorize: Optional[bool] = None): +def get_response_from_situation(situation: str, colorize: Optional[bool] = None): if colorize is None: colorize = sys.stdout.isatty() # get message - mood = get_mood() - template = random.choice(RESPONSES[mood][situation]) - message = template.format(**get_template_values(mood)) + config = json.loads(get_compiled_config_file().read_text()) + existing_moods = list(config["moods"].keys()) + template_options = config["moods"][random.choice(existing_moods)][situation] + template: str = random.choice(template_options) + + template_values = {} + for key, values in config["vars"].items(): + template_values[key] = random.choice(values) + + message = template.format(**template_values) # return message if not colorize: return message return colors.BOLD + message + colors.ENDC + + +def get_response(code: int, colorize: Optional[bool] = None) -> str: + return get_response_from_situation("positive" if code == 0 else "negative") diff --git a/python_mommy_venv/__main__.py b/python_mommy_venv/__main__.py index a154930..326d692 100644 --- a/python_mommy_venv/__main__.py +++ b/python_mommy_venv/__main__.py @@ -3,16 +3,14 @@ from pathlib import Path import stat import subprocess import logging +import json +import argparse -import toml - -from . import get_response -from .static import Situation -from .config import CONFIG_FILES, CONFIG_DIRECTORY, generate_current_configuration - +from .responses import compile_config +from .static import IS_VENV, VENV_DIRECTORY, CONFIG_DIRECTORY, COMPILED_CONFIG_FILE_NAME logging.basicConfig( - format=' %(message)s', + format='%(message)s', force=True, ) @@ -21,7 +19,18 @@ log_level = logging.INFO mommy_logger = logging.getLogger("mommy") mommy_logger.setLevel(logging.INFO) serious_logger = logging.getLogger("serious") -serious_logger.setLevel(logging.WARNING) +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) def development(): @@ -29,28 +38,14 @@ def development(): if len(sys.argv) > 1: s = sys.argv[1] - print(get_response(Situation(s))) + compile_config() -def write_current_config(): - f = "python-mommy.toml" - if len(sys.argv) > 1: - f = sys.argv[1] - - config_file = CONFIG_DIRECTORY / f - print(f"writing to: {config_file}") - - data = toml.dumps(generate_current_configuration()) - print(data) - with config_file.open("w") as f: - f.write(data) - - WRAPPER_TEMPLATE = """#!{inner_bin} # -*- coding: utf-8 -*- import sys, subprocess -from python_mommy_venv import get_response, Situation +from python_mommy_venv import get_response INTERPRETER = "{inner_bin}" @@ -58,7 +53,7 @@ result = subprocess.run([INTERPRETER] + sys.argv[1:]) code = result.returncode print() -print(get_response(Situation.POSITIVE if code == 0 else Situation.NEGATIVE)) +print(get_response(code)) exit(code=code) """ @@ -98,6 +93,13 @@ PIP_HOOK = """# GENERATED BY MOMMY sys.exit(code)""" +def assert_venv(): + if not IS_VENV: + mommy_logger.error("mommy doesn't run in a virtual environment~") + serious_logger.error("this has to run in a virtual environment") + exit(1) + + def wrap_interpreter(path: Path): mommy_logger.info("mommy found a symlink to an interpreter~ %s", str(path)) serious_logger.info("interpreter symlink found at %s", str(path)) @@ -145,12 +147,39 @@ def install_pip_hook(path: Path): 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" + ) - v = ".venv" - if len(sys.argv) > 1: - v = sys.argv[1] + parser.add_argument( + "-l", "--local", + action="store_true", + help="compile the config only for the current virtual environment" + ) - bin_path = Path(v, "bin") + args = parser.parse_args() + + config_logging(args.verbose) + assert_venv() + + compiled_base_dir = VENV_DIRECTORY if args.local else CONFIG_DIRECTORY + compiled_config_file = compiled_base_dir / COMPILED_CONFIG_FILE_NAME + compiled = compile_config() + 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 args.local: + (VENV_DIRECTORY / COMPILED_CONFIG_FILE_NAME).unlink(missing_ok=True) + + 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)) @@ -162,10 +191,10 @@ def mommify_venv(): if path.is_symlink(): # could be python interpreter # check for both just to be more expressive - if name.startswith("inner_") or not name.startswith("python"): + if name.startswith("inner_"): continue - if subprocess.run([str(path), '-c', '"exit()"']) != 0: + if subprocess.run([str(path), '-c', '"exit(0)"']).returncode != 0: continue wrap_interpreter(path) @@ -176,6 +205,3 @@ def mommify_venv(): continue install_pip_hook(path) - - serious_logger.info("") - mommy_logger.info("") diff --git a/python_mommy_venv/config.py b/python_mommy_venv/config.py deleted file mode 100644 index 3025005..0000000 --- a/python_mommy_venv/config.py +++ /dev/null @@ -1,193 +0,0 @@ -from typing import Optional, List, Dict, Union -import os -from os.path import expandvars -from sys import platform -import logging -from pathlib import Path -import toml -import random - -from .static import RESPONSES - -logger = logging.Logger("mommy_config") - -PREFIXES = [ - "PYTHON", # first one is always the prefix of the current program - "CARGO", -] - - -# env key is just a backup key for compatibility with cargo mommy -CONFIG = { - "mood": { - "defaults": ["chill"] - }, - "emote": { - "defaults": ["❤️", "💖", "💗", "💓", "💞"] - }, - "pronoun": { - "defaults": ["her"] - }, - "role": { - "defaults": ["mommy"] - }, - "affectionate_term": { - "defaults": ["girl"], - "env_key": "LITTLE" - }, - "denigrating_term": { - "spiciness": "yikes", - "defaults": ["slut", "toy", "pet", "pervert", "whore"], - "env_key": "FUCKING" - }, - "part": { - "spiciness": "yikes", - "defaults": ["milk"] - } -} - -MOOD_PRIORITIES: Dict[str, int] = {} -for i, mood in enumerate(RESPONSES): - MOOD_PRIORITIES[mood] = i - -PREFIXES = [ - "PYTHON", # first one is always the prefix of the current program - "CARGO", -] - -for key, value in CONFIG.items(): - env_keys = [ - PREFIXES[0] + "_MOMMY_" + key.upper(), - "MOMMY_" + key.upper(), - *(p + "_MOMMY_" + key.upper() for p in PREFIXES) - ] - - if value.get("env_key") is not None: - env_keys.append(value.get("env_key")) - - for env_key in env_keys: - res = os.environ.get(env_key) - if res is not None: - value["default"] = res.split("/") - - -def _get_xdg_config_dir() -> Path: - res = os.environ.get("XDG_CONFIG_HOME") - if res is not None: - return Path(res) - - xdg_user_dirs_file = Path(os.environ.get("XDG_CONFIG_HOME") or Path(Path.home(), ".config", "user-dirs.dirs")) - xdg_user_dirs_default_file = Path("/etc/xdg/user-dirs.defaults") - - def get_dir_from_xdg_file(xdg_file_path: Path, key_a: str) -> Optional[str]: - if not xdg_file_path.exists(): - logger.info("config file not found in %s", str(xdg_file_path)) - return - - with xdg_file_path.open("r") as f: - for line in f: - if line.startswith("#"): - continue - - parts = line.split("=") - if len(parts) > 2: - continue - - key_b = parts[0].lower().strip() - value = parts[1].strip().split("#")[0] - - if key_a.lower() == key_b: - return value - - logger.info("key %s not found in %s", key_a, str(xdg_file_path)) - - res = get_dir_from_xdg_file(xdg_user_dirs_file, "XDG_CONFIG_HOME") - if res is not None: - return Path(res) - - res = get_dir_from_xdg_file(xdg_user_dirs_default_file, "CONFIG") - if res is not None: - return Path(Path.home(), res) - - - res = get_dir_from_xdg_file(xdg_user_dirs_default_file, "XDG_CONFIG_HOME") - if res is not None: - return Path(Path.home(), res) - - default = Path(Path.home(), ".config") - logging.info("falling back to %s", default) - return default - - -CONFIG_DIRECTORY = _get_xdg_config_dir() / "mommy" -CONFIG_FILES = [ - CONFIG_DIRECTORY / "python-mommy.toml", - CONFIG_DIRECTORY / "mommy.toml", -] - -def load_config_file(config_file: Path) -> bool: - global CONFIG - if not config_file.exists(): - return False - - with config_file.open("r") as f: - data = toml.load(f) - - for key, value in data.items(): - if isinstance(value, str): - CONFIG[key]["defaults"] = [value] - else: - CONFIG[key]["defaults"] = value - - return True - - -for c in CONFIG_FILES: - if load_config_file(c): - break - - -# validate config file -if True: - unfiltered_moods = CONFIG["mood"]["defaults"] - CONFIG["mood"]["defaults"] = filtered_moods = [] - for mood in unfiltered_moods: - if mood in RESPONSES: - filtered_moods.append(mood) - else: - logger.warning("mood %s isn't supported", mood) - - -def get_mood() -> str: - return random.choice(CONFIG["mood"]["defaults"]) - -def get_template_values(mood: str) -> Dict[str, str]: - mood_spice_level = MOOD_PRIORITIES[mood] - result = {} - - for key, value in CONFIG.items(): - spice = value.get("spiciness") - allow_key = spice is None - if not allow_key: - key_spice_level = MOOD_PRIORITIES[spice] - allow_key = mood_spice_level >= key_spice_level - - if not allow_key: - continue - - result[key] = random.choice(value["defaults"]) - - return result - -def generate_current_configuration() -> Dict[str, Union[str, List[str]]]: - global CONFIG - generated = {} - - for key, definition in CONFIG.items(): - value = definition["defaults"] - if len(value) == 1: - value = value[0] - - generated[key] = value - - return generated diff --git a/python_mommy_venv/responses.py b/python_mommy_venv/responses.py new file mode 100644 index 0000000..1791547 --- /dev/null +++ b/python_mommy_venv/responses.py @@ -0,0 +1,130 @@ +from pathlib import Path +import json +from typing import Dict, Optional, List +import os +import logging +import toml +import random +import requests + +from .static import get_config_file + +mommy_logger = logging.getLogger("mommy") +serious_logger = logging.getLogger("serious") + +PREFIX = "MOMMY" + +RESPONSES_URL = "https://raw.githubusercontent.com/diamondburned/go-mommy/refs/heads/main/responses.json" +RESPONSES_FILE = Path(__file__).parent / "responses.json" +ADDITIONAL_ENV_VARS = { + "pronoun": "PRONOUNS", + "role": "ROLES", + "emote": "EMOTES", + "mood": "MOODS", +} + + + +def _load_config_file(config_file: Path) -> Dict[str, List[str]]: + with config_file.open("r") as f: + data = toml.load(f) + + result = {} + for key, value in data.items(): + if isinstance(value, str): + result[key] = [value] + else: + result[key] = value + + return result + + +ADDITIONAL_PROGRAM_PREFIXES = [ + "cargo", # only as fallback if user already configured cargo +] + +def _get_env_var_names(name: str): + BASE = PREFIX + "_" + name.upper() + yield "PYTHON_" + BASE + yield BASE + for a in ADDITIONAL_PROGRAM_PREFIXES: + yield a + "_" + BASE + +def _get_env_value(name: str) -> Optional[str]: + if name in ADDITIONAL_ENV_VARS: + for key in _get_env_var_names(ADDITIONAL_ENV_VARS[name]): + val = os.environ.get(key) + if val is not None: + return val + + for key in _get_env_var_names(name): + val = os.environ.get(key) + if val is not None: + return val + + +def compile_config(disable_requests: bool = False) -> dict: + global RESPONSES_FILE, RESPONSES_URL + + data = json.loads(RESPONSES_FILE.read_text()) + + if not disable_requests: + mommy_logger.info("mommy downloads newest responses for her girl~ %s", RESPONSES_URL) + serious_logger.info("downloading cargo mommy responses: %s", RESPONSES_URL) + r = requests.get(RESPONSES_URL) + data = r.json() + + config_definition: Dict[str, dict] = data["vars"] + mood_definitions: Dict[str, dict] = data["moods"] + + # environment variables for compatibility with cargo mommy + # fill ADDITIONAL_ENV_VARS with the "env_key" values + for key, conf in config_definition.items(): + if "env_key" in conf: + ADDITIONAL_ENV_VARS[key] = conf["env_key"] + + # set config to the default values + config: Dict[str, List[str]] = {} + for key, conf in config_definition.items(): + config[key] = conf["defaults"] + + # load config file + config_file = get_config_file() + if config_file is not None: + config.update(_load_config_file(config_file)) + + # fill config with env + for key, conf in config_definition.items(): + val = _get_env_value(key) + if val is not None: + config[key] = val.split("/") + + # validate moods + for mood in config["mood"]: + if mood not in mood_definitions: + supported_moods_str = ", ".join(mood_definitions.keys()) + mommy_logger.error( + "%s doesn't know how to feel %s... %s moods are %s", + random.choice(config['role']), + mood, + random.choice(config['pronoun']), + supported_moods_str, + ) + serious_logger.error( + "mood '%s' doesn't exist. moods are %s", + mood, + supported_moods_str, + ) + exit(1) + + # compile + compiled = {} + compiled_moods = compiled["moods"] = {} + compiled_vars = compiled["vars"] = {} + + for mood in config["mood"]: + compiled_moods[mood] = mood_definitions[mood] + del config["mood"] + compiled_vars.update(config) + + return compiled diff --git a/python_mommy_venv/static.py b/python_mommy_venv/static.py index 499bf0b..a934e4c 100644 --- a/python_mommy_venv/static.py +++ b/python_mommy_venv/static.py @@ -1,136 +1,14 @@ from __future__ import annotations -from enum import Enum -class Situation(Enum): - POSITIVE = "positive" - NEGATIVE = "negative" - OVERFLOW = "overflow " +from pathlib import Path +import os +import logging +from typing import Optional +import sys -RESPONSES = { - "chill": { - Situation.POSITIVE: [ - "*pets your head*", - "*gives you scritches*", - "you're such a smart cookie~", - "that's a good {affectionate_term}~", - "{role} thinks {pronoun} little {affectionate_term} earned a big hug~", - "good {affectionate_term}~\n{role}'s so proud of you~", - "aww, what a good {affectionate_term}~\n{role} knew you could do it~", - "you did it~!", - "{role} loves you~", - "*gives you a sticker*", - "*boops your nose*", - "*wraps you in a big hug*", - "well done~!\n{role} is so happy for you~", - "what a good {affectionate_term} you are~", - "that's {role}'s clever little {affectionate_term}~", - "you're doing so well~!", - "you're making {role} so happy~", - "{role} loves {pronoun} cute little {affectionate_term}~" - ], - Situation.NEGATIVE: [ - "{role} believes in you~", - "don't forget to hydrate~", - "aww, you'll get it next time~", - "do you need {role}'s help~?", - "everything's gonna be ok~", - "{role} still loves you no matter what~", - "oh no did {role}'s little {affectionate_term} make a big mess~?", - "{role} knows {pronoun} little {affectionate_term} can do better~", - "{role} still loves you~", - "{role} thinks {pronoun} little {affectionate_term} is getting close~", - "it's ok, {role}'s here for you~", - "oh, darling, you're almost there~", - "does {role}'s little {affectionate_term} need a bit of a break~?", - "oops~! {role} loves you anyways~", - "try again for {role}, {affectionate_term}~", - "don't worry, {role} knows you can do it~" - ], - Situation.OVERFLOW: [ - "{role} has executed too many times and needs to take a nap~" - ] - }, - "ominous": { - Situation.POSITIVE: [ - "What you have set in motion today will be remembered for aeons to come!", - "{role} will see to it that {pronoun} little {affectionate_term}'s name is feared~", - "{role} is proud of the evil seed {pronoun} {affectionate_term} has planted into this accursed world" - ], - Situation.NEGATIVE: [ - "Ah, failure? {role} will make sure the stars are right next time", - "Does {role}'s little {affectionate_term} need more time for worship~?", - "May the mark of the beast stain your flesh forever, {role} will haunt your soul forevermore" - ], - Situation.OVERFLOW: [ - "THOU HAST DRUNK TOO DEEPLY OF THE FONT" - ] - }, - "thirsty": { - "spiciness": "thirsty", - Situation.POSITIVE: [ - "*tugs your leash*\nthat's a VERY good {affectionate_term}~", - "*runs {pronoun} fingers through your hair* good {affectionate_term}~ keep going~", - "*smooches your forehead*\ngood job~", - "*nibbles on your ear*\nthat's right~\nkeep going~", - "*pats your butt*\nthat's a good {affectionate_term}~", - "*drags {pronoun} nail along your cheek*\nsuch a good {affectionate_term}~", - "*bites {pronoun} lip*\nmhmm~", - "give {role} a kiss~", - "*heavy breathing against your neck*" - ], - Situation.NEGATIVE: [ - "you're so cute when you're flustered~", - "do you think you're going to get a reward from {role} like that~?", - "*grabs your hair and pulls your head back*\nyou can do better than that for {role} can't you~?", - "if you don't learn how to code better, {role} is going to put you in time-out~", - "does {role} need to give {pronoun} little {affectionate_term} some special lessons~?", - "you need to work harder to please {role}~", - "gosh you must be flustered~", - "are you just keysmashing now~?\ncute~", - "is {role}'s little {affectionate_term} having trouble reaching the keyboard~?" - ], - Situation.OVERFLOW: [ - "you've been a bad little {affectionate_term} and worn out {role}~" - ] - }, - "yikes": { - "spiciness": "yikes", - Situation.POSITIVE: [ - "keep it up and {role} might let you cum you little {denigrating_term}~", - "good {denigrating_term}~\nyou've earned five minutes with the buzzy wand~", - "mmm~ come taste {role}'s {part}~", - "*slides {pronoun} finger in your mouth*\nthat's a good little {denigrating_term}~", - "you're so good with your fingers~\n{role} knows where {pronoun} {denigrating_term} should put them next~", - "{role} is getting hot~", - "that's a good {denigrating_term}~", - "yes~\nyes~~\nyes~~~", - "{role}'s going to keep {pronoun} good little {denigrating_term}~", - "open wide {denigrating_term}.\nyou've earned {role}'s {part}~", - "do you want {role}'s {part}?\nkeep this up and you'll earn it~", - "oooh~ what a good {denigrating_term} you are~" - ], - Situation.NEGATIVE: [ - "you filthy {denigrating_term}~\nyou made a mess, now clean it up~\nwith your tongue~", - "*picks you up by the throat*\npathetic~", - "*drags {pronoun} claws down your back*\ndo it again~", - "*brandishes {pronoun} paddle*\ndon't make me use this~", - "{denigrating_term}.\n{denigrating_term}~\n{denigrating_term}~~", - "get on your knees and beg {role} for forgiveness you {denigrating_term}~", - "{role} doesn't think {pronoun} little {denigrating_term} should have permission to wear clothes anymore~", - "never forget you belong to {role}~", - "does {role} need to put you in the {denigrating_term} wiggler~?", - "{role} is starting to wonder if you should just give up and become {pronoun} breeding stock~", - "on your knees {denigrating_term}~", - "oh dear. {role} is not pleased", - "one spank per error sounds appropriate, don't you think {denigrating_term}?", - "no more {part} for you {denigrating_term}" - ], - Situation.OVERFLOW: [ - "brats like you don't get to talk to {role}" - ] - } -} +logger = logging.Logger(__name__) + class colors: HEADER = '\033[95m' @@ -142,3 +20,89 @@ class colors: BOLD = '\033[1m' UNDERLINE = '\033[4m' ENDC = '\033[0m' + + +def _get_xdg_config_dir() -> Path: + res = os.environ.get("XDG_CONFIG_HOME") + if res is not None: + return Path(res) + + xdg_user_dirs_file = Path(os.environ.get("XDG_CONFIG_HOME") or Path(Path.home(), ".config", "user-dirs.dirs")) + xdg_user_dirs_default_file = Path("/etc/xdg/user-dirs.defaults") + + def get_dir_from_xdg_file(xdg_file_path: Path, key_a: str) -> Optional[str]: + if not xdg_file_path.exists(): + logger.info("config file not found in %s", str(xdg_file_path)) + return + + with xdg_file_path.open("r") as f: + for line in f: + if line.startswith("#"): + continue + + parts = line.split("=") + if len(parts) > 2: + continue + + key_b = parts[0].lower().strip() + value = parts[1].strip().split("#")[0] + + if key_a.lower() == key_b: + return value + + logger.info("key %s not found in %s", key_a, str(xdg_file_path)) + + res = get_dir_from_xdg_file(xdg_user_dirs_file, "XDG_CONFIG_HOME") + if res is not None: + return Path(res) + + res = get_dir_from_xdg_file(xdg_user_dirs_default_file, "CONFIG") + if res is not None: + return Path(Path.home(), res) + + + res = get_dir_from_xdg_file(xdg_user_dirs_default_file, "XDG_CONFIG_HOME") + if res is not None: + return Path(Path.home(), res) + + default = Path(Path.home(), ".config") + logging.info("falling back to %s", default) + return default + + +CONFIG_DIRECTORY = _get_xdg_config_dir() / "mommy" +COMPILED_CONFIG_FILE_NAME = "compiled-mommy.json" + +IS_VENV = sys.prefix != sys.base_prefix +VENV_DIRECTORY = Path(sys.prefix) + +def get_config_file() -> Optional[Path]: + config_files = [] + if IS_VENV: + config_files.extend([ + VENV_DIRECTORY / "python-mommy.toml", + VENV_DIRECTORY / "mommy.toml", + ]) + config_files.extend([ + CONFIG_DIRECTORY / "python-mommy.toml", + CONFIG_DIRECTORY / "mommy.toml", + ]) + + for f in config_files: + if f.exists(): + return f + + + + +def get_compiled_config_file() -> Path: + compiled_config_files = [ + VENV_DIRECTORY / "compiled-mommy.json", + CONFIG_DIRECTORY / "compiled-mommy.json", + ] + + for f in compiled_config_files: + if f.exists(): + return f + + raise Exception("couldn't find compiled config file") diff --git a/test.py b/test.py new file mode 100644 index 0000000..f184c11 --- /dev/null +++ b/test.py @@ -0,0 +1 @@ +print("success") \ No newline at end of file