mirror of
https://github.com/dtomlinson91/panaetius.git
synced 2025-12-22 04:55:44 +00:00
Merge branch 'feature/skip_header_in_config_init' into develop
This commit is contained in:
7
.coveragerc
Normal file
7
.coveragerc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
# Have to re-enable the standard pragma
|
||||||
|
pragma: no cover
|
||||||
|
|
||||||
|
# Don't complain if tests don't hit defensive assertion code:
|
||||||
|
raise NotImplementedError
|
||||||
@@ -15,31 +15,41 @@ from typing import Any
|
|||||||
# import toml
|
# import toml
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from panaetius.exceptions import KeyErrorTooDeepException, InvalidPythonException
|
from panaetius.exceptions import KeyErrorTooDeepException
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""The configuration class to access variables."""
|
"""The configuration class to access variables."""
|
||||||
|
|
||||||
def __init__(self, header_variable: str, config_path: str = "") -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
header_variable: str,
|
||||||
|
config_path: str | None = None,
|
||||||
|
skip_header_init: bool = False,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Create a Config object to set and access variables.
|
Create a Config object to set and access variables.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
header_variable (str): Your header variable name.
|
header_variable (str): Your header variable name.
|
||||||
config_path (str, optional): The path where the header directory is stored.
|
config_path (str, optional): The path where the header directory is stored.
|
||||||
Defaults to `~/.config`.
|
Defaults to None on initialisation.
|
||||||
|
skip_header_init (bool, optional): If True will not use a header
|
||||||
|
subdirectory in the `config_path`. Defaults to False.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
`config_path` defaults to None on initialisation but will be set to `~/.config`.
|
||||||
|
|
||||||
Example:
|
|
||||||
A header of `data_analysis` with a config_path of `~/myapps` will define
|
A header of `data_analysis` with a config_path of `~/myapps` will define
|
||||||
a config file in `~/myapps/data_analysis/config.yml`.
|
a config file in `~/myapps/data_analysis/config.yml`.
|
||||||
"""
|
"""
|
||||||
self.header_variable = header_variable
|
self.header_variable = header_variable
|
||||||
self.config_path = (
|
self.config_path = (
|
||||||
pathlib.Path(config_path).expanduser()
|
pathlib.Path(config_path).expanduser()
|
||||||
if config_path
|
if config_path is not None
|
||||||
else pathlib.Path.home() / ".config"
|
else pathlib.Path.home() / ".config"
|
||||||
)
|
)
|
||||||
|
self.skip_header_init = skip_header_init
|
||||||
self._missing_config = self._check_config_file_exists()
|
self._missing_config = self._check_config_file_exists()
|
||||||
|
|
||||||
# default logging options
|
# default logging options
|
||||||
@@ -55,7 +65,12 @@ class Config:
|
|||||||
Returns:
|
Returns:
|
||||||
dict: The contents of the `.yml` loaded as a python dictionary.
|
dict: The contents of the `.yml` loaded as a python dictionary.
|
||||||
"""
|
"""
|
||||||
config_file_location = self.config_path / self.header_variable / "config.yml"
|
if self.skip_header_init:
|
||||||
|
config_file_location = self.config_path / "config.yml"
|
||||||
|
else:
|
||||||
|
config_file_location = (
|
||||||
|
self.config_path / self.header_variable / "config.yml"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
with open(config_file_location, "r", encoding="utf-8") as config_file:
|
with open(config_file_location, "r", encoding="utf-8") as config_file:
|
||||||
# return dict(toml.load(config_file))
|
# return dict(toml.load(config_file))
|
||||||
@@ -103,7 +118,10 @@ class Config:
|
|||||||
return self._get_env_value(env_key, default)
|
return self._get_env_value(env_key, default)
|
||||||
|
|
||||||
def _check_config_file_exists(self) -> bool:
|
def _check_config_file_exists(self) -> bool:
|
||||||
config_file_location = self.config_path / self.header_variable / "config.yml"
|
if self.skip_header_init is False:
|
||||||
|
config_file_location = self.config_path / self.header_variable / "config.yml"
|
||||||
|
else:
|
||||||
|
config_file_location = self.config_path / "config.yml"
|
||||||
try:
|
try:
|
||||||
with open(config_file_location, "r", encoding="utf-8"):
|
with open(config_file_location, "r", encoding="utf-8"):
|
||||||
return False
|
return False
|
||||||
@@ -165,7 +183,8 @@ class Config:
|
|||||||
try:
|
try:
|
||||||
return ast.literal_eval(value)
|
return ast.literal_eval(value)
|
||||||
except (ValueError, SyntaxError):
|
except (ValueError, SyntaxError):
|
||||||
raise InvalidPythonException(f"{value} is not valid Python.") # noqa
|
# string without spaces: ValueError, with spaces; SyntaxError
|
||||||
|
return value
|
||||||
|
|
||||||
def __load_default_value(self, default: Any) -> Any: # noqa
|
def __load_default_value(self, default: Any) -> Any: # noqa
|
||||||
return default
|
return default
|
||||||
|
|||||||
@@ -98,11 +98,11 @@ class LoggingData(metaclass=ABCMeta):
|
|||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def format(self) -> str:
|
def format(self) -> str:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __init__(self, logging_level: str):
|
def __init__(self, logging_level: str):
|
||||||
self.logging_level = logging_level
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class SimpleLogger(LoggingData):
|
class SimpleLogger(LoggingData):
|
||||||
|
|||||||
14
rewrite.todo
14
rewrite.todo
@@ -1,16 +1,26 @@
|
|||||||
|
Testing:
|
||||||
|
To Write:
|
||||||
|
☐ Test the Config file skipping header with `skip_header_init`
|
||||||
|
☐ Document coverage commands
|
||||||
|
`coverage run --source=./panaetius -m pytest`
|
||||||
|
`coverage report` & `coverage html` > gives ./htmlcov/index.html
|
||||||
|
☐ Document for abstract methods should raise NotImplementedError
|
||||||
|
☐ Document https://stackoverflow.com/a/9212387
|
||||||
Documentation:
|
Documentation:
|
||||||
☐ Rewrite documentation using `mkdocs` and using `.md`.
|
☐ Rewrite documentation using `mkdocs` and using `.md`.
|
||||||
☐ Update the metadata in the `pyproject.toml`.
|
☐ Update the metadata in the `pyproject.toml`.
|
||||||
☐ Create a new `Readme.md` and remove the `.rst`.
|
☐ Create a new `Readme.md` and remove the `.rst`.
|
||||||
|
☐ Document the logging strategy
|
||||||
|
CLI tools should use `logger.critical` and raise SystemExit(1)
|
||||||
|
Libraries should raise custom errors and have a `logger.critical(exec_info=1)`
|
||||||
|
|
||||||
Misc:
|
Misc:
|
||||||
☐ Use the python runner to build the docs & run the tests (including coverage html)
|
☐ Use the python runner to build the docs & run the tests (including coverage html)
|
||||||
coverage run -m pytest && coverage report && coverage html
|
coverage run -m pytest && coverage report && coverage html
|
||||||
☐ document this in trilium
|
☐ document this in trilium
|
||||||
☐ Bump the version to release 2.0
|
|
||||||
|
|
||||||
|
|
||||||
Archive:
|
Archive:
|
||||||
|
✘ Bump the version to release 2.0 @cancelled(21-10-23 05:36) @project(Misc)
|
||||||
✔ Handle if a bool is passed in as a default @done(21-10-16 05:25) @project(Coding.No Config File)
|
✔ Handle if a bool is passed in as a default @done(21-10-16 05:25) @project(Coding.No Config File)
|
||||||
✔ Handle if a bool is passed in as a default @done(21-10-16 05:25) @project(Coding.Config File)
|
✔ Handle if a bool is passed in as a default @done(21-10-16 05:25) @project(Coding.Config File)
|
||||||
✔ Create SimpleLogger, AdvancedLogger, CustomLogger classes @done(21-10-16 16:22) @project(Coding.Logging)
|
✔ Create SimpleLogger, AdvancedLogger, CustomLogger classes @done(21-10-16 16:22) @project(Coding.Logging)
|
||||||
|
|||||||
9
tests/data/without_header/config.yml
Normal file
9
tests/data/without_header/config.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
panaetius_testing:
|
||||||
|
some_top_string: some_top_value
|
||||||
|
second:
|
||||||
|
some_second_string: some_second_value
|
||||||
|
some_second_int: 1
|
||||||
|
some_second_float: 1.0
|
||||||
|
some_second_list: ["some", "second", "value"]
|
||||||
|
some_second_table: { "first": ["some", "first", "value"] }
|
||||||
|
some_second_table_bools: { "bool": [true, false] }
|
||||||
@@ -29,6 +29,17 @@ def test_user_config_path_set(header, shared_datadir):
|
|||||||
assert str(config.config_path) == config_path
|
assert str(config.config_path) == config_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_config_path_without_header_dir_set(header, shared_datadir):
|
||||||
|
# arrange
|
||||||
|
config_path = str(shared_datadir / "without_header")
|
||||||
|
|
||||||
|
# act
|
||||||
|
config = panaetius.Config(header, config_path, skip_header_init=True)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert str(config.config_path) == config_path
|
||||||
|
|
||||||
|
|
||||||
# test config files
|
# test config files
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +55,18 @@ def test_config_file_exists(header, shared_datadir):
|
|||||||
assert config._missing_config is False
|
assert config._missing_config is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_file_without_header_dir_exists(header, shared_datadir):
|
||||||
|
# arrange
|
||||||
|
config_path = str(shared_datadir / "without_header")
|
||||||
|
|
||||||
|
# act
|
||||||
|
config = panaetius.Config(header, config_path, skip_header_init=True)
|
||||||
|
_ = config.config
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert config._missing_config is False
|
||||||
|
|
||||||
|
|
||||||
def test_config_file_contents_read_success(
|
def test_config_file_contents_read_success(
|
||||||
header, shared_datadir, testing_config_contents
|
header, shared_datadir, testing_config_contents
|
||||||
):
|
):
|
||||||
@@ -106,7 +129,7 @@ def test_get_value_from_key(
|
|||||||
|
|
||||||
def test_get_value_environment_var_override(header, shared_datadir):
|
def test_get_value_environment_var_override(header, shared_datadir):
|
||||||
# arrange
|
# arrange
|
||||||
os.environ[f"{header.upper()}_SOME_TOP_STRING"] = '"some_overridden_value"'
|
os.environ[f"{header.upper()}_SOME_TOP_STRING"] = "some_overridden_value"
|
||||||
config_path = str(shared_datadir / "without_logging")
|
config_path = str(shared_datadir / "without_logging")
|
||||||
config = panaetius.Config(header, config_path)
|
config = panaetius.Config(header, config_path)
|
||||||
panaetius.set_config(config, "some_top_string")
|
panaetius.set_config(config, "some_top_string")
|
||||||
@@ -158,7 +181,7 @@ def test_get_value_missing_key_from_default(header, shared_datadir):
|
|||||||
|
|
||||||
def test_get_value_missing_key_from_env(header, shared_datadir):
|
def test_get_value_missing_key_from_env(header, shared_datadir):
|
||||||
# arrange
|
# arrange
|
||||||
os.environ[f"{header.upper()}_MISSING_KEY"] = '"some missing key"'
|
os.environ[f"{header.upper()}_MISSING_KEY"] = "some missing key"
|
||||||
|
|
||||||
config_path = str(shared_datadir / "without_logging")
|
config_path = str(shared_datadir / "without_logging")
|
||||||
config = panaetius.Config(header, config_path)
|
config = panaetius.Config(header, config_path)
|
||||||
@@ -205,7 +228,7 @@ def test_missing_config_read_from_default(header, shared_datadir):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"env_value,expected_value",
|
"env_value,expected_value",
|
||||||
[
|
[
|
||||||
('"a missing string"', "a missing string"),
|
("a missing string", "a missing string"),
|
||||||
("1", 1),
|
("1", 1),
|
||||||
("1.0", 1.0),
|
("1.0", 1.0),
|
||||||
("True", True),
|
("True", True),
|
||||||
@@ -237,6 +260,7 @@ def test_missing_config_read_from_env_var(
|
|||||||
del os.environ[f"{header.upper()}_MISSING_KEY_READ_FROM_ENV_VAR"]
|
del os.environ[f"{header.upper()}_MISSING_KEY_READ_FROM_ENV_VAR"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="No longer needed as strings are loaded without quotes")
|
||||||
def test_missing_config_read_from_env_var_invalid_python(header):
|
def test_missing_config_read_from_env_var_invalid_python(header):
|
||||||
# arrange
|
# arrange
|
||||||
os.environ[f"{header.upper()}_INVALID_PYTHON"] = "a string without quotes"
|
os.environ[f"{header.upper()}_INVALID_PYTHON"] = "a string without quotes"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ def test_logging_directory_does_not_exist(header, shared_datadir):
|
|||||||
assert str(logging_exception.value) == ""
|
assert str(logging_exception.value) == ""
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: change this test so it asserts the dir exists
|
||||||
def test_logging_directory_does_exist(header, shared_datadir):
|
def test_logging_directory_does_exist(header, shared_datadir):
|
||||||
# arrange
|
# arrange
|
||||||
config = Config(header)
|
config = Config(header)
|
||||||
@@ -32,3 +33,5 @@ def test_logging_directory_does_exist(header, shared_datadir):
|
|||||||
|
|
||||||
# assert
|
# assert
|
||||||
assert isinstance(logger, logging.Logger)
|
assert isinstance(logger, logging.Logger)
|
||||||
|
|
||||||
|
# TODO: add tests to check that SimpleLogger, AdvancedLogger, CustomLogger work as intended
|
||||||
|
|||||||
Reference in New Issue
Block a user