Skip to content

Commit

Permalink
Add web app config support.
Browse files Browse the repository at this point in the history
For #177.
  • Loading branch information
lemon24 committed Aug 18, 2020
1 parent 76d41f4 commit f2c0b0e
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 38 deletions.
33 changes: 19 additions & 14 deletions src/reader/_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
from reader import Entry
from reader import EntrySearchResult
from reader import InvalidSearchQueryError
from reader import make_reader
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 +47,10 @@

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


Expand Down Expand Up @@ -232,8 +233,12 @@ def preview():
# TODO: maybe redirect to the feed we have if we already have it

# TODO: maybe cache stuff
reader = make_reader(':memory:')
current_app.reader_load_plugins(reader)

# 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.add_feed(url)

Expand Down Expand Up @@ -439,25 +444,25 @@ def handle_error(self, exception, cause):
)


def create_app(db_path, plugins=(), app_plugins=()):
def create_app(config):
app = Flask(__name__)
app.secret_key = 'secret'

app.config['READER_DB'] = db_path
app.config['READER_CONFIG'] = config
app.teardown_appcontext(close_db)

# Force reader plugins to load, so we can fail fast for import errors
# (although some errors may just be logged while we use FlaskPluginLoader).
with app.app_context():
get_reader()

app.register_blueprint(blueprint)

# NOTE: this is part of the app extension API
app.reader_additional_enclosure_links = []

app.reader_load_plugins = FlaskPluginLoader(plugins).load_plugins
# Force reader plugins to load, so we can fail fast for import errors.
with app.app_context():
get_reader()

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

return app
6 changes: 1 addition & 5 deletions src/reader/_app/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,5 @@ def serve(config, host, port, plugin, verbose):
config['app'] = merge_config(default_options, config['app'], user_options)

# FIXME: once create_app knows how to work from config, change these
app = create_app(
config['reader']['url'],
tuple(config['reader'].get('plugins', ())),
tuple(config['app'].get('plugins', ())),
)
app = create_app(config)
run_simple(host, port, app)
4 changes: 3 additions & 1 deletion src/reader/_app/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
position: fixed;
bottom: 0;
right: 0;
"><code>{{ config.READER_DB }}</code></p>
"
title='{{ config.READER_CONFIG | tojson(indent=4) | escape }}'
><code>{{ config.READER_CONFIG.reader.url }}</code></p>
{% endif %}


Expand Down
33 changes: 27 additions & 6 deletions src/reader/_app/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,40 @@
To run a local development server:
FLASK_DEBUG=1 FLASK_TRAP_BAD_REQUEST_ERRORS=1 \
FLASK_APP=src/reader/app/wsgi.py \
READER_DB=db.sqlite flask run -h 0.0.0.0 -p 8000
FLASK_APP=src/reader/_app/wsgi.py \
READER_CONFIG=examples/config.yaml READER_DB=db.sqlite \
flask run -h 0.0.0.0 -p 8000
"""
import os

import reader._app
import reader._config

app = reader._app.create_app(
os.environ[reader._DB_ENVVAR],
os.environ.get(reader._PLUGIN_ENVVAR, '').split(),
os.environ.get(reader._APP_PLUGIN_ENVVAR, '').split(),

# TODO: this if is for compatibility only, remove after config is released
if reader._CONFIG_ENVVAR in os.environ:
with open(os.environ[reader._CONFIG_ENVVAR]) as file:
config = reader._config.load_config(file)
else:
config = reader._config.load_config({'reader': {}, 'app': {}})


# TODO: this is for compatibility only, remove after config is released
user_config = {'reader': {}, 'app': {}}
if reader._DB_ENVVAR in os.environ:
user_config['reader']['url'] = os.environ[reader._DB_ENVVAR]
user_config['reader']['plugins'] = dict.fromkeys(
os.environ.get(reader._PLUGIN_ENVVAR, '').split()
)
user_config['app']['plugins'] = dict.fromkeys(
os.environ.get(reader._APP_PLUGIN_ENVVAR, '').split()
)
for key in 'reader', 'app':
config[key] = reader._config.merge_config(config[key], user_config[key])


app = reader._app.create_app(config)
app.config['TRAP_BAD_REQUEST_ERRORS'] = bool(
os.environ.get('FLASK_TRAP_BAD_REQUEST_ERRORS', '')
)
4 changes: 2 additions & 2 deletions src/reader/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
MERGE_KWARGS = ('plugins',)


def make_reader_from_config(*, plugins=None, **kwargs):
def make_reader_from_config(*, plugins=None, plugin_loader_cls=Loader, **kwargs):
"""Like reader.make_reader(), but:
* If *_cls arguments are str, import them.
Expand All @@ -32,7 +32,7 @@ def make_reader_from_config(*, plugins=None, **kwargs):
reader = make_reader(**kwargs)

try:
Loader(plugins).load_plugins(reader)
plugin_loader_cls(plugins).load_plugins(reader)
except Exception:
reader.close()
raise
Expand Down
11 changes: 6 additions & 5 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

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


@pytest.fixture
def browser(db_path):
app = create_app(db_path)
app = create_app({'reader': {'url': db_path}})
session = requests.Session()
session.mount('http://app/', wsgiadapter.WSGIAdapter(app))
browser = mechanicalsoup.StatefulBrowser(session)
Expand Down Expand Up @@ -99,15 +100,15 @@ def test_add_delete_feed(db_path, browser, monkeypatch):
feed = parser.feed(1, datetime(2010, 1, 1))
entry = parser.entry(1, 1, datetime(2010, 1, 1))

def app_make_reader(db_path):
reader = make_reader(db_path)
def app_make_reader(**kwargs):
reader = make_reader_from_config(**kwargs)
reader._parser = parser
return reader

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

reader = app_make_reader(db_path)
reader = app_make_reader(url=db_path)

browser.open('http://app/')
response = browser.follow_link(browser.find_link(text='feeds'))
Expand Down
12 changes: 7 additions & 5 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,16 @@ def test_cli_serve_calls_create_app(db_path, monkeypatch):

exception = Exception("create_app error")

def create_app(*args):
assert args == (db_path, (), ())
def create_app(config):
create_app.config = config
raise exception

monkeypatch.setattr('reader._app.create_app', create_app)

runner = CliRunner()
result = runner.invoke(cli, ['--db', db_path, 'serve'])

assert result.exit_code != 0
assert result.exception == exception
with pytest.raises(Exception) as excinfo:
runner.invoke(cli, ['--db', db_path, 'serve'], catch_exceptions=False)

assert excinfo.value is exception
assert create_app.config == {'reader': {'url': db_path}, 'app': {}, 'cli': {}}

0 comments on commit f2c0b0e

Please sign in to comment.