Skip to content

Commit e9f3cf2

Browse files
Re-writing unit tests
1 parent 2cecf26 commit e9f3cf2

10 files changed

Lines changed: 115 additions & 51 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ SCHEMA_CONFIG = {
2121
"core":{
2222
"logging":{
2323
"format": And(Use(str), lambda string: len(string) > 0),
24-
"datefmt": And(Use(str), lambda string: len(string) > 0)
24+
"datefmt": And(Use(str), lambda string: len(string) > 0),
25+
"level": str
2526
},
2627
"allowed_clients":[
2728
{
@@ -40,6 +41,7 @@ core:
4041
logging:
4142
format: "[%(asctime)s][%(levelname)s]: %(message)s"
4243
datefmt: "%d-%b-%y %H:%M:%S"
44+
level: ${LEVEL_ENV_VARIABLE}
4345
allowed_clients:
4446
- ip: 192.168.0.10
4547
timeout: 60

config/bad_content.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{""

config/bad_content.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{'''}

config/config.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
"core":{
33
"logging":{
44
"format": "format",
5-
"datefmt": "datefmt"
5+
"datefmt": "${DATE_FORMAT_TEST}",
6+
"level": "$LOG_LEVEL_TEST"
67
},
7-
"obj_list":[{"name":"bruno", "age":24}]
8+
"obj_list":[{"name":"Mike", "age":24}]
89
}
910
}

config/config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
core:
2+
logging:
3+
format: format
4+
datefmt: datefmt
5+
level: level
6+
obj_list:
7+
- name: ${TEST_PYCONFIGPARSER}
8+
age: 24
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
'core': {
33
'logging': {
44
'format': str,
5-
'datefmt': str
5+
'datefmt': str,
6+
'level': str
67
},
78
'obj_list': [{'name': str, 'age': int}]
89
}
910
}
11+
12+
UNSUPPORTED_OBJECT_KEYS_SCHEMA = {
13+
'testeWith-': int
14+
}

config/unsupported_object_key.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"testeWith-": 1}

pyconfigparser.py

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ def json_parser(file_buff):
1111
try:
1212
return json.loads(file_buff)
1313
except json.JSONDecodeError as e:
14-
raise ConfigFileDecodeError(e)
14+
raise ConfigFileDecodeError(f'Unable to decode config file using json', e)
1515

1616

1717
def yaml_parser(file_buff):
1818
try:
19-
return yaml.load(file_buff, yaml.FullLoader)
19+
return yaml.safe_load(file_buff)
2020
except yaml.YAMLError as e:
21-
raise ConfigFileDecodeError(e)
21+
raise ConfigFileDecodeError(f'Unable to decode config file using yaml', e)
2222

2323

24+
LINUX_KEY_VARIABLE_PATTERN = r'\$([a-zA-Z][\w]+|\{[a-zA-Z][\w]+\})$'
2425
DEFAULT_CONFIG_FILES = ('config.json', 'config.yaml', 'config.yml')
25-
ENTITY_NAME_PATTERN = '^[\w\d_]+$'
26+
ENTITY_NAME_PATTERN = r'^[a-zA-Z][\w]+$'
27+
2628
SUPPORTED_EXTENSIONS = {
2729
'json': json_parser,
2830
'yaml': yaml_parser,
@@ -41,15 +43,30 @@ def __iter__(self):
4143

4244
class Config:
4345
__instance = None
46+
__hold_an_instance = True
47+
48+
@classmethod
49+
def hold_an_instance(cls):
50+
return cls.__hold_an_instance
51+
52+
@classmethod
53+
def set_hold_an_instance(cls, value):
54+
if type(value) is not bool:
55+
raise ValueError('value must be a bool')
56+
cls.__hold_an_instance = value
4457

4558
def __new__(cls, *args, **kwargs):
4659
raise RuntimeError('A instance of config is not allowed, use Config.get_config() instead')
4760

4861
@classmethod
4962
def get_config(cls, schema: dict = None, config_dir: str = 'config', file_name: Any = DEFAULT_CONFIG_FILES):
5063

51-
if cls.__instance is None or schema is not None:
52-
cls.__create_new_instance(schema, config_dir, file_name)
64+
if cls.__instance is None:
65+
instance = cls.__create_new_instance(schema, config_dir, file_name)
66+
if cls.__hold_an_instance:
67+
cls.__instance = instance
68+
else:
69+
return instance
5370
return cls.__instance
5471

5572
@classmethod
@@ -61,17 +78,17 @@ def __create_new_instance(cls, schema, config_dir, file_name):
6178

6279
try:
6380
config = Schema(schema).validate(parser(file_buff))
64-
cls.__instance = cls.__dict_2_obj(config)
81+
return cls.__dict_2_obj(config)
6582
except SchemaError as e:
66-
raise ConfigFileModelError(str(e))
83+
raise ConfigError('Schema validation error', e)
6784

6885
@classmethod
6986
def __get_file_parser(cls, file_path):
7087
try:
7188
extension = file_path.split('.')[-1]
7289
return SUPPORTED_EXTENSIONS[extension]
7390
except KeyError:
74-
raise ConfigFileExtensionNotSupportedError(f'Supported extensions: {list(SUPPORTED_EXTENSIONS.keys())}')
91+
raise ConfigError(f'Supported extensions: {list(SUPPORTED_EXTENSIONS.keys())}')
7592

7693
@classmethod
7794
def __get_file_path(cls, config_dir, file_name):
@@ -94,11 +111,8 @@ def __check_schema(cls, schema):
94111

95112
@classmethod
96113
def __get_file_buff(cls, path_file: str):
97-
try:
98-
with open(path_file, 'r') as f:
99-
return f.read()
100-
except Exception as e:
101-
raise ConfigFileOpenReadError(str(e))
114+
with open(path_file, 'r') as f:
115+
return f.read()
102116

103117
@classmethod
104118
def __dict_2_obj(cls, data: Any):
@@ -108,43 +122,39 @@ def __dict_2_obj(cls, data: Any):
108122
obj = ConfigValue()
109123
for key, value in data.items():
110124
if re.search(ENTITY_NAME_PATTERN, key) is None:
111-
raise ConfigEntitiesWithWrongNameError(
125+
raise ConfigError(
112126
f'The key {key} is invalid. The entity keys only may have words, number and underscores.')
113127
setattr(obj, key, cls.__dict_2_obj(value))
114128
return obj
115129
if _type in (list, set, tuple):
116130
return list(map(lambda v: cls.__dict_2_obj(v), data))
117131
else:
132+
if type(data) is str and re.search(LINUX_KEY_VARIABLE_PATTERN, data) is not None:
133+
return cls.interpol_variable(data)
118134
return data
119135

136+
@classmethod
137+
def interpol_variable(cls, data):
138+
try:
139+
return os.environ[cls.extract_env_variable_key(data)]
140+
except KeyError:
141+
raise ConfigError(f'Environment variable {data} was not found')
120142

121-
class ConfigError(Exception):
122-
pass
143+
@classmethod
144+
def extract_env_variable_key(cls, variable):
145+
variable = variable[1:]
146+
if variable[0] == '{':
147+
return variable[1:-1]
148+
return variable
123149

124150

125-
class ConfigFileModelError(ConfigError):
151+
class ConfigError(Exception):
126152
pass
127153

128154

129155
class ConfigFileDecodeError(ConfigError):
130156
pass
131157

132158

133-
class ConfigSchemaModelError(ConfigError):
134-
pass
135-
136-
137-
class ConfigFileOpenReadError(ConfigError):
138-
pass
139-
140-
141159
class ConfigFileNotFoundError(ConfigError):
142160
pass
143-
144-
145-
class ConfigFileExtensionNotSupportedError(ConfigError):
146-
pass
147-
148-
149-
class ConfigEntitiesWithWrongNameError(ConfigError):
150-
pass

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name='python-config-parser',
8-
version='2.0.3',
8+
version='2.0.4',
99
author='Bruno Silva de Andrade',
1010
author_email='brunojf.andrade@gmail.com',
1111
description='Project created to given the possibility of create dynamics config files',

test_configparser.py

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,65 @@
1+
import os
12
import unittest
2-
from pyconfigparser import *
3-
from config.schema_config_test import SIMPLE_SCHEMA_CONFIG
3+
from config.schemas import SIMPLE_SCHEMA_CONFIG, UNSUPPORTED_OBJECT_KEYS_SCHEMA
4+
from pyconfigparser import Config, ConfigError, ConfigFileNotFoundError, ConfigFileDecodeError
5+
6+
DT_FMT_TEST = '%Y-%m-%dT%H:%M:%SZ'
7+
VAR_LOG_LEVEL_INFO = 'INFO'
48

59

610
class ConfigTestCase(unittest.TestCase):
11+
def setUp(self) -> None:
12+
Config.set_hold_an_instance(False)
13+
os.environ['DATE_FORMAT_TEST'] = DT_FMT_TEST
14+
os.environ['LOG_LEVEL_TEST'] = VAR_LOG_LEVEL_INFO
715

8-
def test_config_without_file(self):
9-
self.assertRaises(ConfigFileNotFoundError, Config.get_config, SIMPLE_SCHEMA_CONFIG, 'config', 'some_non_exists_file.json')
16+
def test_new(self):
17+
self.assertRaises(RuntimeError, Config)
1018

11-
def test_config_with_wrong_json_model(self):
12-
self.assertRaises(ConfigFileModelError, Config.get_config, SIMPLE_SCHEMA_CONFIG, 'config', 'wrong_model.json')
19+
def test_schema_checking(self):
20+
self.assertRaises(ConfigError, Config.get_config)
21+
self.assertRaises(ConfigError, Config.get_config, [])
1322

14-
def test_config_file_with_unsupported_extension(self):
15-
self.assertRaises(ConfigFileExtensionNotSupportedError, Config.get_config, SIMPLE_SCHEMA_CONFIG, 'config', 'config.bad_extension')
23+
def test_config_without_file(self):
24+
self.assertRaises(ConfigFileNotFoundError, Config.get_config, SIMPLE_SCHEMA_CONFIG,
25+
'config',
26+
'some_non_exists_file.json')
27+
28+
def test_undefined_env_var(self):
29+
try:
30+
Config.get_config(SIMPLE_SCHEMA_CONFIG, file_name='config.yaml')
31+
except Exception as e:
32+
self.assertIn('Environment', str(e))
1633

1734
def test_to_access_attr_from_config(self):
1835
config = Config.get_config(SIMPLE_SCHEMA_CONFIG)
19-
self.assertEqual(config.core.logging.format, 'format')
20-
self.assertEqual(config.core.logging.datefmt, 'datefmt')
21-
self.assertEqual(config.core.obj_list[0].name, 'bruno')
22-
self.assertEqual(config.core.obj_list[0].age, 24)
36+
self.assertEqual(VAR_LOG_LEVEL_INFO, config.core.logging.level)
37+
self.assertEqual(DT_FMT_TEST, config.core.logging.datefmt)
38+
self.assertEqual('format', config.core.logging.format)
39+
self.assertEqual(24, config.core.obj_list[0].age)
40+
self.assertEqual('Mike', config.core.obj_list[0]['name']) # <- using subscriptable access
2341

2442
def test_access_fake_attr(self):
2543
config = Config.get_config(SIMPLE_SCHEMA_CONFIG)
2644
self.assertRaises(AttributeError, lambda: config.fake_attr)
2745

46+
def test_unsupported_object_key(self):
47+
self.assertRaises(ConfigError, Config.get_config, UNSUPPORTED_OBJECT_KEYS_SCHEMA,
48+
file_name='unsupported_object_key.json')
49+
50+
def test_set_hold_an_invalid_instance(self):
51+
self.assertRaises(ValueError, Config.set_hold_an_instance, [])
52+
53+
def test_config_with_wrong_json_model(self):
54+
self.assertRaises(ConfigError, Config.get_config, SIMPLE_SCHEMA_CONFIG, file_name='wrong_model.json')
55+
56+
def test_config_file_with_unsupported_extension(self):
57+
self.assertRaises(ConfigError, Config.get_config, SIMPLE_SCHEMA_CONFIG, file_name='config.bad_extension')
58+
59+
def test_bad_decoder_error(self):
60+
self.assertRaises(ConfigFileDecodeError, Config.get_config, SIMPLE_SCHEMA_CONFIG, file_name='bad_content.json')
61+
self.assertRaises(ConfigFileDecodeError, Config.get_config, SIMPLE_SCHEMA_CONFIG, file_name='bad_content.yaml')
62+
2863

2964
if __name__ == '__main__':
3065
unittest.main()

0 commit comments

Comments
 (0)