Skip to content

Commit

Permalink
Merge pull request #15 from BrunoSilvaAndrade/develop
Browse files Browse the repository at this point in the history
fix(api)!: Renaming classes and creating a default package object
  • Loading branch information
bcchagas authored Jul 13, 2022
2 parents 96df547 + 184dde5 commit d142809
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 83 deletions.
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ MAIN FEATURES
===
---
* Declarative configurations without using .ini files
* Access OOP or subscriptable, which means that you can iterate the config object items
* Access using OOP or subscriptable, which means that you can iterate the config object items
* Runtime validation using [schema](https://github.com/keleshev/schema)
* Automatic environment variables interpolation
* Automatic parser selecting by config file extension
Expand Down Expand Up @@ -105,21 +105,23 @@ This config file as a json would be something like:
```

The instance of Config Class:

```python
from pyconfigparser import Config, ConfigError
from pyconfigparser import Configparser, ConfigError
import logging

try:
config = Config.get_config(SCHEMA_CONFIG) # <- Here I'm using that SCHEMA_CONFIG we had declared, and the dir file default value is being used
config = Configparser.get_config(
SCHEMA_CONFIG) # <- Here I'm using that SCHEMA_CONFIG we had declared, and the dir file default value is being used
except ConfigError as e:
print(e)
exit()

#to access your config you need just:
# to access your config you need just:


fmt = config.core.logging.format #look this, at this point I'm already using the config variable
date_fmt = config['core']['logging']['date_fmt'] #here subscriptable access
fmt = config.core.logging.format # look this, at this point I'm already using the config variable
date_fmt = config['core']['logging']['date_fmt'] # here subscriptable access

logging.getLogger(__name__)

Expand All @@ -129,38 +131,36 @@ logging.basicConfig(
level=logging.INFO
)

#the list of object example:
# the list of object example:

for client in config.core.allowed_clients:
print(client.ip)
print(client.timeout)


#The config object's parts which is not a list can also be itered but, it'll give you the attribute's names
#So you can access the values by subscriptale access
# The config object's parts which is not a list can also be itered but, it'll give you the attribute's names
# So you can access the values by subscriptale access
for logging_section_attr_key in config.core.logging:
print(config.core.logging[logging_section_attr_key])

#Accessing the environment variable already resolved
# Accessing the environment variable already resolved
print(config.random_env_variable)

```
Since you've already created the first Config's instance this instance will be cached inside Config class,
so after this first creation you can just invoke Config.get_config()

```python
from pyconfigparser import Config
from pyconfigparser import Configparser

config = Config.get_config() #At this point you already have the configuration properties in your config object
config = Configparser.get_config() # At this point you already have the configuration properties in your config object
```

You can also disable the action to cache the instance config


```python
from pyconfigparser import Config
from pyconfigparser import Configparser

Config.set_hold_an_instance(False)
Configparser.hold_an_instance = False
```


Expand Down
99 changes: 48 additions & 51 deletions pyconfigparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os
import re

LINUX_KEY_VARIABLE_PATTERN = r'\$([a-zA-Z][\w]+|\{[a-zA-Z][\w]+\})$'
VARIABLE_PATTERN = r'\$([a-zA-Z][\w]+|\{[a-zA-Z][\w]+\})$'
DEFAULT_CONFIG_FILES = ('config.json', 'config.yaml', 'config.yml')
ENTITY_NAME_PATTERN = r'^[a-zA-Z][\w]+$'
SUPPORTED_EXTENSIONS = {
Expand All @@ -23,7 +23,7 @@ class ConfigFileNotFoundError(ConfigError):
pass


class ConfigValue:
class Config:

def __getitem__(self, item):
return self.__dict__[item]
Expand All @@ -41,58 +41,52 @@ def values(self):
return self.__dict__.values()


class Config:
__instance = None
__hold_an_instance = True
class Configparser:
def __init__(self):
self.__instance = None
self.__hold_an_instance = True

@classmethod
def hold_an_instance(cls):
return cls.__hold_an_instance
@property
def hold_an_instance(self):
return self.__hold_an_instance

@classmethod
def set_hold_an_instance(cls, value):
@hold_an_instance.setter
def hold_an_instance(self, value):
if type(value) is not bool:
raise ValueError('value must be a bool')
cls.__hold_an_instance = value
self.__hold_an_instance = value

def __new__(cls, *args, **kwargs):
raise RuntimeError('A instance of config is not allowed, use Config.get_config() instead')
def get_config(self, schema: dict = None, config_dir: str = 'config', file_name: Any = DEFAULT_CONFIG_FILES):

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

if cls.__instance is None:
instance = cls.__create_new_instance(schema, config_dir, file_name)
if cls.__hold_an_instance:
cls.__instance = instance
if self.__instance is None:
instance = self.__create_new_instance(schema, config_dir, file_name)
if self.__hold_an_instance:
self.__instance = instance
else:
return instance
return cls.__instance
return self.__instance

@classmethod
def __create_new_instance(cls, schema, config_dir, file_name):
file_path = cls.__get_file_path(config_dir, file_name)
parser = cls.__get_file_parser(file_path)
file_buff = cls.__get_file_buff(file_path)
def __create_new_instance(self, schema, config_dir, file_name):
file_path = self.__get_file_path(config_dir, file_name)
parser = self.__get_file_parser(file_path)
file_buff = self.__get_file_buff(file_path)

try:
config = cls.__validate_schema(schema, parser(file_buff))
return cls.__dict_2_obj(config)
config = self.__validate_schema(schema, parser(file_buff))
return self.__dict_2_obj(config)
except ParseError as e:
raise ConfigError(e)
except SchemaError as e:
raise ConfigError('Schema validation error', e)

@classmethod
def __get_file_parser(cls, file_path):
def __get_file_parser(self, file_path):
try:
extension = file_path.split('.')[-1]
return SUPPORTED_EXTENSIONS[extension]
except KeyError:
raise ConfigError(f'Supported extensions: {list(SUPPORTED_EXTENSIONS.keys())}')

@classmethod
def __get_file_path(cls, config_dir, file_name):
def __get_file_path(self, config_dir, file_name):
file_path = f'{os.getcwd()}/{config_dir}/'
if type(file_name) is str:
file_name = [file_name]
Expand All @@ -103,49 +97,52 @@ def __get_file_path(cls, config_dir, file_name):

raise ConfigFileNotFoundError(f'Config file {file_path}{file_name} was not found')

@classmethod
def __validate_schema(cls, schema, config_obj):
def __validate_schema(self, schema, config_obj):
if schema is None:
return config_obj
elif type(schema) not in (dict, list):
raise ConfigError('The first config\'s schema element should be a Map or a List')

return Schema(schema).validate(config_obj)

@classmethod
def __get_file_buff(cls, path_file: str):
def __get_file_buff(self, path_file: str):
with open(path_file, 'r') as f:
return f.read()

@classmethod
def __dict_2_obj(cls, data: Any):
def __dict_2_obj(self, data: Any):
_type = type(data)

if _type is dict:
obj = ConfigValue()
obj = Config()
for key, value in data.items():
if re.search(ENTITY_NAME_PATTERN, key) is None:
raise ConfigError(
f'The key {key} is invalid. The entity keys only may have words, number and underscores.')
setattr(obj, key, cls.__dict_2_obj(value))
self.__is_a_valid_object_key(key)
setattr(obj, key, self.__dict_2_obj(value))
return obj
if _type in (list, set, tuple):
return list(map(lambda v: cls.__dict_2_obj(v), data))
return list(map(lambda v: self.__dict_2_obj(v), data))
else:
if type(data) is str and re.search(LINUX_KEY_VARIABLE_PATTERN, data) is not None:
return cls.interpol_variable(data)
if self.__is_variable(data):
return self.__interpol_variable(data)
return data

@classmethod
def interpol_variable(cls, data):
def __is_a_valid_object_key(self, key):
if re.search(ENTITY_NAME_PATTERN, key) is None:
raise ConfigError(f'The key {key} is invalid. The entity keys only may have words, number and underscores.')

def __is_variable(self, data):
return type(data) is str and re.search(VARIABLE_PATTERN, data) is not None

def __interpol_variable(self, data):
try:
return os.environ[cls.extract_env_variable_key(data)]
return os.environ[self.__extract_env_variable_key(data)]
except KeyError:
raise ConfigError(f'Environment variable {data} was not found')

@classmethod
def extract_env_variable_key(cls, variable):
def __extract_env_variable_key(self, variable):
variable = variable[1:]
if variable[0] == '{':
return variable[1:-1]
return variable


configparser = Configparser()
31 changes: 15 additions & 16 deletions test_configparser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pyconfigparser import Config, ConfigError, ConfigFileNotFoundError
from pyconfigparser import configparser, ConfigError, ConfigFileNotFoundError
from config.schemas import SIMPLE_SCHEMA_CONFIG, UNSUPPORTED_OBJECT_KEYS_SCHEMA
import unittest
import os
Expand All @@ -9,55 +9,54 @@

class ConfigTestCase(unittest.TestCase):
def setUp(self) -> None:
Config.set_hold_an_instance(False)
configparser.hold_an_instance = False
os.environ['DATE_FORMAT_TEST'] = DT_FMT_TEST
os.environ['LOG_LEVEL_TEST'] = VAR_LOG_LEVEL_INFO

def test_new(self):
self.assertRaises(RuntimeError, Config)

def test_schema_checking(self):
self.assertRaises(ConfigError, Config.get_config, 1)
self.assertRaises(ConfigError, configparser.get_config, 1)

def test_config_without_file(self):
self.assertRaises(ConfigFileNotFoundError, Config.get_config, SIMPLE_SCHEMA_CONFIG,
self.assertRaises(ConfigFileNotFoundError, configparser.get_config, SIMPLE_SCHEMA_CONFIG,
'config',
'some_non_exists_file.json')

def test_undefined_env_var(self):
try:
Config.get_config(file_name='config.yaml')
configparser.get_config(file_name='config.yaml')
except Exception as e:
self.assertIn('Environment', str(e))

def test_to_access_attr_from_config(self):
config = Config.get_config(SIMPLE_SCHEMA_CONFIG)
config = configparser.get_config(SIMPLE_SCHEMA_CONFIG)
self.assertEqual(VAR_LOG_LEVEL_INFO, config.core.logging.level)
self.assertEqual(DT_FMT_TEST, config.core.logging.datefmt)
self.assertEqual('format', config.core.logging.format)
self.assertEqual(24, config.core.obj_list[0].age)
self.assertEqual('Mike', config.core.obj_list[0]['name']) # <- using subscriptable access

def test_access_fake_attr(self):
config = Config.get_config(SIMPLE_SCHEMA_CONFIG)
config = configparser.get_config(SIMPLE_SCHEMA_CONFIG)
self.assertRaises(AttributeError, lambda: config.fake_attr)

def test_unsupported_object_key(self):
self.assertRaises(ConfigError, Config.get_config, UNSUPPORTED_OBJECT_KEYS_SCHEMA,
self.assertRaises(ConfigError, configparser.get_config, UNSUPPORTED_OBJECT_KEYS_SCHEMA,
file_name='unsupported_object_key.json')

def test_set_hold_an_invalid_instance(self):
self.assertRaises(ValueError, Config.set_hold_an_instance, [])
def assign_a_bad_type():
configparser.hold_an_instance = []
self.assertRaises(ValueError, assign_a_bad_type)

def test_config_with_wrong_json_model(self):
self.assertRaises(ConfigError, Config.get_config, SIMPLE_SCHEMA_CONFIG, file_name='wrong_model.json')
self.assertRaises(ConfigError, configparser.get_config, SIMPLE_SCHEMA_CONFIG, file_name='wrong_model.json')

def test_config_file_with_unsupported_extension(self):
self.assertRaises(ConfigError, Config.get_config, SIMPLE_SCHEMA_CONFIG, file_name='config.bad_extension')
self.assertRaises(ConfigError, configparser.get_config, SIMPLE_SCHEMA_CONFIG, file_name='config.bad_extension')

def test_bad_decoder_error(self):
self.assertRaises(ConfigError, Config.get_config, SIMPLE_SCHEMA_CONFIG, file_name='bad_content.json')
self.assertRaises(ConfigError, Config.get_config, SIMPLE_SCHEMA_CONFIG, file_name='bad_content.yaml')
self.assertRaises(ConfigError, configparser.get_config, SIMPLE_SCHEMA_CONFIG, file_name='bad_content.json')
self.assertRaises(ConfigError, configparser.get_config, SIMPLE_SCHEMA_CONFIG, file_name='bad_content.yaml')


if __name__ == '__main__':
Expand Down

0 comments on commit d142809

Please sign in to comment.