Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle app factory with arguments in FLASK_APP #2326

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 68 additions & 22 deletions flask/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
:license: BSD, see LICENSE for more details.
"""

import ast
import inspect
import os
import re
import sys
import traceback
from functools import update_wrapper
Expand Down Expand Up @@ -55,20 +58,20 @@ def find_best_app(script_info, module):
' one.'.format(module=module.__name__)
)

# Search for app factory callables.
# Search for app factory functions.
for attr_name in ('create_app', 'make_app'):
app_factory = getattr(module, attr_name, None)

if callable(app_factory):
if inspect.isfunction(app_factory):
try:
app = call_factory(app_factory, script_info)
if isinstance(app, Flask):
return app
except TypeError:
raise NoAppException(
'Auto-detected "{callable}()" in module "{module}", but '
'Auto-detected "{function}()" in module "{module}", but '
'could not call it without specifying arguments.'.format(
callable=attr_name, module=module.__name__
function=attr_name, module=module.__name__
)
)

Expand All @@ -79,18 +82,66 @@ def find_best_app(script_info, module):
)


def call_factory(func, script_info):
"""Checks if the given app factory function has an argument named
``script_info`` or just a single argument and calls the function passing
``script_info`` if so. Otherwise, calls the function without any arguments
and returns the result.
def call_factory(app_factory, script_info, arguments=()):
"""Takes an app factory, a ``script_info` object and optionally a tuple
of arguments. Checks for the existence of a script_info argument and calls
the app_factory depending on that and the arguments provided.
"""
arguments = getargspec(func).args
if 'script_info' in arguments:
return func(script_info=script_info)
elif len(arguments) == 1:
return func(script_info)
return func()
arg_names = getargspec(app_factory).args
if 'script_info' in arg_names:
return app_factory(*arguments, script_info=script_info)
elif arguments:
return app_factory(*arguments)
elif not arguments and len(arg_names) == 1:
return app_factory(script_info)
return app_factory()


def find_app_by_string(string, script_info, module):
"""Checks if the given string is a variable name or a function. If it is
a function, it checks for specified arguments and whether it takes
a ``script_info`` argument and calls the function with the appropriate
arguments. If it is a """
davidism marked this conversation as resolved.
Show resolved Hide resolved
from . import Flask
function_regex = r'^(?P<name>\w+)(?:\((?P<args>.*)\))?$'
match = re.match(function_regex, string)
if match:
name, args = match.groups()
try:
if args is not None:
args = args.rstrip(' ,')
if args:
args = ast.literal_eval(
"({args}, )".format(args=args))
else:
args = ()
app_factory = getattr(module, name, None)
app = call_factory(app_factory, script_info, args)
else:
attr = getattr(module, name, None)
if inspect.isfunction(attr):
app = call_factory(attr, script_info)
else:
app = attr

if isinstance(app, Flask):
return app
else:
raise RuntimeError('Failed to find application in module '
'"{name}"'.format(name=module))
except TypeError as e:
new_error = NoAppException(
'{e}\nThe app factory "{factory}" in module "{module}" could'
' not be called with the specified arguments (and a'
' script_info argument automatically added if applicable).'
' Did you make sure to use the right number of arguments as'
' well as not using keyword arguments or'
' non-literals?'.format(e=e, factory=string, module=module))
reraise(NoAppException, new_error, sys.exc_info()[2])
else:
raise NoAppException(
'The provided string "{string}" is not a valid variable name'
'or function expression.'.format(string=string))


def prepare_exec_for_file(filename):
Expand Down Expand Up @@ -148,14 +199,9 @@ def locate_app(script_info, app_id):

mod = sys.modules[module]
if app_obj is None:
app = find_best_app(script_info, mod)
return find_best_app(script_info, mod)
else:
app = getattr(mod, app_obj, None)
if app is None:
raise RuntimeError('Failed to find application in module "%s"'
% module)

return app
return find_app_by_string(app_obj, script_info, mod)


def find_default_import_path():
Expand Down
15 changes: 15 additions & 0 deletions tests/test_apps/cliapp/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import absolute_import, print_function

from flask import Flask


def create_app():
return Flask('create_app')


def create_app2(foo, bar):
return Flask("_".join(['create_app2', foo, bar]))


def create_app3(foo, bar, script_info):
return Flask("_".join(['create_app3', foo, bar]))
38 changes: 30 additions & 8 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,37 @@ def test_locate_app(test_apps):
script_info = ScriptInfo()
assert locate_app(script_info, "cliapp.app").name == "testapp"
assert locate_app(script_info, "cliapp.app:testapp").name == "testapp"
assert locate_app(script_info, "cliapp.factory").name == "create_app"
assert locate_app(
script_info, "cliapp.factory").name == "create_app"
assert locate_app(
script_info, "cliapp.factory:create_app").name == "create_app"
assert locate_app(
script_info, "cliapp.factory:create_app()").name == "create_app"
assert locate_app(
script_info, "cliapp.factory:create_app2('foo', 'bar')"
).name == "create_app2_foo_bar"
assert locate_app(
script_info, "cliapp.factory:create_app2('foo', 'bar', )"
).name == "create_app2_foo_bar"
assert locate_app(
script_info, "cliapp.factory:create_app3('baz', 'qux')"
).name == "create_app3_baz_qux"
assert locate_app(script_info, "cliapp.multiapp:app1").name == "app1"
pytest.raises(NoAppException, locate_app,
script_info, "notanpp.py")
pytest.raises(NoAppException, locate_app,
script_info, "cliapp/app")
pytest.raises(RuntimeError, locate_app,
script_info, "cliapp.app:notanapp")
pytest.raises(NoAppException, locate_app,
script_info, "cliapp.importerrorapp")
pytest.raises(
NoAppException, locate_app, script_info, "notanpp.py")
pytest.raises(
NoAppException, locate_app, script_info, "cliapp/app")
pytest.raises(
RuntimeError, locate_app, script_info, "cliapp.app:notanapp")
pytest.raises(
NoAppException, locate_app,
script_info, "cliapp.factory:create_app2('foo')")
pytest.raises(
NoAppException, locate_app,
script_info, "cliapp.factory:create_app ()")
pytest.raises(
NoAppException, locate_app, script_info, "cliapp.importerrorapp")


def test_find_default_import_path(test_apps, monkeypatch, tmpdir):
Expand Down