summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJonathan Wren <jonathan@nowandwren.com>2023-07-01 17:14:45 -0700
committerJonathan Wren <jonathan@nowandwren.com>2023-07-01 23:58:40 -0700
commit3f3354b7c078b0f43c01c877d1a53bd39ccf5c1b (patch)
treea49f01aeea51a00992b122680472993f24479390
parent2543260a4111faa1599631dd5d462db5ed85abc0 (diff)
keep working on new config handlingconfig-refactor-1102
-rw-r--r--jrnl/config.py228
-rw-r--r--jrnl/config/BaseConfigReader.py8
-rw-r--r--jrnl/config/Config.py42
-rw-r--r--jrnl/config/DefaultConfigReader.py35
-rw-r--r--jrnl/config/FileConfigReader.py45
-rw-r--r--jrnl/config/__init__.py210
-rw-r--r--jrnl/controller.py6
-rw-r--r--jrnl/exception.py7
8 files changed, 301 insertions, 280 deletions
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
+import logging
+from jrnl.exception import JrnlConfigException
+from jrnl.exception import JrnlException
+from jrnl.config.BaseConfigReader import BaseConfigReader
-class Config(abc.MutableMapping):
- def __init__(self, configs):
- pass
+class Config():
+ def __init__(self):
+ self.configs: list[dict[str, list[BaseConfigReader] | bool]] = []
- def add_config(config, priority):
+ def add_config(self, readers: list[BaseConfigReader], required: bool = False):
+ self.configs.append({
+ "readers" : readers,
+ "required": required,
+ })
- def sub_configs():
- return [
- one,
- two,
- three,
- ]
-
+ 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([
- ArgsConfigReader(args.config_override),
- ])
+ FileConfigReader(get_config_path()),
+ FileConfigReader(os.path.join(home_dir(), ".jrnl_config")),
+ ], required=True)
+
+ # config.add_config([
+ # ArgsConfigReader(args.config_override),
+ # ])
+
+ # config.add_config(EnvConfigReader(env.whatever))
+
+ 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
- # config.add_config(EnvConfigReader(env.whatever))
- config.validate()
- except e:
- # TODO: catch warnings instead of fatal exceptions
+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)