diff --git a/jrnl/config.py b/jrnl/config.py deleted file mode 100644 index a2f0885c..00000000 --- a/jrnl/config.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import argparse -import logging -import os -from typing import Any -from typing import Callable - -import colorama -from rich.pretty import pretty_repr -from ruamel.yaml import YAML -from ruamel.yaml import constructor - -from jrnl import __version__ -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import list_journals -from jrnl.output import print_msg -from jrnl.path import get_config_path -from jrnl.path import get_default_journal_path - -# Constants -DEFAULT_JOURNAL_KEY = "default" - -YAML_SEPARATOR = ": " -YAML_FILE_ENCODING = "utf-8" - - -def make_yaml_valid_dict(input: list) -> dict: - """ - - Convert a two-element list of configuration key-value pair into a flat dict. - - The dict is created through the yaml loader, with the assumption that - "input[0]: input[1]" is valid yaml. - - :param input: list of configuration keys in dot-notation and their respective values. - :type input: list - :return: A single level dict of the configuration keys in dot-notation and their respective desired values - :rtype: dict - """ - - assert len(input) == 2 - - # yaml compatible strings are of the form Key:Value - yamlstr = YAML_SEPARATOR.join(input) - - runtime_modifications = YAML(typ="safe").load(yamlstr) - - return runtime_modifications - - -def save_config(config: dict, alt_config_path: str | None = None) -> None: - """Supply alt_config_path if using an alternate config through --config-file.""" - config["version"] = __version__ - - yaml = YAML(typ="safe") - yaml.default_flow_style = False # prevents collapsing of tree structure - - with open( - alt_config_path if alt_config_path else get_config_path(), - "w", - encoding=YAML_FILE_ENCODING, - ) as f: - yaml.dump(config, f) - - -def get_default_config() -> dict[str, Any]: - return { - "version": __version__, - "journals": {"default": {"journal": get_default_journal_path()}}, - "editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "", - "encrypt": False, - "template": False, - "default_hour": 9, - "default_minute": 0, - "timeformat": "%F %r", - "tagsymbols": "#@", - "highlight": True, - "linewrap": 79, - "indent_character": "|", - "colors": { - "body": "none", - "date": "none", - "tags": "none", - "title": "none", - }, - } - - -def get_default_colors() -> dict[str, Any]: - return { - "body": "none", - "date": "black", - "tags": "yellow", - "title": "cyan", - } - - -def scope_config(config: dict, journal_name: str) -> dict: - if journal_name not in config["journals"]: - return config - config = config.copy() - journal_conf = config["journals"].get(journal_name) - if type(journal_conf) is dict: - # We can override the default config on a by-journal basis - logging.debug( - "Updating configuration with specific journal overrides:\n%s", - pretty_repr(journal_conf), - ) - config.update(journal_conf) - else: - # But also just give them a string to point to the journal file - config["journal"] = journal_conf - - logging.debug("Scoped config:\n%s", pretty_repr(config)) - return config - - -def verify_config_colors(config: dict) -> bool: - """ - Ensures the keys set for colors are valid colorama.Fore attributes, or "None" - :return: True if all keys are set correctly, False otherwise - """ - all_valid_colors = True - for key, color in config["colors"].items(): - upper_color = color.upper() - if upper_color == "NONE": - continue - if not getattr(colorama.Fore, upper_color, None): - print_msg( - Message( - MsgText.InvalidColor, - MsgStyle.NORMAL, - { - "key": key, - "color": color, - }, - ) - ) - all_valid_colors = False - return all_valid_colors - - -def load_config(config_path: str) -> dict: - """Tries to load a config file from YAML.""" - try: - with open(config_path, encoding=YAML_FILE_ENCODING) as f: - yaml = YAML(typ="safe") - yaml.allow_duplicate_keys = False - return yaml.load(f) - except constructor.DuplicateKeyError as e: - print_msg( - Message( - MsgText.ConfigDoubleKeys, - MsgStyle.WARNING, - { - "error_message": e, - }, - ) - ) - with open(config_path, encoding=YAML_FILE_ENCODING) as f: - yaml = YAML(typ="safe") - yaml.allow_duplicate_keys = True - return yaml.load(f) - - -def is_config_json(config_path: str) -> bool: - with open(config_path, "r", encoding="utf-8") as f: - config_file = f.read() - return config_file.strip().startswith("{") - - -def update_config( - config: dict, new_config: dict, scope: str | None, force_local: bool = False -) -> None: - """Updates a config dict with new values - either global if scope is None - or config['journals'][scope] is just a string pointing to a journal file, - or within the scope""" - if scope and type(config["journals"][scope]) is dict: # Update to journal specific - config["journals"][scope].update(new_config) - elif scope and force_local: # Convert to dict - config["journals"][scope] = {"journal": config["journals"][scope]} - config["journals"][scope].update(new_config) - else: - config.update(new_config) - - -def get_journal_name(args: argparse.Namespace, config: dict) -> argparse.Namespace: - args.journal_name = DEFAULT_JOURNAL_KEY - - # The first arg might be a journal name - if args.text: - potential_journal_name = args.text[0] - if potential_journal_name[-1] == ":": - potential_journal_name = potential_journal_name[0:-1] - - if potential_journal_name in config["journals"]: - args.journal_name = potential_journal_name - args.text = args.text[1:] - - logging.debug("Using journal name: %s", args.journal_name) - return args - - -def cmd_requires_valid_journal_name(func: Callable) -> Callable: - def wrapper(args: argparse.Namespace, config: dict, original_config: dict): - validate_journal_name(args.journal_name, config) - func(args=args, config=config, original_config=original_config) - - return wrapper - - -def validate_journal_name(journal_name: str, config: dict) -> None: - if journal_name not in config["journals"]: - raise JrnlException( - Message( - MsgText.NoNamedJournal, - MsgStyle.ERROR, - { - "journal_name": journal_name, - "journals": list_journals(config), - }, - ), - ) diff --git a/jrnl/config/BaseConfigReader.py b/jrnl/config/BaseConfigReader.py index 8c1aa3ff..3a09e5e0 100644 --- a/jrnl/config/BaseConfigReader.py +++ b/jrnl/config/BaseConfigReader.py @@ -4,15 +4,21 @@ import logging from abc import ABC from abc import abstractmethod +from typing import Any +from rich.pretty import pretty_repr class BaseConfigReader(ABC): def __init__(self): logging.debug("start") - self.config: dict = {} + self.config: dict[str, Any] = {} + + def __str__(self): + return pretty_repr(self.config) @abstractmethod def read(self): + """Needs to set self.config""" pass def get_config(self): diff --git a/jrnl/config/Config.py b/jrnl/config/Config.py index f3aabc01..ba013869 100644 --- a/jrnl/config/Config.py +++ b/jrnl/config/Config.py @@ -1,20 +1,38 @@ # Copyright © 2012-2023 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html -from collections import abc - -class Config(abc.MutableMapping): - def __init__(self, configs): - pass +import logging +from jrnl.exception import JrnlConfigException +from jrnl.exception import JrnlException +from jrnl.config.BaseConfigReader import BaseConfigReader - def add_config(config, priority): +class Config(): + def __init__(self): + self.configs: list[dict[str, list[BaseConfigReader] | bool]] = [] - def sub_configs(): - return [ - one, - two, - three, - ] - + def add_config(self, readers: list[BaseConfigReader], required: bool = False): + self.configs.append({ + "readers" : readers, + "required": required, + }) + def read(self): + for config in self.configs: + found = False + for reader in config["readers"]: + keep_going = False + + try: + reader.read() + found = True + except JrnlConfigException as e: + print(e) + keep_going = True + + if not keep_going: + break + + logging.debug(f"config read: {reader}") + if config["required"] and not found: + raise JrnlException diff --git a/jrnl/config/DefaultConfigReader.py b/jrnl/config/DefaultConfigReader.py index b2baaa81..0fd2c914 100644 --- a/jrnl/config/DefaultConfigReader.py +++ b/jrnl/config/DefaultConfigReader.py @@ -2,21 +2,36 @@ # License: https://www.gnu.org/licenses/gpl-3.0.html import logging + from .BaseConfigReader import BaseConfigReader -from pathlib import PurePath class DefaultConfigReader(BaseConfigReader): - def __init__(self, filename: str): + def __init__(self, *args, **kwargs): logging.debug("start") - super() - self.filename: PurePath = PurePath(filename) + super().__init__(*args, **kwargs) def read(self): - self._parse_args() - # do some actual reading + logging.debug("start read") + self.config = { + # TODO: Uncomment these lines - def _parse_args(self): - # read self.args - # update self.cofig somehow - pass + # "version": __version__, + # "journals": {"default": {"journal": get_default_journal_path()}}, + # "editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "", + "encrypt": False, + "template": False, + "default_hour": 9, + "default_minute": 0, + "timeformat": "%F %r", + "tagsymbols": "#@", + "highlight": True, + "linewrap": 79, + "indent_character": "|", + "colors": { + "body": "none", + "date": "none", + "tags": "none", + "title": "none", + }, + } diff --git a/jrnl/config/FileConfigReader.py b/jrnl/config/FileConfigReader.py index 7fd05c26..0d9690af 100644 --- a/jrnl/config/FileConfigReader.py +++ b/jrnl/config/FileConfigReader.py @@ -3,20 +3,51 @@ import logging from .BaseConfigReader import BaseConfigReader +from jrnl.exception import JrnlConfigException +from jrnl.path import expand_path from pathlib import PurePath +from ruamel.yaml import YAML +from ruamel.yaml import constructor + +YAML_SEPARATOR = ": " +YAML_FILE_ENCODING = "utf-8" + +def load_config(config_path: str) -> dict: + """Tries to load a config file from YAML.""" + try: + with open(config_path, encoding=YAML_FILE_ENCODING) as f: + yaml = YAML(typ="safe") + yaml.allow_duplicate_keys = False + return yaml.load(f) + except constructor.DuplicateKeyError as e: + print_msg( + Message( + MsgText.ConfigDoubleKeys, + MsgStyle.WARNING, + { + "error_message": e, + }, + ) + ) + with open(config_path, encoding=YAML_FILE_ENCODING) as f: + yaml = YAML(typ="safe") + yaml.allow_duplicate_keys = True + return yaml.load(f) class FileConfigReader(BaseConfigReader): def __init__(self, filename: str): logging.debug("start") super() - self.filename: PurePath = PurePath(filename) + self.filename: PurePath = PurePath(expand_path(filename)) def read(self): - self._parse_args() - # do some actual reading + logging.debug(f"start read for {self.filename}") - def _parse_args(self): - # read self.args - # update self.cofig somehow - pass + try: + self._raw_config_file = read_file(self.filename) + # do some tests on config file contents + # self.config = load_config(expand_path(self.filename)) + + except FileNotFoundError: + raise JrnlConfigException("File is missing") diff --git a/jrnl/config/__init__.py b/jrnl/config/__init__.py index 497304bc..c03b0603 100644 --- a/jrnl/config/__init__.py +++ b/jrnl/config/__init__.py @@ -2,35 +2,211 @@ # License: https://www.gnu.org/licenses/gpl-3.0.html from .Config import Config -from .BaseConfigReader import BaseConfigReader from .DefaultConfigReader import DefaultConfigReader from .FileConfigReader import FileConfigReader -from .ArgsConfigReader import ArgsConfigReader +from jrnl.path import get_config_path def get_config(args): config = Config() - try: - # these are in ascending priority (last one has most priority) - config.add_config([ - DefaultConfigReader(), - ]) + # these are in ascending priority (last one has most priority) + config.add_config([ + DefaultConfigReader(), + ]) + if args.config_file_path: config.add_config([ - FileConfigReader(args.config_file), - FileConfigReader(config.get_config_path()), - FileConfigReader(jrnlV1Path), + FileConfigReader(args.config_file_path), + ], required=True) + else: + config.add_config([ + FileConfigReader(get_config_path()), + FileConfigReader(os.path.join(home_dir(), ".jrnl_config")), ], required=True) - config.add_config([ - ArgsConfigReader(args.config_override), - ]) + # config.add_config([ + # ArgsConfigReader(args.config_override), + # ]) - # config.add_config(EnvConfigReader(env.whatever)) - config.validate() + # config.add_config(EnvConfigReader(env.whatever)) - except e: - # TODO: catch warnings instead of fatal exceptions + config.read() return config + + + +# --- OLD CODE HERE --- # +import argparse +import logging +from typing import Any +from typing import Callable + +import colorama +from rich.pretty import pretty_repr +from ruamel.yaml import YAML + +from jrnl import __version__ +from jrnl.exception import JrnlException +from jrnl.messages import Message +from jrnl.messages import MsgStyle +from jrnl.messages import MsgText +from jrnl.output import list_journals +from jrnl.output import print_msg +from jrnl.path import get_config_path + +# Constants +DEFAULT_JOURNAL_KEY = "default" + +def make_yaml_valid_dict(input: list) -> dict: + """ + + Convert a two-element list of configuration key-value pair into a flat dict. + + The dict is created through the yaml loader, with the assumption that + "input[0]: input[1]" is valid yaml. + + :param input: list of configuration keys in dot-notation and their respective values. + :type input: list + :return: A single level dict of the configuration keys in dot-notation and their respective desired values + :rtype: dict + """ + + assert len(input) == 2 + + # yaml compatible strings are of the form Key:Value + yamlstr = YAML_SEPARATOR.join(input) + + runtime_modifications = YAML(typ="safe").load(yamlstr) + + return runtime_modifications + + +def save_config(config: dict, alt_config_path: str | None = None) -> None: + """Supply alt_config_path if using an alternate config through --config-file.""" + config["version"] = __version__ + + yaml = YAML(typ="safe") + yaml.default_flow_style = False # prevents collapsing of tree structure + + with open( + alt_config_path if alt_config_path else get_config_path(), + "w", + encoding=YAML_FILE_ENCODING, + ) as f: + yaml.dump(config, f) + + +def get_default_colors() -> dict[str, Any]: + return { + "body": "none", + "date": "black", + "tags": "yellow", + "title": "cyan", + } + + +def scope_config(config: dict, journal_name: str) -> dict: + if journal_name not in config["journals"]: + return config + config = config.copy() + journal_conf = config["journals"].get(journal_name) + if type(journal_conf) is dict: + # We can override the default config on a by-journal basis + logging.debug( + "Updating configuration with specific journal overrides:\n%s", + pretty_repr(journal_conf), + ) + config.update(journal_conf) + else: + # But also just give them a string to point to the journal file + config["journal"] = journal_conf + + logging.debug("Scoped config:\n%s", pretty_repr(config)) + return config + + +def verify_config_colors(config: dict) -> bool: + """ + Ensures the keys set for colors are valid colorama.Fore attributes, or "None" + :return: True if all keys are set correctly, False otherwise + """ + all_valid_colors = True + for key, color in config["colors"].items(): + upper_color = color.upper() + if upper_color == "NONE": + continue + if not getattr(colorama.Fore, upper_color, None): + print_msg( + Message( + MsgText.InvalidColor, + MsgStyle.NORMAL, + { + "key": key, + "color": color, + }, + ) + ) + all_valid_colors = False + return all_valid_colors + + +def is_config_json(config_path: str) -> bool: + with open(config_path, "r", encoding="utf-8") as f: + config_file = f.read() + return config_file.strip().startswith("{") + + +def update_config( + config: dict, new_config: dict, scope: str | None, force_local: bool = False +) -> None: + """Updates a config dict with new values - either global if scope is None + or config['journals'][scope] is just a string pointing to a journal file, + or within the scope""" + if scope and type(config["journals"][scope]) is dict: # Update to journal specific + config["journals"][scope].update(new_config) + elif scope and force_local: # Convert to dict + config["journals"][scope] = {"journal": config["journals"][scope]} + config["journals"][scope].update(new_config) + else: + config.update(new_config) + + +def get_journal_name(args: argparse.Namespace, config: dict) -> argparse.Namespace: + args.journal_name = DEFAULT_JOURNAL_KEY + + # The first arg might be a journal name + if args.text: + potential_journal_name = args.text[0] + if potential_journal_name[-1] == ":": + potential_journal_name = potential_journal_name[0:-1] + + if potential_journal_name in config["journals"]: + args.journal_name = potential_journal_name + args.text = args.text[1:] + + logging.debug("Using journal name: %s", args.journal_name) + return args + + +def cmd_requires_valid_journal_name(func: Callable) -> Callable: + def wrapper(args: argparse.Namespace, config: dict, original_config: dict): + validate_journal_name(args.journal_name, config) + func(args=args, config=config, original_config=original_config) + + return wrapper + + +def validate_journal_name(journal_name: str, config: dict) -> None: + if journal_name not in config["journals"]: + raise JrnlException( + Message( + MsgText.NoNamedJournal, + MsgStyle.ERROR, + { + "journal_name": journal_name, + "journals": list_journals(config), + }, + ), + ) diff --git a/jrnl/controller.py b/jrnl/controller.py index bb7d2f7d..856fd45e 100644 --- a/jrnl/controller.py +++ b/jrnl/controller.py @@ -5,14 +5,12 @@ import logging import sys from typing import TYPE_CHECKING -from jrnl import install from jrnl import plugins from jrnl import time from jrnl.config import get_config from jrnl.config import DEFAULT_JOURNAL_KEY from jrnl.config import get_config_path from jrnl.config import get_journal_name -from jrnl.config import scope_config from jrnl.editor import get_text_from_editor from jrnl.editor import get_text_from_stdin from jrnl.editor import read_template_file @@ -23,7 +21,6 @@ from jrnl.messages import MsgStyle from jrnl.messages import MsgText from jrnl.output import print_msg from jrnl.output import print_msgs -from jrnl.override import apply_overrides if TYPE_CHECKING: from argparse import Namespace @@ -50,8 +47,7 @@ def run(args: "Namespace"): config = get_config(args) - if config.needs_upgrade(): - upgrade.run_upgrade(config) + raise JrnlException # old code diff --git a/jrnl/exception.py b/jrnl/exception.py index 87489821..6c1b7f3f 100644 --- a/jrnl/exception.py +++ b/jrnl/exception.py @@ -22,3 +22,10 @@ class JrnlException(Exception): def has_message_text(self, message_text: "MsgText"): return any([m.text == message_text for m in self.messages]) + + +class JrnlConfigException(JrnlException): + """For catching something something""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs)