Merge pull request 'building_config' (#1) from building_config into main

Reviewed-on: #1
This commit is contained in:
Hazel 2025-07-29 10:28:01 +00:00
commit 3a7b46219e
7 changed files with 302 additions and 364 deletions

View File

@ -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]

View File

@ -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")

View File

@ -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("")

View File

@ -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

View File

@ -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

View File

@ -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")

1
test.py Normal file
View File

@ -0,0 +1 @@
print("success")