byoconfig 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.1
2
+ Name: byoconfig
3
+ Version: 0.0.1
4
+ Summary: A configuration class that supports plugins, multiple file formats, heirarchical configuration, and more.
5
+ Author-email: Cam Ratchford <camratchford@gmail.com>
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Programming Language :: Python
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Information Technology
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Environment :: Win32 (MS Windows)
13
+ Classifier: Environment :: MacOS X
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.8
16
+ Requires-Dist: PyYAML==6.0.2
17
+ Requires-Dist: toml==0.10.2
18
+ Provides-Extra: development
19
+ Requires-Dist: mkdocs; extra == "development"
20
+ Requires-Dist: mkdocs-material; extra == "development"
21
+ Requires-Dist: ruff; extra == "development"
22
+ Requires-Dist: pytest; extra == "development"
23
+ Requires-Dist: build; extra == "development"
24
+ Requires-Dist: twine; extra == "development"
File without changes
@@ -0,0 +1,4 @@
1
+ from byo_config.sources.file import FileVariableSource
2
+ from .config import Config
3
+
4
+ __all__ = ['FileVariableSource', 'Config']
@@ -0,0 +1,112 @@
1
+ import logging
2
+ import inspect
3
+ from typing import Optional, Type
4
+
5
+ from byo_config.sources import (
6
+ BaseVariableSource,
7
+ FileVariableSource,
8
+ EnvVariableSource,
9
+ FileTypes,
10
+ )
11
+ from byo_config.error import BYOConfigError
12
+
13
+ __all__ = ['Config']
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Config(FileVariableSource, EnvVariableSource):
19
+ """
20
+ A versatile config object that can parse many file types, load environment variables,
21
+ and load dictionary keys as class attributes.
22
+ - Multiple config objects from different unique sources can be collated easily with a precedence value.
23
+ - Config instances with higher precedence have their values overwrite the values of lower precedence
24
+ instances when merging objects.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ source_file_path: Optional[str] = None,
30
+ forced_file_type: Optional[FileTypes] = None,
31
+ env_prefix: Optional[str] = None,
32
+ var_source_name: Optional[str] = "Config",
33
+ precedence: Optional[int] = 1,
34
+ **kwargs
35
+ ):
36
+ """
37
+ Initialize a Config object.
38
+
39
+ Args:
40
+ source_file_path (str):
41
+ The path to the source file.
42
+
43
+ forced_file_type (FileTypes):
44
+ The file type of the source file, if you don't want to use the file's extension.
45
+
46
+ var_source_name (str):
47
+ The name of the variable source.
48
+
49
+ env_prefix (str):
50
+ The configuration keys will be loaded from the environment variables with this prefix.
51
+ Use the "*" wildcard if you want to load all environment variables as configuration keys.
52
+
53
+ precedence (int):
54
+ The precedence of the variable source.
55
+
56
+ **kwargs:
57
+ Arbitrary keyword arguments, to be loaded as class attributes.
58
+ """
59
+ try:
60
+ self.precedence = precedence
61
+ self.var_source_name = var_source_name
62
+ super().__init__(
63
+ source_file=source_file_path,
64
+ forced_file_type=forced_file_type
65
+ )
66
+ super().load_env(env_prefix)
67
+
68
+ self.set_data(kwargs)
69
+ logger.debug(f"Config object {self.var_source_name} created with precedence {self.precedence}")
70
+
71
+ except BYOConfigError as e:
72
+ raise e
73
+
74
+ except FileNotFoundError as e:
75
+ raise e
76
+
77
+ except ValueError as e:
78
+ raise e
79
+
80
+ except Exception as e:
81
+ raise e
82
+
83
+ def include(self, plugin_class: Type[BaseVariableSource], **kwargs):
84
+ """
85
+ Include a plugin class in the config object.
86
+
87
+ Args:
88
+ plugin_class (Type[BaseVariableSource]):
89
+ The plugin class to include in the config object.
90
+
91
+ **kwargs:
92
+ Arbitrary keyword arguments to pass to the plugin class.
93
+ """
94
+ try:
95
+ # get signature of plugin class
96
+ sig = inspect.signature(plugin_class)
97
+ # Compare kwargs to signature
98
+ for k, v in kwargs.items():
99
+ if k not in sig.parameters:
100
+ raise BYOConfigError(
101
+ f"Invalid parameter '{k}' for plugin class '{plugin_class.__name__}'",
102
+ self
103
+ )
104
+ plugin = plugin_class(**kwargs) # type: ignore
105
+ self.set_data(plugin.get_data())
106
+ logger.debug(f"Initialized plugin '{plugin_class.__name__}' with data: {plugin.get_data()}")
107
+
108
+ except BYOConfigError as e:
109
+ raise e
110
+
111
+ except Exception as e:
112
+ raise e
@@ -0,0 +1,19 @@
1
+
2
+ __all__ = ['BYOConfigError']
3
+
4
+
5
+ class BYOConfigError(ValueError):
6
+ """Subclass of ValueError with the following additional properties:
7
+ args:
8
+ msg: The unformatted error message
9
+ instance_name: The name of which VariableSource instance raised the error
10
+ """
11
+ def __init__(self, msg, instance):
12
+ errmsg = f"{msg} in VariableSource instance '{instance.var_source_name}'"
13
+ super().__init__(self, errmsg)
14
+ self.msg = msg
15
+ self.instance = instance.var_source_name
16
+
17
+ def __reduce__(self):
18
+ return self.__class__, (self.msg, self.instance)
19
+
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.1
2
+ Name: byoconfig
3
+ Version: 0.0.1
4
+ Summary: A configuration class that supports plugins, multiple file formats, heirarchical configuration, and more.
5
+ Author-email: Cam Ratchford <camratchford@gmail.com>
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Programming Language :: Python
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Information Technology
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Environment :: Win32 (MS Windows)
13
+ Classifier: Environment :: MacOS X
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.8
16
+ Requires-Dist: PyYAML==6.0.2
17
+ Requires-Dist: toml==0.10.2
18
+ Provides-Extra: development
19
+ Requires-Dist: mkdocs; extra == "development"
20
+ Requires-Dist: mkdocs-material; extra == "development"
21
+ Requires-Dist: ruff; extra == "development"
22
+ Requires-Dist: pytest; extra == "development"
23
+ Requires-Dist: build; extra == "development"
24
+ Requires-Dist: twine; extra == "development"
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ byo_config/__init__.py
4
+ byo_config/config.py
5
+ byo_config/error.py
6
+ byoconfig.egg-info/PKG-INFO
7
+ byoconfig.egg-info/SOURCES.txt
8
+ byoconfig.egg-info/dependency_links.txt
9
+ byoconfig.egg-info/requires.txt
10
+ byoconfig.egg-info/top_level.txt
11
+ tests/test_config.py
12
+ tests/test_error.py
13
+ tests/test_sources_base.py
14
+ tests/test_sources_env.py
15
+ tests/test_sources_file.py
@@ -0,0 +1,10 @@
1
+ PyYAML==6.0.2
2
+ toml==0.10.2
3
+
4
+ [development]
5
+ mkdocs
6
+ mkdocs-material
7
+ ruff
8
+ pytest
9
+ build
10
+ twine
@@ -0,0 +1 @@
1
+ byo_config
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["setuptools", "setuptools-scm"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools]
6
+ packages = ["byo_config"]
7
+
8
+ [project]
9
+ name = 'byoconfig'
10
+ description = 'A configuration class that supports plugins, multiple file formats, heirarchical configuration, and more.'
11
+ authors = [
12
+ {name = 'Cam Ratchford', email = 'camratchford@gmail.com'},
13
+ ]
14
+
15
+ classifiers = [
16
+ 'Development Status :: 3 - Alpha',
17
+ 'Programming Language :: Python',
18
+ 'Programming Language :: Python :: 3 :: Only',
19
+ 'Intended Audience :: Developers',
20
+ 'Intended Audience :: Information Technology',
21
+ 'Operating System :: POSIX :: Linux',
22
+ 'Environment :: Win32 (MS Windows)',
23
+ 'Environment :: MacOS X',
24
+ 'Topic :: Software Development :: Libraries :: Python Modules',
25
+ ]
26
+ version = "0.0.1"
27
+ requires-python = '>=3.8'
28
+ dependencies = [
29
+ "PyYAML==6.0.2",
30
+ "toml==0.10.2",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ development = [
35
+ "mkdocs",
36
+ "mkdocs-material",
37
+ "ruff",
38
+ "pytest",
39
+ "build",
40
+ "twine"
41
+ ]
42
+
43
+ [tool.ruff]
44
+ exclude = [
45
+ "tests/fixtures/**/*"
46
+ ]
47
+
48
+ [tool.ruff.lint]
49
+ select = ["E4", "E7", "E9", "F"]
50
+ ignore = []
51
+ fixable = ["ALL"]
52
+ unfixable = []
53
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
54
+
55
+ [tool.ruff.format]
56
+ quote-style = "double"
57
+ indent-style = "space"
58
+ skip-magic-trailing-comma = false
59
+ line-ending = "auto"
60
+ docstring-code-format = false
61
+ docstring-code-line-length = "dynamic"
62
+
63
+ [tool.pytest.ini_options]
64
+ addopts = "-ra -q"
65
+ testpaths = [
66
+ "tests",
67
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,56 @@
1
+ from os import environ
2
+ from fixtures.pathing import example_configs
3
+ from fixtures.fixture_source_classes import PluginVarSource
4
+
5
+ from byo_config.config import Config
6
+
7
+
8
+ def test_file_var_source_functionality():
9
+ example_dict = {
10
+ "parent": {
11
+ "some": "thing",
12
+ "child": {
13
+ "other": "thing"
14
+ }
15
+ }
16
+ }
17
+
18
+ yaml_file = str(example_configs / 'same_as.yaml')
19
+ yaml_source = Config(yaml_file)
20
+ assert yaml_source.data == example_dict
21
+
22
+ toml_file = str(example_configs / 'same_as.toml')
23
+ toml_source = Config(toml_file)
24
+ assert toml_source.data == example_dict
25
+
26
+ json_file = str(example_configs / 'same_as.json')
27
+ json_source = Config(json_file)
28
+ assert json_source.data == example_dict
29
+
30
+
31
+ def test_env_var_source_functionality():
32
+ env_prefix = 'BYO_CONFIG_TEST_'
33
+ env_var = 'BYO_CONFIG_TEST_ENV_VAR'
34
+ env_val = 'test_value'
35
+
36
+ env_dict = {env_var: env_val}
37
+ environ.update(env_dict)
38
+
39
+ env_source = Config(env_prefix=env_prefix)
40
+ assert env_source.data.get("ENV_VAR") == environ.get(env_var)
41
+
42
+
43
+ def test_loading_plugins_and_kwargs():
44
+
45
+ config = Config(
46
+ precedence=0,
47
+ test_var1='will_be_overwritten',
48
+ test_var2='will_be_overwritten',
49
+ test_var3='unique to config'
50
+ )
51
+ kwarg_str = 'proof that we can pass plugins kwargs'
52
+ config.include(PluginVarSource, plugin_kwarg=kwarg_str)
53
+ assert config.data['test_var1'] == 'from plugin #1'
54
+ assert config.data['test_var2'] == 'from plugin #2'
55
+ assert config.data['test_var3'] == 'unique to config'
56
+ assert config.data['plugin_kwarg'] == kwarg_str
File without changes
@@ -0,0 +1,96 @@
1
+
2
+ import pytest
3
+
4
+ from fixtures.fixture_source_classes import NameSource
5
+ from byo_config.error import BYOConfigError
6
+
7
+
8
+ def test_base_source():
9
+ source = NameSource('test')
10
+ assert source.data == {'name': 'test', 'should_appear': 'should_appear'}
11
+ assert source.var_source_name == 'NameSource'
12
+ assert source.precedence == 1
13
+ assert str(source) == 'NameSource: NameSource [1]'
14
+ assert repr(source) == 'NameSource: NameSource [1]'
15
+ source_keys = [k for k in source.data.keys()]
16
+ assert 'precedence' not in source_keys
17
+ assert 'var_source_name' not in source_keys
18
+ assert 'metadata' not in source_keys
19
+ assert '_should_not_appear' not in source_keys
20
+ assert 'should_appear' in source_keys
21
+
22
+
23
+ def test_precedence_and_add():
24
+ source_a = NameSource('a', "source_a", 1)
25
+ source_b = NameSource('b', "source_b", 2)
26
+ source_c = NameSource('c', "source_c", 1)
27
+ source_d = NameSource('d', "source_d", 2)
28
+
29
+ # Test __add__ method
30
+ source_a = source_a + source_b
31
+ assert source_a.data == {'name': 'b', 'should_appear': 'should_appear'}
32
+
33
+ # Test all other magic methods that use precedence
34
+ assert int(source_a) == 1
35
+ assert source_a < source_b
36
+ assert source_a <= source_b
37
+ assert source_a <= source_c
38
+ assert source_b > source_a
39
+ assert source_b >= source_a
40
+ assert source_b >= source_d
41
+ assert source_a != source_b
42
+ assert source_a == source_c
43
+
44
+ with pytest.raises(BYOConfigError) as exec_info_precedence:
45
+ source_a += source_c
46
+ assert "as they have the same precedence." in str(exec_info_precedence.value)
47
+
48
+ source_c.precedence = None
49
+ with pytest.raises(BYOConfigError) as exec_info_no_precedence:
50
+ source_a += source_c
51
+ assert "as one or both instances have no precedence value." in str(exec_info_no_precedence.value)
52
+
53
+ # Test that private attributes are not copied over
54
+ source_b._should_not_appear = "shouldn't be copied to other source"
55
+ source_a += source_b
56
+ assert source_a.data == {'name': 'b', 'should_appear': 'should_appear'}
57
+ assert source_a._should_not_appear != source_b._should_not_appear
58
+
59
+
60
+ def test_name_conflict():
61
+ source_a = NameSource('a', "source_a", 1)
62
+ source_b = NameSource('b', "source_a", 2)
63
+ with pytest.raises(BYOConfigError) as exec_info_name:
64
+ source_a += source_b
65
+ assert "as they share the same name." in str(exec_info_name.value)
66
+
67
+
68
+ def test_set_data():
69
+ source_a = NameSource('a', "source_a", 1)
70
+ source_a.data = {'name': 'b'}
71
+ assert source_a.data == {'name': 'b', 'should_appear': 'should_appear'}
72
+
73
+ # Source should be unchanged if data is set to None
74
+ source_a.data = None
75
+ assert source_a.data == {'name': 'b', 'should_appear': 'should_appear'}
76
+
77
+ # Source should be unchanged if data is set to an empty dict
78
+ source_a.data = {}
79
+ assert source_a.data == {'name': 'b', 'should_appear': 'should_appear'}
80
+
81
+ # Test clear_data method with no args
82
+ source_a.clear_data()
83
+ assert source_a.data == {}
84
+
85
+ # Test clear_data method with args
86
+ source_a.data = {'name': 'b', 'age': 30}
87
+ source_a.clear_data('name', 'should_appear')
88
+ assert source_a.data == {'age': 30}
89
+
90
+ # Test clear_data method with non-hashable args
91
+ with pytest.raises(BYOConfigError) as exec_info:
92
+ source_a.clear_data(['name'])
93
+ assert "not hashable types." in str(exec_info.value)
94
+
95
+
96
+
@@ -0,0 +1,62 @@
1
+ import platform
2
+ from os import environ
3
+
4
+ import pytest
5
+
6
+ from byo_config.error import BYOConfigError
7
+
8
+ from fixtures.fixture_source_classes import GenericEnvSource
9
+
10
+
11
+ def test_error_modes():
12
+ with pytest.raises(BYOConfigError) as exec_info_error:
13
+ GenericEnvSource(111)
14
+ assert "env_prefix must be a string" in str(exec_info_error.value)
15
+
16
+ with pytest.raises(BYOConfigError) as exec_info_error:
17
+ GenericEnvSource("illegal prefix")
18
+ assert "env_prefix must be a valid environment variable name" in str(exec_info_error.value)
19
+
20
+
21
+ def load_env():
22
+ environ.update({
23
+ "BYO_CONFIG_TEST_VAR1": "value1",
24
+ "BYO_CONFIG_TEST_VAR2": "value2",
25
+ "BYO_CONFIG_TEST_var3": "value3", # The case should be ignored on windows, converted to uppercase
26
+ })
27
+
28
+
29
+ def test_load_env():
30
+
31
+ prefix = "BYO_CONFIG_TEST_"
32
+ load_env()
33
+ env_source_1 = GenericEnvSource(prefix)
34
+ # Test that it accepts either _ or no _ at the end of the prefix
35
+ env_source_2 = GenericEnvSource(prefix[:-1])
36
+ env_data = {
37
+ "VAR1": "value1",
38
+ "VAR2": "value2",
39
+ "var3": "value3",
40
+ }
41
+ # Handle windows env var case insensitivity
42
+ if platform.system() == "Windows":
43
+ del env_data["var3"]
44
+ env_data["VAR3"] = "value3"
45
+
46
+ assert env_source_1.data == env_data
47
+ assert env_source_2.data == env_data
48
+
49
+ # Use the wildcard to load all environment variables
50
+ env_source_3 = GenericEnvSource("*")
51
+ # Due to slight difference between the two dictionaries, we will compare a subset of values
52
+ assert env_source_3.data['COLUMNS'] == environ.get('COLUMNS')
53
+ if platform.system() == "Windows":
54
+ assert env_source_3.data['HOMEDRIVE'] == environ.get('HOMEDRIVE')
55
+ assert env_source_3.data['HOMEPATH'] == environ.get('HOMEPATH')
56
+ else:
57
+ assert env_source_3.data['HOME'] == environ.get('HOME')
58
+ assert env_source_3.data['USER'] == environ.get('USER')
59
+
60
+ # Test if using None as the prefix does not result in errors
61
+ env_source_4 = GenericEnvSource(None)
62
+ assert env_source_4.data == {}
@@ -0,0 +1,112 @@
1
+ import pytest
2
+
3
+ from byo_config.error import BYOConfigError
4
+
5
+ from fixtures.fixture_source_classes import GenericFileSource
6
+ from fixtures.pathing import example_configs, fixtures_dir, output_dir
7
+
8
+
9
+ def test_error_modes():
10
+ # Test that the error modes are working as expected
11
+ with pytest.raises(FileNotFoundError) as exec_info_file_not_found:
12
+ GenericFileSource('non_existent_file', forced_file_type=None)
13
+ assert "Config file non_existent_file does not exist" in str(exec_info_file_not_found.value)
14
+
15
+ python_file = str(fixtures_dir / 'fixture_source_classes.py')
16
+ with pytest.raises(BYOConfigError) as exec_info_error:
17
+ GenericFileSource(python_file, forced_file_type=None)
18
+ assert "does not posses one of the allowed file extensions" in str(exec_info_error.value)
19
+
20
+ invalid_json = str(fixtures_dir / 'invalid.json')
21
+ with pytest.raises(BYOConfigError) as exec_info_error:
22
+ GenericFileSource(invalid_json, forced_file_type=None)
23
+ assert "Expecting property name enclosed in double quotes " in str(exec_info_error.value)
24
+
25
+ invalid_toml = str(fixtures_dir / 'invalid.toml')
26
+ with pytest.raises(BYOConfigError) as exec_info_error:
27
+ GenericFileSource(invalid_toml, forced_file_type=None)
28
+ assert "Invalid group name '{this is invalid}'." in str(exec_info_error.value)
29
+
30
+ invalid_yaml = str(fixtures_dir / 'invalid.yaml')
31
+ with pytest.raises(BYOConfigError) as exec_info_error:
32
+ GenericFileSource(invalid_yaml, forced_file_type=None)
33
+ # todo: figure out how to get yaml error message
34
+
35
+
36
+ def test_load_file_modes():
37
+ example_dict = {
38
+ "parent": {
39
+ "some": "thing",
40
+ "child": {
41
+ "other": "thing"
42
+ }
43
+ }
44
+ }
45
+
46
+ yaml_file = str(example_configs / 'same_as.yaml')
47
+ yaml_source = GenericFileSource(yaml_file)
48
+ assert yaml_source.data == example_dict
49
+
50
+ toml_file = str(example_configs / 'same_as.toml')
51
+ toml_source = GenericFileSource(toml_file)
52
+ assert toml_source.data == example_dict
53
+
54
+ json_file = str(example_configs / 'same_as.json')
55
+ json_source = GenericFileSource(json_file)
56
+ assert json_source.data == example_dict
57
+
58
+
59
+ def compare_file_contents(file1, file2):
60
+ with open(file1, 'r') as f1:
61
+ with open(file2, 'r') as f2:
62
+ return f1.read() == f2.read()
63
+
64
+
65
+ def test_dump_file_modes():
66
+ example_dict = {
67
+ "parent": {
68
+ "some": "thing",
69
+ "child": {
70
+ "other": "thing"
71
+ }
72
+ }
73
+ }
74
+ # ensure that output_dir exists
75
+ output_dir.mkdir(exist_ok=True)
76
+ yaml_file = str(example_configs / 'same_as.yaml')
77
+ yaml_dump = str(output_dir / 'dumped.yaml')
78
+ yaml_source = GenericFileSource(yaml_file)
79
+ yaml_source.dump(yaml_dump)
80
+ assert compare_file_contents(yaml_file, yaml_dump)
81
+
82
+ toml_file = str(example_configs / 'same_as.toml')
83
+ toml_dump = str(output_dir / 'dumped.toml')
84
+ toml_source = GenericFileSource(toml_file)
85
+ toml_source.dump(toml_dump)
86
+ assert compare_file_contents(toml_file, toml_dump)
87
+
88
+ json_file = str(example_configs / 'same_as.json')
89
+ json_dump = str(output_dir / 'dumped.json')
90
+ json_source = GenericFileSource(json_file)
91
+ json_source.dump(json_dump)
92
+ assert compare_file_contents(json_file, json_dump)
93
+
94
+
95
+ def test_forced_file_type():
96
+ yaml_file = str(fixtures_dir / 'this_is_yaml')
97
+ yaml_source = GenericFileSource(yaml_file, forced_file_type='YAML')
98
+ assert yaml_source.data == {
99
+ 'this': 'is yaml'
100
+ }
101
+
102
+ toml_file = str(fixtures_dir / 'this_is_toml')
103
+ toml_source = GenericFileSource(toml_file, forced_file_type='TOML')
104
+ assert toml_source.data == {
105
+ 'parent': {'this': 'is toml'}
106
+ }
107
+
108
+ json_file = str(fixtures_dir / 'this_is_json')
109
+ json_source = GenericFileSource(json_file, forced_file_type='JSON')
110
+ assert json_source.data == {
111
+ 'this': 'is json'
112
+ }