forked from orbit-oss/flask
Merge pull request #3564 from pallets/cli-ast-parse
use ast to parse FLASK_APP
This commit is contained in:
commit
2062d984ab
4 changed files with 67 additions and 40 deletions
|
|
@ -25,6 +25,8 @@ Unreleased
|
||||||
200 OK and an empty file. :issue:`3358`
|
200 OK and an empty file. :issue:`3358`
|
||||||
- When using ad-hoc certificates, check for the cryptography library
|
- When using ad-hoc certificates, check for the cryptography library
|
||||||
instead of PyOpenSSL. :pr:`3492`
|
instead of PyOpenSSL. :pr:`3492`
|
||||||
|
- When specifying a factory function with ``FLASK_APP``, keyword
|
||||||
|
argument can be passed. :issue:`3553`
|
||||||
|
|
||||||
|
|
||||||
Version 1.1.2
|
Version 1.1.2
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,8 @@ found, the command looks for a factory function named ``create_app`` or
|
||||||
``make_app`` that returns an instance.
|
``make_app`` that returns an instance.
|
||||||
|
|
||||||
If parentheses follow the factory name, their contents are parsed as
|
If parentheses follow the factory name, their contents are parsed as
|
||||||
Python literals and passed as arguments to the function. This means that
|
Python literals and passed as arguments and keyword arguments to the
|
||||||
strings must still be in quotes.
|
function. This means that strings must still be in quotes.
|
||||||
|
|
||||||
|
|
||||||
Run the Development Server
|
Run the Development Server
|
||||||
|
|
|
||||||
100
src/flask/cli.py
100
src/flask/cli.py
|
|
@ -86,55 +86,60 @@ def find_best_app(script_info, module):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def call_factory(script_info, app_factory, arguments=()):
|
def call_factory(script_info, app_factory, args=None, kwargs=None):
|
||||||
"""Takes an app factory, a ``script_info` object and optionally a tuple
|
"""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
|
of arguments. Checks for the existence of a script_info argument and calls
|
||||||
the app_factory depending on that and the arguments provided.
|
the app_factory depending on that and the arguments provided.
|
||||||
"""
|
"""
|
||||||
args_spec = inspect.getfullargspec(app_factory)
|
sig = inspect.signature(app_factory)
|
||||||
|
args = [] if args is None else args
|
||||||
|
kwargs = {} if kwargs is None else kwargs
|
||||||
|
|
||||||
if "script_info" in args_spec.args:
|
if "script_info" in sig.parameters:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The 'script_info' argument is deprecated and will not be"
|
"The 'script_info' argument is deprecated and will not be"
|
||||||
" passed to the app factory function in 2.1.",
|
" passed to the app factory function in 2.1.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
return app_factory(*arguments, script_info=script_info)
|
kwargs["script_info"] = script_info
|
||||||
elif arguments:
|
|
||||||
return app_factory(*arguments)
|
if (
|
||||||
elif not arguments and len(args_spec.args) == 1 and args_spec.defaults is None:
|
not args
|
||||||
|
and len(sig.parameters) == 1
|
||||||
|
and next(iter(sig.parameters.values())).default is inspect.Parameter.empty
|
||||||
|
):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Script info is deprecated and will not be passed as the"
|
"Script info is deprecated and will not be passed as the"
|
||||||
" first argument to the app factory function in 2.1.",
|
" single argument to the app factory function in 2.1.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
return app_factory(script_info)
|
args.append(script_info)
|
||||||
|
|
||||||
return app_factory()
|
return app_factory(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _called_with_wrong_args(factory):
|
def _called_with_wrong_args(f):
|
||||||
"""Check whether calling a function raised a ``TypeError`` because
|
"""Check whether calling a function raised a ``TypeError`` because
|
||||||
the call failed or because something in the factory raised the
|
the call failed or because something in the factory raised the
|
||||||
error.
|
error.
|
||||||
|
|
||||||
:param factory: the factory function that was called
|
:param f: The function that was called.
|
||||||
:return: true if the call failed
|
:return: ``True`` if the call failed.
|
||||||
"""
|
"""
|
||||||
tb = sys.exc_info()[2]
|
tb = sys.exc_info()[2]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while tb is not None:
|
while tb is not None:
|
||||||
if tb.tb_frame.f_code is factory.__code__:
|
if tb.tb_frame.f_code is f.__code__:
|
||||||
# in the factory, it was called successfully
|
# In the function, it was called successfully.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
tb = tb.tb_next
|
tb = tb.tb_next
|
||||||
|
|
||||||
# didn't reach the factory
|
# Didn't reach the function.
|
||||||
return True
|
return True
|
||||||
finally:
|
finally:
|
||||||
# explicitly delete tb as it is circular referenced
|
# Delete tb to break a circular reference.
|
||||||
# https://docs.python.org/2/library/sys.html#sys.exc_info
|
# https://docs.python.org/2/library/sys.html#sys.exc_info
|
||||||
del tb
|
del tb
|
||||||
|
|
||||||
|
|
@ -145,37 +150,60 @@ def find_app_by_string(script_info, module, app_name):
|
||||||
"""
|
"""
|
||||||
from . import Flask
|
from . import Flask
|
||||||
|
|
||||||
match = re.match(r"^ *([^ ()]+) *(?:\((.*?) *,? *\))? *$", app_name)
|
# Parse app_name as a single expression to determine if it's a valid
|
||||||
|
# attribute name or function call.
|
||||||
if not match:
|
try:
|
||||||
|
expr = ast.parse(app_name.strip(), mode="eval").body
|
||||||
|
except SyntaxError:
|
||||||
raise NoAppException(
|
raise NoAppException(
|
||||||
f"{app_name!r} is not a valid variable name or function expression."
|
f"Failed to parse {app_name!r} as an attribute name or function call."
|
||||||
)
|
)
|
||||||
|
|
||||||
name, args = match.groups()
|
if isinstance(expr, ast.Name):
|
||||||
|
name = expr.id
|
||||||
|
args = kwargs = None
|
||||||
|
elif isinstance(expr, ast.Call):
|
||||||
|
# Ensure the function name is an attribute name only.
|
||||||
|
if not isinstance(expr.func, ast.Name):
|
||||||
|
raise NoAppException(
|
||||||
|
f"Function reference must be a simple name: {app_name!r}."
|
||||||
|
)
|
||||||
|
|
||||||
|
name = expr.func.id
|
||||||
|
|
||||||
|
# Parse the positional and keyword arguments as literals.
|
||||||
|
try:
|
||||||
|
args = [ast.literal_eval(arg) for arg in expr.args]
|
||||||
|
kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords}
|
||||||
|
except ValueError:
|
||||||
|
# literal_eval gives cryptic error messages, show a generic
|
||||||
|
# message with the full expression instead.
|
||||||
|
raise NoAppException(
|
||||||
|
f"Failed to parse arguments as literal values: {app_name!r}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NoAppException(
|
||||||
|
f"Failed to parse {app_name!r} as an attribute name or function call."
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
attr = getattr(module, name)
|
attr = getattr(module, name)
|
||||||
except AttributeError as e:
|
except AttributeError:
|
||||||
raise NoAppException(e.args[0])
|
raise NoAppException(
|
||||||
|
f"Failed to find attribute {name!r} in {module.__name__!r}."
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the attribute is a function, call it with any args and kwargs
|
||||||
|
# to get the real application.
|
||||||
if inspect.isfunction(attr):
|
if inspect.isfunction(attr):
|
||||||
if args:
|
|
||||||
try:
|
|
||||||
args = ast.literal_eval(f"({args},)")
|
|
||||||
except (ValueError, SyntaxError):
|
|
||||||
raise NoAppException(f"Could not parse the arguments in {app_name!r}.")
|
|
||||||
else:
|
|
||||||
args = ()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = call_factory(script_info, attr, args)
|
app = call_factory(script_info, attr, args, kwargs)
|
||||||
except TypeError as e:
|
except TypeError:
|
||||||
if not _called_with_wrong_args(attr):
|
if not _called_with_wrong_args(attr):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
raise NoAppException(
|
raise NoAppException(
|
||||||
f"{e}\nThe factory {app_name!r} in module"
|
f"The factory {app_name!r} in module"
|
||||||
f" {module.__name__!r} could not be called with the"
|
f" {module.__name__!r} could not be called with the"
|
||||||
" specified arguments."
|
" specified arguments."
|
||||||
)
|
)
|
||||||
|
|
@ -362,8 +390,6 @@ class ScriptInfo:
|
||||||
if self._loaded_app is not None:
|
if self._loaded_app is not None:
|
||||||
return self._loaded_app
|
return self._loaded_app
|
||||||
|
|
||||||
app = None
|
|
||||||
|
|
||||||
if self.create_app is not None:
|
if self.create_app is not None:
|
||||||
app = call_factory(self, self.create_app)
|
app = call_factory(self, self.create_app)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,6 @@ def test_prepare_import(request, value, path, result):
|
||||||
("cliapp.factory", None, "app"),
|
("cliapp.factory", None, "app"),
|
||||||
("cliapp.factory", "create_app", "app"),
|
("cliapp.factory", "create_app", "app"),
|
||||||
("cliapp.factory", "create_app()", "app"),
|
("cliapp.factory", "create_app()", "app"),
|
||||||
# no script_info
|
|
||||||
("cliapp.factory", 'create_app2("foo", "bar")', "app2_foo_bar"),
|
("cliapp.factory", 'create_app2("foo", "bar")', "app2_foo_bar"),
|
||||||
# trailing comma space
|
# trailing comma space
|
||||||
("cliapp.factory", 'create_app2("foo", "bar", )', "app2_foo_bar"),
|
("cliapp.factory", 'create_app2("foo", "bar", )', "app2_foo_bar"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue