Skip to content

Commit

Permalink
Add config object helper.
Browse files Browse the repository at this point in the history
For #177.
  • Loading branch information
lemon24 committed Aug 19, 2020
1 parent 754ede0 commit bc15e50
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 121 deletions.
16 changes: 6 additions & 10 deletions src/reader/_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from reader import InvalidSearchQueryError
from reader import ParseError
from reader import ReaderError
from reader._config import make_reader_from_config
from reader._plugins import Loader
from reader._plugins import LoaderError

Expand All @@ -47,9 +46,8 @@

def get_reader():
if not hasattr(g, 'reader'):
g.reader = make_reader_from_config(
plugin_loader_cls=FlaskPluginLoader,
**current_app.config['READER_CONFIG']['reader'],
g.reader = current_app.config['READER_CONFIG'].make_reader(
'default', plugin_loader_cls=FlaskPluginLoader
)
return g.reader

Expand Down Expand Up @@ -234,11 +232,9 @@ def preview():

# TODO: maybe cache stuff

# TODO: config should have a helper to do this
kwargs = current_app.config['READER_CONFIG']['reader'].copy()
kwargs['url'] = ':memory:'
current_app.config['READER_CONFIG']['reader']
reader = make_reader_from_config(**kwargs, plugin_loader_cls=FlaskPluginLoader)
reader = current_app.config['READER_CONFIG'].make_reader(
'default', url=':memory:', plugin_loader_cls=FlaskPluginLoader
)

reader.add_feed(url)

Expand Down Expand Up @@ -463,6 +459,6 @@ def create_app(config):

# app_context() needed for logging to work.
with app.app_context():
FlaskPluginLoader(config.get('app', {}).get('plugins', {})).load_plugins(app)
FlaskPluginLoader(config.merged('app').get('plugins', {})).load_plugins(app)

return app
2 changes: 1 addition & 1 deletion src/reader/_app/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def serve(config, host, port, plugin, verbose):
config['app']['plugins'] = dict.fromkeys(plugin)

# FIXME: remove this once we make debug_storage a storage_arg
config['reader'].pop('debug_storage', None)
config['default']['reader'].pop('debug_storage', None)

app = create_app(config)
run_simple(host, port, app)
9 changes: 4 additions & 5 deletions src/reader/_app/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,16 @@
with open(os.environ[reader._CONFIG_ENVVAR]) as file:
config = reader._config.load_config(file)
else:
config = reader._config.load_config({'reader': {}, 'app': {}})
config = reader._config.load_config({})

# TODO: if we ever merge sections, this needs to become: config[*]['reader']['url'] = ...
if reader._DB_ENVVAR in os.environ:
config['reader']['url'] = os.environ[reader._DB_ENVVAR]
config.all['reader']['url'] = os.environ[reader._DB_ENVVAR]
if reader._PLUGIN_ENVVAR in os.environ:
config['reader']['plugins'] = dict.fromkeys(
config.all['reader']['plugins'] = dict.fromkeys(
os.environ[reader._PLUGIN_ENVVAR].split()
)
if reader._APP_PLUGIN_ENVVAR in os.environ:
config['app']['plugins'] = dict.fromkeys(
config.data['app']['plugins'] = dict.fromkeys(
os.environ[reader._APP_PLUGIN_ENVVAR].split()
)

Expand Down
36 changes: 10 additions & 26 deletions src/reader/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from ._config import make_reader_from_config
from ._plugins import LoaderError
from ._sqlite_utils import DebugConnection
from .types import MISSING


APP_NAME = reader.__name__
Expand Down Expand Up @@ -124,18 +123,6 @@ def wrapper(*args, **kwargs):
return wrapper


def get_dict_path(d, path):
for key in path[:-1]:
d = d.get(key, {})
return d.get(path[-1], MISSING)


def set_dict_path(d, path, value):
for key in path[:-1]:
d = d.setdefault(key, {})
d[path[-1]] = value


def config_option(*args, **kwargs):
def callback(ctx, param, value):
# TODO: the default file is allowed to not exist, a user specified file must exist
Expand All @@ -145,9 +132,9 @@ def callback(ctx, param, value):
except FileNotFoundError as e:
if value != param.default:
raise click.BadParameter(str(e), ctx=ctx, param=param)
config = {}
config = load_config({})

ctx.default_map = config.get('cli', {}).get('defaults', {})
ctx.default_map = config['cli'].get('defaults', {})

ctx.obj = config
return config
Expand All @@ -169,7 +156,8 @@ def pass_reader(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
ctx = click.get_current_context().find_root()
reader = make_reader_with_plugins(**ctx.obj['reader'])
# TODO: replace with ctx.obj.make_reader('cli') when we get rid of debug_storage
reader = make_reader_with_plugins(**ctx.obj.merged('cli').get('reader', {}))
ctx.call_on_close(reader.close)
return fn(reader, *args, **kwargs)

Expand Down Expand Up @@ -209,27 +197,23 @@ def cli(config, db, plugin, debug_storage):
# (same for wsgi envvars)
# NOTE: we can never use click defaults for --db/--plugin, because they would override the config always

# TODO: remove me after we have object
for key in 'reader', 'cli', 'app':
config.setdefault(key, {})

# TODO: if we ever merge sections, this needs to become: config[*]['reader']['url'] = ...
if db:
config['reader']['url'] = db
config.all['reader']['url'] = db
else:
if not config['reader'].get('url'):
# ... could be the 'cli' section, maybe...
if not config['default'].get('reader', {}).get('url'):
try:
db = get_default_db_path(create_dir=True)
except Exception as e:
abort("{}", e)
config['reader']['url'] = db
config.all['reader']['url'] = db

if plugin:
config['reader']['plugins'] = dict.fromkeys(plugin)
config.all['reader']['plugins'] = dict.fromkeys(plugin)

# until we make debug_storage a proper make_reader argument,
# and we get rid of make_reader_with_plugins
config['reader']['debug_storage'] = debug_storage
config['default']['reader']['debug_storage'] = debug_storage


@cli.command()
Expand Down
102 changes: 86 additions & 16 deletions src/reader/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
"""
from collections.abc import Mapping
from dataclasses import dataclass
from dataclasses import field

from reader import make_reader
from reader._plugins import import_string
from reader._plugins import Loader


IMPORT_KWARGS = ('storage_cls', 'search_cls')
MERGE_KWARGS = ('plugins',)
MAKE_READER_IMPORT_KWARGS = ('storage_cls', 'search_cls')


def make_reader_from_config(*, plugins=None, plugin_loader_cls=Loader, **kwargs):
Expand All @@ -24,7 +25,7 @@ def make_reader_from_config(*, plugins=None, plugin_loader_cls=Loader, **kwargs)
"""
plugins = plugins or {}

for name in IMPORT_KWARGS:
for name in MAKE_READER_IMPORT_KWARGS:
thing = kwargs.get(name)
if thing and isinstance(thing, str):
kwargs[name] = import_string(thing)
Expand All @@ -40,7 +41,22 @@ def make_reader_from_config(*, plugins=None, plugin_loader_cls=Loader, **kwargs)
return reader


def merge_config(*configs, merge_kwargs=MERGE_KWARGS):
def load_config(thing):
if isinstance(thing, Mapping):
config = thing
else:
import yaml

config = yaml.safe_load(thing)
if not isinstance(config, Mapping):
raise ValueError("config must be a mapping")

# TODO: validate / raise nicer exceptions here

return Config(config, sections={'cli', 'app'}, merge_keys={'reader', 'plugins'})


def _merge_config(*configs, merge_keys=()):
"""Merge multiple make_app_from_config() kwargs dicts into a single one.
plugins is assumed to be a dict and is merged. All other keys are replaced.
Expand All @@ -51,26 +67,80 @@ def merge_config(*configs, merge_kwargs=MERGE_KWARGS):

for config in configs:
config = config.copy()
for name in MERGE_KWARGS:
for name in merge_keys:
if name in config:
to_merge.setdefault(name, []).append(config.pop(name))
rv.update(config)

for name, dicts in to_merge.items():
rv[name] = merge_config(*dicts, merge_kwargs=())
rv[name] = _merge_config(*dicts, merge_keys=())

return rv


def load_config(thing):
if isinstance(thing, Mapping):
config = thing
else:
import yaml
@dataclass
class Config:

config = yaml.safe_load(thing)
if not isinstance(config, Mapping):
raise ValueError("config must be a mapping")
data: dict = field(default_factory=dict)
sections: set = field(default_factory=set)
merge_keys: set = field(default_factory=set)
default_section: str = 'default'

# TODO: validate / raise nicer exceptions here
return config
def __post_init__(self):
self.sections.add(self.default_section)

unknown_sections = self.data.keys() - self.sections

if self.default_section in self.data:
if unknown_sections:
raise ValueError(f"unknown sections in config: {unknown_sections!r}")
else:
self.data[self.default_section] = {
section: self.data.pop(section) for section in unknown_sections
}

for section in self.sections:
self.data.setdefault(section, {})

def merged(self, section, overrides=None):
if section not in self.sections:
raise ValueError(f"unknown section: {section!r}")

return _merge_config(
self.data[self.default_section],
self.data[section],
overrides or {},
merge_keys=self.merge_keys,
)

def make_reader(self, section, **kwargs):
return make_reader_from_config(
**self.merged(section, {'reader': kwargs}).get('reader', {}),
)

@property
def all(self):
return MultiMapping(list(self.data.values()))

def __getitem__(self, key):
return self.data[key]


@dataclass
class MultiMapping:

mappings: list = field(default_factory=list)
default_factory: callable = dict

def __getitem__(self, key):
return MultiMapping(
[
mapping.setdefault(key, self.default_factory())
for mapping in self.mappings
],
self.default_factory,
)

def __setitem__(self, key, value):
for mapping in self.mappings:
mapping[key] = value
5 changes: 3 additions & 2 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

from reader import make_reader
from reader._app import create_app
from reader._config import load_config
from reader._config import make_reader_from_config


@pytest.fixture
def browser(db_path):
app = create_app({'reader': {'url': db_path}})
app = create_app(load_config({'reader': {'url': db_path}}))
session = requests.Session()
session.mount('http://app/', wsgiadapter.WSGIAdapter(app))
browser = mechanicalsoup.StatefulBrowser(session)
Expand Down Expand Up @@ -106,7 +107,7 @@ def app_make_reader(**kwargs):
return reader

# this is brittle, it may break if we change how we use make_reader in app
monkeypatch.setattr('reader._app.make_reader_from_config', app_make_reader)
monkeypatch.setattr('reader._config.make_reader_from_config', app_make_reader)

reader = app_make_reader(url=db_path)

Expand Down
Loading

0 comments on commit bc15e50

Please sign in to comment.