Source code for projspec.config

from contextlib import contextmanager
import json
import os

from typing import Any

conf: dict[str, dict[str, Any]] = {}
default_conf_dir = os.path.join(os.path.expanduser("~"), ".config/projspec")


def conf_dir():
    return os.environ.get("PROJSPEC_CONFIG_DIR", default_conf_dir)


def defaults():
    return {
        "library_path": f"{conf_dir()}/library.json",
        "scan_types": [".py", ".yaml", ".yml", ".toml", ".json", ".md"],
        "scan_max_files": 100,
        "scan_max_size": 5 * 2**10,
        "remote_artifact_status": False,
        "capture_artifact_output": True,
        "preferred_install_methods": ["conda", "pip"],
    }


config_doc = {
    "library_path": "location of persisted project objects",
    "scan_types": "files extensions automatically read for scanning",
    "scan_max_files": "don't scan files if more than this number in the project",
    "scan_max_size": "don't scan files bigger than this (in bytes)",
    "remote_artifact_status": "whether to check status for remote artifacts",
    "capture_artifact_output": (
        "if True, capture and enqueue output from spawned Process artifacts. "
        "Otherwise, output appears on stdout/err."
    ),
    "preferred_install_methods": (
        "ordered list of preferred installer names for install_tool(), "
        "e.g. ['uv', 'conda', 'pip']. Empty list uses the platform default."
    ),
}


def load_conf(path: str | None = None):
    fn = f"{path or conf_dir()}/projspec.json"
    conf.clear()
    if os.path.exists(fn):
        with open(fn) as f:
            conf.update(json.load(f))
    # TODO: warn on unknown keys?


load_conf()


[docs] def get_conf(name: str): """Fetch the value of the given conf parameter from the current config or defaults""" if f"PROJSPEC_{name.upper()}" in os.environ: val = os.environ[f"PROJSPEC_{name.upper()}"] else: assert name in config_doc, f"Unknown config parameter {name}" val = conf[name] if name in conf else defaults()[name] return coerce(defaults()[name], val)
def coerce(template, val): """ensure val has the same type as template""" typ = type(template) if typ is bool: return val in ("true", "True", "1", True, "T") if typ is list: return [coerce(template[0], _) for _ in val] if typ in (str, int, float): return typ(val) return val def set_conf(name: str, value: Any): """Set the value of the given conf parameter and save to the config file""" # TODO: require new value to be of same type as default? if value: conf[name] = value else: conf.pop(name, None) os.makedirs(conf_dir(), exist_ok=True) with open(f"{conf_dir()}/projspec.json", "wt") as f: json.dump(conf, f) @contextmanager def temp_conf(**kwargs): """Temporarily set the config""" old = conf.copy() # TODO: only allow keys that exist in defaults()? conf.update(kwargs) try: yield finally: conf.clear() conf.update(old)