Skip to content

Commit

Permalink
Merge pull request #153 from pyapp-org/feature/reset-settings
Browse files Browse the repository at this point in the history
Feature/reset settings
  • Loading branch information
timsavage authored Apr 19, 2022
2 parents 46c9e8e + 211637d commit dcdcbe4
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
cd dist
echo "::set-output name=source::$(ls *.tar.gz)"
echo "::set-output name=wheel::$(ls *.whl)"
echo "::set-output name=version::$(poetry version | awk '{print $2}')"
echo "::set-output name=version::$(poetry version -s)"
- name: Create a Release
id: create_release
Expand Down
10 changes: 10 additions & 0 deletions HISTORY
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
4.7.0
=====

Features
--------

- Add reset option to settings modify context to allow settings to be completely
cleared during testing. This is particularly useful for testing CLI methods.


4.6.0
=====

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "rtd_poetry"

[tool.poetry]
name = "pyapp"
version = "4.6.0"
version = "4.7.0"
description = "A Python application framework - Let us handle the boring stuff!"
authors = ["Tim Savage <tim@savage.company>"]
license = "BSD-3-Clause"
Expand Down
50 changes: 47 additions & 3 deletions src/pyapp/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
>>> settings.MY_CONFIG_VALUE
'foo'
.. note::
All settings must be UPPER_CASE. If a setting is not upper case it will not
be imported into the settings object.
The settings object also has helper methods to simplify your testing::
>>> from pyapp.conf import settings
Expand All @@ -30,9 +34,25 @@
removed using the `del` keyword. Once the context has been exited all changes
are reverted.
.. note::
All settings must be UPPER_CASE. If a setting is not upper case it will not
be imported into the settings object.
Testing CLI Commands
--------------------
When testing the CLI settings loading can be a problem if your test case loads
different settings or requires changes to be applied to settings files for test
cases to execute correctly.
To reset settings to allow the CLI to rebuild the settings object use the
``reset_settings``::
>>> from pyapp.conf import settings
>>> with settings.modify() as patch:
... patch.reset_settings()
... assert not settings.is_configured
>>> assert settings.is_configured
Just like with any usage of ``settings.modify()`` the origional settings are
restored once the with block is exited.
Settings
========
Expand Down Expand Up @@ -161,6 +181,23 @@ def __delattr__(self, item):

del items[item]

def reset_settings(self):
"""
Completely reset all settings (including SETTINGS_SOURCES) to allow
reloading to occur.
This is useful for testing CLI entry points
"""
setting_keys = [
key for key in self._container.keys if key != "SETTINGS_SOURCES"
]

for setting_key in setting_keys:
delattr(self, setting_key)

# Clear settings sources
setattr(self, "SETTINGS_SOURCES", [])


class Settings:
"""
Expand Down Expand Up @@ -194,6 +231,13 @@ def is_configured(self) -> bool:
"""
return bool(self.SETTINGS_SOURCES)

@property
def keys(self) -> Sequence[str]:
"""
All settings keys available
"""
return [key for key in self.__dict__ if key.isupper()]

def load(self, loader: Loader, apply_method=None):
"""
Load settings from a loader instance. A loader is an iterator that yields key/value pairs.
Expand Down
69 changes: 58 additions & 11 deletions src/pyapp/conf/base_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,75 @@
###############################################################################
# Logging

LOG_LOGGERS = {}
"""
Simple method for configuring loggers that is merged into the default
logging configuration. This allows for custom loggers to be configured
without needing to duplicate the entire logging configuration.
Example::
LOG_LOGGERS = {
"my_package.my_module": {
"level": "INFO",
"handlers": ["console"]
}
}
"""

LOG_HANDLERS = {}
"""
Simple method for configuring log handlers that is merged into the default
logging configuration. This allows for custom handlers to be configured
without needing to duplicate the entire logging configuration.
By default all handlers defined in this dict are added to the `root`
handler, if this is not desired set the ``non_root`` argument to ``True``.
See the `Logging Handlers <https://docs.python.org/3/library/logging.handlers.html>`_
in the Python documentation for a complete list of builtin handlers.
Example::
LOG_HANDLERS = {
"file": {
"class": "logging.handlers.RotatingFileHandler",
"stream": "/path/to/my/file.log",
"maxBytes": 5_242_880, # 5MiB
},
"special_file": {
"class": "logging.FileHandler",
"non_root": True, # Don't assign to root logger
"stream": "/path/to/my/special.log",
}
}
"""


LOGGING: Dict[str, Any] = {}
"""
Logging configuration.
The following configuration is applied by default::
LOGGING = {
'formatters': {
'default': {
'format': '%(asctime)s | %(levelname)s | %(name)s | %(message)s',
"formatters": {
"default": {
"format": "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'default',
'stream': 'ext://sys.stderr',
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
"stream": "ext://sys.stderr",
},
},
'root': {
# 'level' : 'INFO', # Set from command line arg parser.
'handlers': ['console'],
"root": {
# "level" : "INFO", # Set from command line arg parser.
"handlers": ["console"],
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/pyapp/conf/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@ def run(self):
"""
Run the report
"""
for key, setting in self.settings.__dict__.items():
if key.isupper():
self.output_result(key, setting)
settings = self.settings
for key in settings.keys:
self.output_result(key, getattr(settings, key))
23 changes: 21 additions & 2 deletions tests/conf/test_.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import pytest

import pyapp.conf
import pytest


class TestSettings:
Expand Down Expand Up @@ -133,3 +132,23 @@ def test_modify__multiple_changes_reversed(self, target: pyapp.conf.Settings):
assert target.SETTING_2 == 2
assert target.SETTING_3 == 3
assert not hasattr(target, "SETTING_6")

def test_modify__reset_settings(self, target: pyapp.conf.Settings):
known_keys = {
"UPPER_VALUE",
"SETTING_2",
"TEST_NAMED_FACTORY",
"TEST_ALIAS_FACTORY",
"TEST_PROVIDERS",
}

with target.modify() as patch:
patch.reset_settings()

assert all(not hasattr(target, key) for key in known_keys)
assert target.SETTINGS_SOURCES == []
assert not target.is_configured

# Check items have been restored
assert all(hasattr(target, key) for key in known_keys)
assert target.SETTINGS_SOURCES == ["python:tests.settings"]

0 comments on commit dcdcbe4

Please sign in to comment.