Browse Source

allow local packages in FLASK_APP

don't require .py extension in FLASK_APP
add tests for nested package loading
parametrize cli loading tests
pull/2414/head
David Lord 7 years ago
parent
commit
fb845b9032
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
  1. 3
      CHANGES
  2. 125
      flask/cli.py
  3. 8
      tests/test_apps/cliapp/factory.py
  4. 3
      tests/test_apps/cliapp/inner1/__init__.py
  5. 0
      tests/test_apps/cliapp/inner1/inner2/__init__.py
  6. 3
      tests/test_apps/cliapp/inner1/inner2/flask.py
  7. 1
      tests/test_apps/cliapp/message.txt
  8. 170
      tests/test_cli.py

3
CHANGES

@ -52,6 +52,8 @@ Major release, unreleased
- FLASK_APP=myproject.app:create_app('dev') support. - FLASK_APP=myproject.app:create_app('dev') support.
- ``FLASK_APP`` can be set to an app factory, with arguments if needed, for - ``FLASK_APP`` can be set to an app factory, with arguments if needed, for
example ``FLASK_APP=myproject.app:create_app('dev')``. (`#2326`_) example ``FLASK_APP=myproject.app:create_app('dev')``. (`#2326`_)
- ``FLASK_APP`` can point to local packages that are not installed in dev mode,
although `pip install -e` should still be preferred. (`#2414`_)
- ``View.provide_automatic_options = True`` is set on the view function from - ``View.provide_automatic_options = True`` is set on the view function from
``View.as_view``, to be detected in ``app.add_url_rule``. (`#2316`_) ``View.as_view``, to be detected in ``app.add_url_rule``. (`#2316`_)
- Error handling will try handlers registered for ``blueprint, code``, - Error handling will try handlers registered for ``blueprint, code``,
@ -127,6 +129,7 @@ Major release, unreleased
.. _#2373: https://github.com/pallets/flask/pull/2373 .. _#2373: https://github.com/pallets/flask/pull/2373
.. _#2385: https://github.com/pallets/flask/issues/2385 .. _#2385: https://github.com/pallets/flask/issues/2385
.. _#2412: https://github.com/pallets/flask/pull/2412 .. _#2412: https://github.com/pallets/flask/pull/2412
.. _#2414: https://github.com/pallets/flask/pull/2414
Version 0.12.2 Version 0.12.2
-------------- --------------

125
flask/cli.py

@ -130,7 +130,7 @@ def find_app_by_string(string, script_info, module):
if isinstance(app, Flask): if isinstance(app, Flask):
return app return app
else: else:
raise RuntimeError('Failed to find application in module ' raise NoAppException('Failed to find application in module '
'"{name}"'.format(name=module)) '"{name}"'.format(name=module))
except TypeError as e: except TypeError as e:
new_error = NoAppException( new_error = NoAppException(
@ -147,85 +147,61 @@ def find_app_by_string(string, script_info, module):
'or function expression.'.format(string=string)) 'or function expression.'.format(string=string))
def prepare_exec_for_file(filename): def prepare_import(path):
"""Given a filename this will try to calculate the python path, add it """Given a filename this will try to calculate the python path, add it
to the search path and return the actual module name that is expected. to the search path and return the actual module name that is expected.
""" """
module = [] path = os.path.realpath(path)
# Chop off file extensions or package markers if os.path.splitext(path)[1] == '.py':
if os.path.split(filename)[1] == '__init__.py': path = os.path.splitext(path)[0]
filename = os.path.dirname(filename)
elif filename.endswith('.py'): if os.path.basename(path) == '__init__':
filename = filename[:-3] path = os.path.dirname(path)
else:
raise NoAppException('The file provided (%s) does exist but is not a ' module_name = []
'valid Python file. This means that it cannot '
'be used as application. Please change the ' # move up until outside package structure (no __init__.py)
'extension to .py' % filename) while True:
filename = os.path.realpath(filename) path, name = os.path.split(path)
module_name.append(name)
dirpath = filename
while 1: if not os.path.exists(os.path.join(path, '__init__.py')):
dirpath, extra = os.path.split(dirpath)
module.append(extra)
if not os.path.isfile(os.path.join(dirpath, '__init__.py')):
break break
if sys.path[0] != dirpath: if sys.path[0] != path:
sys.path.insert(0, dirpath) sys.path.insert(0, path)
return '.'.join(module[::-1]) return '.'.join(module_name[::-1])
def locate_app(script_info, app_id, raise_if_not_found=True): def locate_app(script_info, module_name, app_name, raise_if_not_found=True):
"""Attempts to locate the application.""" """Attempts to locate the application."""
__traceback_hide__ = True __traceback_hide__ = True
if ':' in app_id:
module, app_obj = app_id.split(':', 1)
else:
module = app_id
app_obj = None
try: try:
__import__(module) __import__(module_name)
except ImportError: except ImportError:
# Reraise the ImportError if it occurred within the imported module. # Reraise the ImportError if it occurred within the imported module.
# Determine this by checking whether the trace has a depth > 1. # Determine this by checking whether the trace has a depth > 1.
if sys.exc_info()[-1].tb_next: if sys.exc_info()[-1].tb_next:
stack_trace = traceback.format_exc()
raise NoAppException( raise NoAppException(
'There was an error trying to import the app ({module}):\n' 'While importing "{name}", an ImportError was raised:'
'{stack_trace}'.format( '\n\n{tb}'.format(name=module_name, tb=traceback.format_exc())
module=module, stack_trace=stack_trace
)
) )
elif raise_if_not_found: elif raise_if_not_found:
raise NoAppException( raise NoAppException(
'The file/path provided ({module}) does not appear to exist.' 'Could not import "{name}"."'.format(name=module_name)
' Please verify the path is correct. If app is not on'
' PYTHONPATH, ensure the extension is .py.'.format(
module=module)
) )
else: else:
return return
mod = sys.modules[module] module = sys.modules[module_name]
if app_obj is None: if app_name is None:
return find_best_app(script_info, mod) return find_best_app(script_info, module)
else: else:
return find_app_by_string(app_obj, script_info, mod) return find_app_by_string(app_name, script_info, module)
def find_default_import_path():
app = os.environ.get('FLASK_APP')
if app is None:
return
if os.path.isfile(app):
return prepare_exec_for_file(app)
return app
def get_version(ctx, param, value): def get_version(ctx, param, value):
@ -308,15 +284,8 @@ class ScriptInfo(object):
""" """
def __init__(self, app_import_path=None, create_app=None): def __init__(self, app_import_path=None, create_app=None):
if create_app is None:
if app_import_path is None:
app_import_path = find_default_import_path()
self.app_import_path = app_import_path
else:
app_import_path = None
#: Optionally the import path for the Flask application. #: Optionally the import path for the Flask application.
self.app_import_path = app_import_path self.app_import_path = app_import_path or os.environ.get('FLASK_APP')
#: Optionally a function that is passed the script info to create #: Optionally a function that is passed the script info to create
#: the instance of the application. #: the instance of the application.
self.create_app = create_app self.create_app = create_app
@ -335,37 +304,39 @@ class ScriptInfo(object):
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:
rv = call_factory(self.create_app, self) app = call_factory(self.create_app, self)
else: else:
if self.app_import_path: if self.app_import_path:
rv = locate_app(self, self.app_import_path) path, name = (self.app_import_path.split(':', 1) + [None])[:2]
import_name = prepare_import(path)
app = locate_app(self, import_name, name)
else: else:
for module in ['wsgi.py', 'app.py']: for path in ('wsgi.py', 'app.py'):
import_path = prepare_exec_for_file(module) import_name = prepare_import(path)
rv = locate_app( app = locate_app(
self, import_path, raise_if_not_found=False self, import_name, None, raise_if_not_found=False
) )
if rv: if app:
break break
if not rv: if not app:
raise NoAppException( raise NoAppException(
'Could not locate Flask application. You did not provide ' 'Could not locate a Flask application. You did not provide '
'the FLASK_APP environment variable, and a wsgi.py or ' 'the "FLASK_APP" environment variable, and a "wsgi.py" or '
'app.py module was not found in the current directory.\n\n' '"app.py" module was not found in the current directory.'
'For more information see '
'http://flask.pocoo.org/docs/latest/quickstart/'
) )
debug = get_debug_flag() debug = get_debug_flag()
if debug is not None: if debug is not None:
rv._reconfigure_for_run_debug(debug) app._reconfigure_for_run_debug(debug)
self._loaded_app = rv self._loaded_app = app
return rv return app
pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True)

8
tests/test_apps/cliapp/factory.py

@ -4,12 +4,12 @@ from flask import Flask
def create_app(): def create_app():
return Flask('create_app') return Flask('app')
def create_app2(foo, bar): def create_app2(foo, bar):
return Flask("_".join(['create_app2', foo, bar])) return Flask('_'.join(['app2', foo, bar]))
def create_app3(foo, bar, script_info): def create_app3(foo, script_info):
return Flask("_".join(['create_app3', foo, bar])) return Flask('_'.join(['app3', foo, script_info.data['test']]))

3
tests/test_apps/cliapp/inner1/__init__.py

@ -0,0 +1,3 @@
from flask import Flask
application = Flask(__name__)

0
tests/test_apps/cliapp/inner1/inner2/__init__.py

3
tests/test_apps/cliapp/inner1/inner2/flask.py

@ -0,0 +1,3 @@
from flask import Flask
app = Flask(__name__)

1
tests/test_apps/cliapp/message.txt

@ -0,0 +1 @@
So long, and thanks for all the fish.

170
tests/test_cli.py

@ -11,7 +11,8 @@
# the Revised BSD License. # the Revised BSD License.
# Copyright (C) 2015 CERN. # Copyright (C) 2015 CERN.
# #
from __future__ import absolute_import, print_function from __future__ import absolute_import
import os import os
import sys import sys
from functools import partial from functools import partial
@ -19,11 +20,10 @@ from functools import partial
import click import click
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from flask import Flask, current_app
from flask.cli import cli, AppGroup, FlaskGroup, NoAppException, ScriptInfo, \ from flask import Flask, current_app
find_best_app, locate_app, with_appcontext, prepare_exec_for_file, \ from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \
find_default_import_path, get_version find_best_app, get_version, locate_app, prepare_import, with_appcontext
@pytest.fixture @pytest.fixture
@ -125,78 +125,104 @@ def test_find_best_app(test_apps):
pytest.raises(NoAppException, find_best_app, script_info, Module) pytest.raises(NoAppException, find_best_app, script_info, Module)
def test_prepare_exec_for_file(test_apps): cwd = os.getcwd()
"""Expect the correct path to be set and the correct module name to be returned. test_path = os.path.abspath(os.path.join(
os.path.dirname(__file__), 'test_apps'
:func:`prepare_exec_for_file` has a side effect, where ))
the parent directory of given file is added to `sys.path`.
@pytest.mark.parametrize('value,path,result', (
('test', cwd, 'test'),
('test.py', cwd, 'test'),
('a/test', os.path.join(cwd, 'a'), 'test'),
('test/__init__.py', cwd, 'test'),
('test/__init__', cwd, 'test'),
# nested package
(
os.path.join(test_path, 'cliapp', 'inner1', '__init__'),
test_path, 'cliapp.inner1'
),
(
os.path.join(test_path, 'cliapp', 'inner1', 'inner2'),
test_path, 'cliapp.inner1.inner2'
),
# dotted name
('test.a.b', cwd, 'test.a.b'),
(os.path.join(test_path, 'cliapp.app'), test_path, 'cliapp.app'),
# not a Python file, will be caught during import
(
os.path.join(test_path, 'cliapp', 'message.txt'),
test_path, 'cliapp.message.txt'
),
))
def test_prepare_import(request, value, path, result):
"""Expect the correct path to be set and the correct import and app names
to be returned.
:func:`prepare_exec_for_file` has a side effect where the parent directory
of the given import is added to :data:`sys.path`. This is reset after the
test runs.
""" """
realpath = os.path.realpath('/tmp/share/test.py') original_path = sys.path[:]
dirname = os.path.dirname(realpath)
assert prepare_exec_for_file('/tmp/share/test.py') == 'test' def reset_path():
assert dirname in sys.path sys.path[:] = original_path
realpath = os.path.realpath('/tmp/share/__init__.py') request.addfinalizer(reset_path)
dirname = os.path.dirname(os.path.dirname(realpath))
assert prepare_exec_for_file('/tmp/share/__init__.py') == 'share' assert prepare_import(value) == result
assert dirname in sys.path assert sys.path[0] == path
@pytest.mark.parametrize('iname,aname,result', (
('cliapp.app', None, 'testapp'),
('cliapp.app', 'testapp', 'testapp'),
('cliapp.factory', None, 'app'),
('cliapp.factory', 'create_app', 'app'),
('cliapp.factory', 'create_app()', 'app'),
# no script_info
('cliapp.factory', 'create_app2("foo", "bar")', 'app2_foo_bar'),
# trailing comma space
('cliapp.factory', 'create_app2("foo", "bar", )', 'app2_foo_bar'),
# takes script_info
('cliapp.factory', 'create_app3("foo")', 'app3_foo_spam'),
))
def test_locate_app(test_apps, iname, aname, result):
info = ScriptInfo()
info.data['test'] = 'spam'
assert locate_app(info, iname, aname).name == result
@pytest.mark.parametrize('iname,aname', (
('notanapp.py', None),
('cliapp/app', None),
('cliapp.app', 'notanapp'),
# not enough arguments
('cliapp.factory', 'create_app2("foo")'),
# nested import error
('cliapp.importerrorapp', None),
# not a Python file
('cliapp.message.txt', None),
# space before arg list
('cliapp.factory', 'create_app ()'),
))
def test_locate_app_raises(test_apps, iname, aname):
info = ScriptInfo()
with pytest.raises(NoAppException): with pytest.raises(NoAppException):
prepare_exec_for_file('/tmp/share/test.txt') locate_app(info, iname, aname)
def test_locate_app(test_apps): def test_locate_app_suppress_raise():
"""Test of locate_app.""" info = ScriptInfo()
script_info = ScriptInfo() app = locate_app(info, 'notanapp.py', None, raise_if_not_found=False)
assert locate_app(script_info, "cliapp.app").name == "testapp" assert app is None
assert locate_app(script_info, "cliapp.app:testapp").name == "testapp"
assert locate_app(script_info, "cliapp.factory").name == "create_app" # only direct import error is suppressed
assert locate_app( with pytest.raises(NoAppException):
script_info, "cliapp.factory").name == "create_app" locate_app(
assert locate_app( info, 'cliapp.importerrorapp', None, raise_if_not_found=False
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.factory:create_app2('foo')")
pytest.raises(
NoAppException, locate_app,
script_info, "cliapp.factory:create_app ()")
pytest.raises(
NoAppException, locate_app, script_info, "cliapp.importerrorapp")
assert locate_app(
script_info, "notanpp.py", raise_if_not_found=False
) is None
def test_find_default_import_path(test_apps, monkeypatch, tmpdir):
"""Test of find_default_import_path."""
monkeypatch.delitem(os.environ, 'FLASK_APP', raising=False)
assert find_default_import_path() == None
monkeypatch.setitem(os.environ, 'FLASK_APP', 'notanapp')
assert find_default_import_path() == 'notanapp'
tmpfile = tmpdir.join('testapp.py')
tmpfile.write('')
monkeypatch.setitem(os.environ, 'FLASK_APP', str(tmpfile))
expect_rv = prepare_exec_for_file(str(tmpfile))
assert find_default_import_path() == expect_rv
def test_get_version(test_apps, capsys): def test_get_version(test_apps, capsys):

Loading…
Cancel
Save