Skip to content

Commit 17d103a

Browse files
authored
Merge pull request #159 from pyapp-org/development
Release 4.7.1
2 parents 52145e1 + 0fda360 commit 17d103a

File tree

5 files changed

+93
-33
lines changed

5 files changed

+93
-33
lines changed

HISTORY

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
4.7.1
2+
=====
3+
4+
Changes
5+
--------
6+
7+
- Improve the settings reset to properly apply the base-settings after reset.
8+
These are the basic settings required for the application to operate
9+
correctly.
10+
11+
112
4.7.0
213
=====
314

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "rtd_poetry"
44

55
[tool.poetry]
66
name = "pyapp"
7-
version = "4.7.0"
7+
version = "4.7.1"
88
description = "A Python application framework - Let us handle the boring stuff!"
99
authors = ["Tim Savage <[email protected]>"]
1010
license = "BSD-3-Clause"

src/pyapp/conf/__init__.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,19 @@
9393
import logging
9494
import os
9595
import warnings
96+
from typing import Any
97+
from typing import Iterable
9698
from typing import List
9799
from typing import Sequence
100+
from typing import Tuple
101+
from typing import Union
98102

99103
from pyapp.conf import base_settings
100-
from pyapp.conf.loaders import factory
101104
from pyapp.conf.loaders import Loader
102105
from pyapp.conf.loaders import ModuleLoader
103106

107+
from . import loaders
108+
104109
logger = logging.getLogger(__name__)
105110

106111
DEFAULT_ENV_KEY = "PYAPP_SETTINGS"
@@ -188,15 +193,25 @@ def reset_settings(self):
188193
189194
This is useful for testing CLI entry points
190195
"""
191-
setting_keys = [
192-
key for key in self._container.keys if key != "SETTINGS_SOURCES"
193-
]
196+
container = self._container
197+
198+
# Save and remove existing settings
199+
saved_settings = [(key, container.__dict__.pop(key)) for key in container.keys]
194200

195-
for setting_key in setting_keys:
196-
delattr(self, setting_key)
201+
# Initialise base settings
202+
container._populate_base_settings() # pylint: disable=protected-access
197203

198-
# Clear settings sources
199-
setattr(self, "SETTINGS_SOURCES", [])
204+
def restore_settings():
205+
# Remove new settings
206+
for key in container.keys:
207+
del container.__dict__[key]
208+
209+
# Restore saved settings
210+
container.__dict__.update(saved_settings)
211+
212+
# Add restore action
213+
action = restore_settings, ()
214+
self._roll_back.append(action)
200215

201216

202217
class Settings:
@@ -205,25 +220,30 @@ class Settings:
205220
"""
206221

207222
def __init__(self, base_settings_=None):
208-
base_settings_ = base_settings_ or base_settings
209-
210-
# Copy values from base settings file.
211-
self.__dict__.update(
212-
(k, getattr(base_settings_, k)) for k in dir(base_settings_) if k.upper()
213-
)
214-
215-
self.__dict__["SETTINGS_SOURCES"] = [] # pylint: disable=invalid-name
223+
self._populate_base_settings(base_settings_)
216224

217225
def __getattr__(self, item):
218226
raise AttributeError("Setting not defined {!r}".format(item))
219227

220228
def __setattr__(self, key, value):
221229
raise AttributeError("Readonly object")
222230

231+
def __getitem__(self, item):
232+
return self.__dict__[item]
233+
223234
def __repr__(self) -> str:
224235
sources = self.SETTINGS_SOURCES or "UN-CONFIGURED"
225236
return f"{self.__class__.__name__}({sources})"
226237

238+
def _populate_base_settings(self, base_settings_=None):
239+
base_settings_ = base_settings_ or base_settings
240+
241+
# Copy values from base settings file.
242+
self.__dict__.update(
243+
(k, getattr(base_settings_, k)) for k in dir(base_settings_) if k.upper()
244+
)
245+
self.__dict__["SETTINGS_SOURCES"] = [] # pylint: disable=invalid-name
246+
227247
@property
228248
def is_configured(self) -> bool:
229249
"""
@@ -238,6 +258,14 @@ def keys(self) -> Sequence[str]:
238258
"""
239259
return [key for key in self.__dict__ if key.isupper()]
240260

