Skip to content
This repository was archived by the owner on Nov 6, 2022. It is now read-only.

Commit 4e6ee9b

Browse files
improves code 🍰 (#3)
1 parent f467ec9 commit 4e6ee9b

9 files changed

Lines changed: 354 additions & 248 deletions

File tree

.azuredevops/build.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
trigger:
2+
- master
3+
4+
pool:
5+
vmImage: 'ubuntu-latest'
6+
strategy:
7+
matrix:
8+
Python37:
9+
python.version: '3.7'
10+
Python38:
11+
python.version: '3.8'
12+
13+
steps:
14+
- task: UsePythonVersion@0
15+
inputs:
16+
versionSpec: '$(python.version)'
17+
displayName: 'Use Python $(python.version)'
18+
19+
- script: |
20+
python -m pip install --upgrade pip
21+
pip install -r requirements.txt
22+
displayName: 'Install dependencies'
23+
24+
- script: flake8
25+
displayName: Flake8 test
26+
27+
- script: |
28+
pip install pytest pytest-cov
29+
pip install https://github.com/RobertoPrevato/pytest-azurepipelines/archive/css_styles.zip
30+
python -m pytest tests/ --cov roconfiguration --cov-report html
31+
displayName: Run pytest tests
32+
33+
- script: |
34+
pip install --upgrade twine setuptools wheel
35+
python setup.py sdist bdist_wheel
36+
displayName: 'create artifacts'
37+
condition: and(succeeded(), eq(variables['python.version'], '3.8'))
38+
39+
- task: PublishBuildArtifacts@1
40+
inputs:
41+
pathtoPublish: dist
42+
artifactName: 'dist_$(python.version)'
43+
displayName: 'publish artifacts'
44+
condition: and(succeeded(), eq(variables['python.version'], '3.8'))

.flake8

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[flake8]
2+
exclude = __pycache__,built,build,venv
3+
ignore = E203, E266, W503
4+
max-line-length = 88
5+
max-complexity = 18
6+
select = B,C,E,F,W,T4,B9

mypy.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[mypy]
2+
python_version = 3.7
3+
follow_imports = False
4+
ignore_missing_imports = True
5+
ignore_errors = False
6+
warn_redundant_casts = True
7+
warn_unused_configs = True
8+
show_column_numbers = True

pytest.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[pytest]
2+
markers =
3+
cqa: Code Quality Assurance
4+
junit_family=xunit1

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
pytest
22
PyYAML
3+
flake8
4+
mypy

roconfiguration.code-workspace

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"folders": [
3+
{
4+
"path": "."
5+
}
6+
],
7+
"settings": {
8+
"python.testing.pytestArgs": [
9+
"."
10+
],
11+
"files.trimTrailingWhitespace": true,
12+
"files.trimFinalNewlines": true,
13+
"python.testing.unittestEnabled": false,
14+
"python.testing.nosetestsEnabled": false,
15+
"python.testing.pytestEnabled": true,
16+
"python.linting.pylintEnabled": false,
17+
"python.linting.flake8Enabled": true,
18+
"python.linting.mypyEnabled": true,
19+
"python.linting.enabled": true,
20+
"[python]": {
21+
"editor.tabSize": 4,
22+
"editor.rulers": [
23+
88,
24+
100
25+
]
26+
},
27+
"python.pythonPath": "env/bin/python3.7"
28+
}
29+
}

roconfiguration/__init__.py

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import os
2-
import yaml
3-
import json
41
import configparser
5-
from typing import Union, Dict, Any, Sequence
2+
import json
3+
import os
64
from collections import abc
5+
from typing import Any, Dict, Mapping, Optional
76

7+
import yaml
88

9-
__all__ = ['Configuration', 'ConfigurationError', 'ConfigurationOverrideError']
9+
__all__ = ["Configuration", "ConfigurationError", "ConfigurationOverrideError"]
1010

1111

1212
class ConfigurationError(Exception):
@@ -18,8 +18,8 @@ class ConfigurationOverrideError(ConfigurationError):
1818

1919

2020
def apply_key_value(obj, key, value):
21-
key = key.strip('_:') # remove special characters from both ends
22-
for token in (':', '__'):
21+
key = key.strip("_:") # remove special characters from both ends
22+
for token in (":", "__"):
2323
if token in key:
2424
parts = key.split(token)
2525

@@ -31,7 +31,9 @@ def apply_key_value(obj, key, value):
3131
try:
3232
index = int(part)
3333
except ValueError:
34-
raise ConfigurationOverrideError(f'{part} was supposed to be a numeric index in {key}')
34+
raise ConfigurationOverrideError(
35+
f"{part} was supposed to be a numeric index in {key}"
36+
)
3537

3638
sub_property = sub_property[index]
3739
continue
@@ -42,27 +44,38 @@ def apply_key_value(obj, key, value):
4244
sub_property[part] = {}
4345
sub_property = sub_property[part]
4446
else:
45-
if not isinstance(sub_property, abc.Mapping) and not isinstance(sub_property, abc.MutableSequence):
46-
raise ConfigurationOverrideError(f'The key `{key}` cannot be used because it overrides another '
47-
f'variable with shorter key! ({part}, {sub_property})')
47+
if not isinstance(sub_property, abc.Mapping) and not isinstance(
48+
sub_property, abc.MutableSequence
49+
):
50+
raise ConfigurationOverrideError(
51+
f"The key `{key}` cannot be used "
52+
f"because it overrides another "
53+
f"variable with shorter key! ({part}, {sub_property})"
54+
)
4855

4956
if isinstance(sub_property, abc.MutableSequence):
5057
try:
5158
index = int(last_part)
5259
except ValueError:
53-
raise ConfigurationOverrideError(f'{last_part} was supposed to be a numeric index in {key}, '
54-
f'because the affected property is a mutable sequence.')
60+
raise ConfigurationOverrideError(
61+
f"{last_part} was supposed to be a numeric index in {key}, "
62+
f"because the affected property is a mutable sequence."
63+
)
5564

5665
try:
5766
sub_property[index] = value
5867
except IndexError:
59-
raise ConfigurationOverrideError(f'Invalid override for mutable sequence {key}; '
60-
f'assignment index out of range')
68+
raise ConfigurationOverrideError(
69+
f"Invalid override for mutable sequence {key}; "
70+
f"assignment index out of range"
71+
)
6172
else:
6273
try:
6374
sub_property[last_part] = value
6475
except TypeError as te:
65-
raise ConfigurationOverrideError(f'Invalid assignment {key} -> {value}; {str(te)}')
76+
raise ConfigurationOverrideError(
77+
f"Invalid assignment {key} -> {value}; {str(te)}"
78+
)
6679

6780
return obj
6881

@@ -75,7 +88,9 @@ def _develop_configparser_values(parser):
7588
for section_name in parser.sections():
7689
section_values = {}
7790
for k, v in parser[section_name].items():
78-
section_values[k] = _develop_configparser_values(v) if isinstance(v, abc.Mapping) else v
91+
section_values[k] = (
92+
_develop_configparser_values(v) if isinstance(v, abc.Mapping) else v
93+
)
7994
values[section_name] = section_values
8095
return values
8196

@@ -85,10 +100,11 @@ class Configuration:
85100
Provides methods to handle configuration objects.
86101
A read-only façade for navigating configuration objects using attribute notation.
87102
88-
Thanks to Fluent Python, book by Luciano Ramalho; this class is inspired by his example of JSON structure explorer.
103+
Thanks to Fluent Python, book by Luciano Ramalho; this class is inspired by his
104+
example of JSON structure explorer.
89105
"""
90106

91-
__slots__ = ('__data',)
107+
__slots__ = ("__data",)
92108

93109
def __new__(cls, arg=None):
94110
if not arg:
@@ -99,12 +115,14 @@ def __new__(cls, arg=None):
99115
return [cls(item) for item in arg]
100116
return arg
101117

102-
def __init__(self, mapping: Union[None, Dict[str, Any], Sequence[Dict[str, Any]]] = None):
103-
self.__data = {}
118+
def __init__(
119+
self, mapping: Optional[Mapping[str, Any]] = None
120+
):
121+
self.__data: Dict[str, Any] = {}
104122
if mapping:
105123
self.add_map(mapping)
106124

107-
def __contains__(self, item):
125+
def __contains__(self, item: str) -> bool:
108126
return item in self.__data
109127

110128
def __getitem__(self, name):
@@ -113,19 +131,19 @@ def __getitem__(self, name):
113131
raise KeyError(name)
114132
return value
115133

116-
def __getattr__(self, name, default=None):
134+
def __getattr__(self, name, default=None) -> Any:
117135
if name in self.__data:
118136
value = self.__data.get(name)
119137
if isinstance(value, abc.Mapping) or isinstance(value, abc.MutableSequence):
120-
return Configuration(value)
138+
return Configuration(value) # type: ignore
121139
return value
122140
return default
123141

124-
def __repr__(self):
142+
def __repr__(self) -> str:
125143
return repr(self.values)
126144

127145
@property
128-
def values(self):
146+
def values(self) -> Dict[str, Any]:
129147
"""
130148
Returns a copy of the dictionary of current settings.
131149
"""
@@ -134,7 +152,7 @@ def values(self):
134152
def to_dict(self):
135153
return self.values
136154

137-
def add_value(self, name, value):
155+
def add_value(self, name: str, value: Any):
138156
"""
139157
Adds a configuration value by name. The name can contain
140158
paths to nested objects and list indices.
@@ -144,7 +162,7 @@ def add_value(self, name, value):
144162
"""
145163
apply_key_value(self.__data, name, value)
146164

147-
def add_map(self, value):
165+
def add_map(self, value: Mapping[str, Any]):
148166
"""
149167
Merges a mapping object such as a dictionary,
150168
inside this configuration,
@@ -154,13 +172,18 @@ def add_map(self, value):
154172
for key, value in value.items():
155173
self.__data[key] = value
156174

157-
def add_environmental_variables(self, prefix=None, strip_prefix=False):
175+
def add_environmental_variables(
176+
self,
177+
prefix: Optional[str] = None,
178+
strip_prefix: bool = False
179+
):
158180
"""
159181
Reads environmental variables inside this configuration object,
160182
optionally filtered by prefix.
161183
162184
:param prefix: optional prefix, to filter read environmental variables.
163-
:param strip_prefix: whether to strip the prefix when overriding keys by matched env variables
185+
:param strip_prefix: whether to strip the prefix when overriding keys
186+
by matched env variables
164187
"""
165188
if prefix:
166189
prefix = prefix.lower()
@@ -169,25 +192,30 @@ def add_environmental_variables(self, prefix=None, strip_prefix=False):
169192
if prefix and not lk.startswith(prefix):
170193
continue
171194
if prefix and strip_prefix:
172-
lk = lk[len(prefix):]
195+
lk = lk[len(prefix) :]
173196
apply_key_value(self.__data, lk, v)
174197

175-
def add_ini(self, ini_settings):
198+
def add_ini(self, ini_settings: str) -> None:
176199
"""
177200
Reads settings from given ini configuration code.
178-
179-
:param ini_settings: source ini code
201+
202+
:param ini_settings: source ini code
180203
"""
181204
parser = configparser.ConfigParser()
182205
parser.read_string(ini_settings)
183206
self.add_map(_develop_configparser_values(parser))
184207

185-
def _handle_missing_configuration_file(self, file_path):
186-
raise FileNotFoundError(f'missing configuration file: {file_path}')
208+
def _handle_missing_configuration_file(self, file_path: str) -> None:
209+
raise FileNotFoundError(f"missing configuration file: {file_path}")
187210

188-
def add_ini_file(self, file_path, optional=False):
211+
def add_ini_file(
212+
self,
213+
file_path: str,
214+
optional: bool = False
215+
) -> None:
189216
"""
190-
Reads and parse an ini file, merging its values into an instance of Configuration.
217+
Reads and parse an ini file, merging its values into an instance of
218+
Configuration.
191219
192220
:param file_path: path to an ini file
193221
:param optional: whether the ini file is optional.
@@ -200,9 +228,14 @@ def add_ini_file(self, file_path, optional=False):
200228
parser.read(file_path)
201229
self.add_map(_develop_configparser_values(parser))
202230

203-
def add_json_file(self, file_path, optional=False):
231+
def add_json_file(
232+
self,
233+
file_path: str,
234+
optional: bool = False
235+
) -> None:
204236
"""
205-
Reads and parse an json file, merging its values into an instance of Configuration.
237+
Reads and parse an json file, merging its values into an instance of
238+
Configuration.
206239
207240
:param file_path: path to an json file
208241
:param optional: whether the json file is optional.
@@ -212,12 +245,18 @@ def add_json_file(self, file_path, optional=False):
212245
return
213246
self._handle_missing_configuration_file(file_path)
214247

215-
with open(file_path, 'rt', encoding='utf-8') as f:
248+
with open(file_path, "rt", encoding="utf-8") as f:
216249
self.add_map(json.load(f))
217250

218-
def add_yaml_file(self, file_path, optional=False, safe_load=True):
251+
def add_yaml_file(
252+
self,
253+
file_path: str,
254+
optional: bool = False,
255+
safe_load: bool = True
256+
) -> None:
219257
"""
220-
Reads and parse an yaml file, merging its values into an instance of Configuration.
258+
Reads and parse an yaml file, merging its values into an instance of
259+
Configuration.
221260
222261
:param file_path: path to an yaml file
223262
:param optional: whether the yaml file is optional.
@@ -228,7 +267,7 @@ def add_yaml_file(self, file_path, optional=False, safe_load=True):
228267
return
229268
self._handle_missing_configuration_file(file_path)
230269

231-
with open(file_path, 'rt', encoding='utf-8') as f:
270+
with open(file_path, "rt", encoding="utf-8") as f:
232271

233272
if safe_load:
234273
data = yaml.safe_load(f)

0 commit comments

Comments
 (0)