261+
def items(self) -> Iterable[Tuple[str, Any]]:
262+
"""
263+
Return a sorted iterable of all key/value pairs of settings
264+
"""
265+
data = self.__dict__
266+
for key in sorted(self.keys):
267+
yield key, data[key]
268+
241269
def load(self, loader: Loader, apply_method=None):
242270
"""
243271
Load settings from a loader instance. A loader is an iterator that yields key/value pairs.
@@ -270,7 +298,7 @@ def load(self, loader: Loader, apply_method=None):
270298
include_settings = self.__dict__.pop("INCLUDE_SETTINGS", None)
271299
if include_settings:
272300
for source_url in include_settings:
273-
self.load(factory(source_url), apply_method)
301+
self.load(loaders.factory(source_url), apply_method)
274302

275303
def load_from_loaders(self, loader_list: Sequence[Loader], override: bool = True):
276304
"""
@@ -288,7 +316,7 @@ def load_from_loaders(self, loader_list: Sequence[Loader], override: bool = True
288316

289317
def configure(
290318
self,
291-
default_settings: Sequence[str],
319+
default_settings: Union[str, Sequence[str]],
292320
runtime_settings: str = None,
293321
additional_loaders: Sequence[Loader] = None,
294322
env_settings_key: str = DEFAULT_ENV_KEY,
@@ -304,13 +332,18 @@ def configure(
304332
"""
305333
logger.debug("Configuring settings...")
306334

335+
# Allow a simple string to be supplied
336+
if isinstance(default_settings, str):
337+
default_settings = [default_settings]
338+
339+
# Build list of loaders
307340
loader_list: List[Loader] = [ModuleLoader(s) for s in default_settings]
308341

309342
# Add run time settings (which can be overridden or specified by an
310343
# environment variable).
311344
runtime_settings = runtime_settings or os.environ.get(env_settings_key)
312345
if runtime_settings:
313-
loader_list.append(ModuleLoader(runtime_settings))
346+
loader_list.append(loaders.factory(runtime_settings))
314347

315348
# Append the additional loaders if defined
316349
if additional_loaders:

src/pyapp/conf/report.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,5 @@ def run(self):
7575
"""
7676
Run the report
7777
"""
78-
settings = self.settings
79-
for key in settings.keys:
80-
self.output_result(key, getattr(settings, key))
78+
for key, value in self.settings.items():
79+
self.output_result(key, value)

tests/conf/test_.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class TestSettings:
66
@pytest.fixture
77
def target(self) -> pyapp.conf.Settings:
88
target = pyapp.conf.Settings()
9-
target.configure(["tests.settings"])
9+
target.configure("tests.settings")
1010
return target
1111

1212
def test_ensure_readonly(self, target: pyapp.conf.Settings):
@@ -20,6 +20,16 @@ def test_configure(self, target: pyapp.conf.Settings):
2020
assert not hasattr(target, "mixed_VALUE")
2121

2222
def test_configure__from_runtime_parameter(self):
23+
target = pyapp.conf.Settings()
24+
target.configure("tests.settings", "tests.runtime_settings")
25+
26+
assert "python:tests.runtime_settings" in target.SETTINGS_SOURCES
27+
assert hasattr(target, "UPPER_VALUE")
28+
assert hasattr(target, "RUNTIME_VALUE")
29+
assert not hasattr(target, "lower_value")
30+
assert not hasattr(target, "mixed_VALUE")
31+
32+
def test_configure__with_a_list_of_settings(self):
2333
target = pyapp.conf.Settings()
2434
target.configure(["tests.settings"], "tests.runtime_settings")
2535

@@ -33,7 +43,7 @@ def test_configure__from_environment(self, monkeypatch):
3343
monkeypatch.setenv("PYAPP_SETTINGS", "tests.runtime_settings")
3444

3545
target = pyapp.conf.Settings()
36-
target.configure(["tests.settings"])
46+
target.configure("tests.settings")
3747

3848
assert "python:tests.runtime_settings" in target.SETTINGS_SOURCES
3949
assert hasattr(target, "UPPER_VALUE")
@@ -46,7 +56,7 @@ def test_configure__additional_loaders(self):
4656

4757
with pytest.warns(ImportWarning):
4858
target.configure(
49-
["tests.settings"],
59+
"tests.settings",
5060
"tests.runtime_settings",
5161
[pyapp.conf.ModuleLoader("tests.runtime_settings_with_imports")],
5262
)
@@ -56,14 +66,14 @@ def test_configure__additional_loaders(self):
5666

5767
def test_load__duplicate_settings_file(self):
5868
target = pyapp.conf.Settings()
59-
target.configure(["tests.settings"], "tests.runtime_settings")
69+
target.configure("tests.settings", "tests.runtime_settings")
6070

6171
with pytest.warns(ImportWarning):
6272
target.load(pyapp.conf.ModuleLoader("tests.runtime_settings"))
6373

6474
def test_load__specify_include_settings(self):
6575
target = pyapp.conf.Settings()
66-
target.configure(["tests.settings"], "tests.runtime_settings_with_imports")
76+
target.configure("tests.settings", "tests.runtime_settings_with_imports")
6777

6878
assert "python:tests.runtime_settings_with_imports" in target.SETTINGS_SOURCES
6979
assert "python:tests.runtime_settings" in target.SETTINGS_SOURCES
@@ -142,13 +152,20 @@ def test_modify__reset_settings(self, target: pyapp.conf.Settings):
142152
"TEST_PROVIDERS",
143153
}
144154

155+
initial_keys = target.keys
156+
145157
with target.modify() as patch:
146158
patch.reset_settings()
147159

148-
assert all(not hasattr(target, key) for key in known_keys)
149-
assert target.SETTINGS_SOURCES == []
150-
assert not target.is_configured
160+
assert all(
161+
not hasattr(target, key) for key in known_keys
162+
), "Custom keys still exist"
163+
assert target.SETTINGS_SOURCES == [], "Sources have not been cleared"
164+
assert not target.is_configured, "Is still listed as configured"
165+
assert isinstance(target.LOGGING, dict), "Base settings missing"
151166

152167
# Check items have been restored
153-
assert all(hasattr(target, key) for key in known_keys)
154-
assert target.SETTINGS_SOURCES == ["python:tests.settings"]
168+
assert initial_keys == target.keys, "All settings not restored"
169+
assert target.SETTINGS_SOURCES == [
170+
"python:tests.settings"
171+
], "Sources not restored"

0 commit comments

Comments
 (0)