From 2433522d2967b8a5e46f16de587a8fac5088a47c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 6 Jan 2018 17:07:56 +0100 Subject: [PATCH] Add Support for FLASK_ENV (#2570) This introduces environments to Flask --- docs/cli.rst | 15 +++++++- docs/config.rst | 43 +++++++++++++++------- docs/patterns/packages.rst | 6 +-- docs/quickstart.rst | 9 +++-- docs/server.rst | 16 ++++++-- docs/tutorial/packaging.rst | 6 ++- flask/app.py | 73 +++++++++++++++++++++---------------- flask/cli.py | 29 +++++++++------ flask/helpers.py | 14 ++++++- tests/test_helpers.py | 17 +++++++-- 10 files changed, 154 insertions(+), 74 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 9e558ece..55d09963 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -116,6 +116,16 @@ context will be active, and the app instance will be imported. :: Use :meth:`~Flask.shell_context_processor` to add other automatic imports. +Environments +------------ + +.. versionadded:: 1.0 + +The environment in which the Flask app should run is set by the +:envvar:`FLASK_ENV` environment variable. If not set it defaults to +``production``. The other default environment which is known is +``development``. If the env is set to ``development`` the debug mode is +for instance automatically enabled. Debug Mode ---------- @@ -123,11 +133,14 @@ Debug Mode Set the :envvar:`FLASK_DEBUG` environment variable to override the application's :attr:`~Flask.debug` flag. The value ``1`` enables it, ``0`` disables it. Forcing the debug flag on also enables the debugger and reloader -when running the development server. :: +when running the development server. + +:: $ FLASK_DEBUG=1 flask run * Serving Flask app "hello" * Forcing debug mode on + * Env production * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with inotify reloader * Debugger is active! diff --git a/docs/config.rst b/docs/config.rst index 6ed27a83..e8da82d1 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -27,35 +27,52 @@ The :attr:`~flask.Flask.config` is actually a subclass of a dictionary and can be modified just like any dictionary:: app = Flask(__name__) - app.config['DEBUG'] = True + app.config['TESTING'] = True Certain configuration values are also forwarded to the :attr:`~flask.Flask` object so you can read and write them from there:: - app.debug = True + app.testing = True To update multiple keys at once you can use the :meth:`dict.update` method:: app.config.update( - DEBUG=True, + TESTING=True, SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/' ) -.. admonition:: Debug Mode with the ``flask`` Script +Environment and Debug Features +------------------------------ - If you use the :command:`flask` script to start a local development - server, to enable the debug mode, you need to export the ``FLASK_DEBUG`` - environment variable before running the server:: +Some values are special in that they can show unexpected behavior when +changed late. In particular that applies to the Flask environment and +debug mode. - $ export FLASK_DEBUG=1 - $ flask run +If you use the :command:`flask` script to start a local development server +for instance you should tell Flask that you want to work in the +development environment. For safety reasons we default the flask +environment to production mode instead of development. This is done +because development mode can turn on potentially unsafe features such as +the debugger by default. - (On Windows you need to use ``set`` instead of ``export``). +To control the environment and such fundamental features Flask provides +the two environment variables :envvar:`FLASK_ENV` and :envvar:`FLASK_DEBUG`. +In versions of Flask older than 1.0 the :envvar:`FLASK_ENV` environment +variable did not exist. - ``app.debug`` and ``app.config['DEBUG']`` are not compatible with -   the :command:`flask` script. They only worked when using ``Flask.run()`` - method. +The most common way to switch Flask to development mode is to tell it to +work on the ``development`` environment:: + +$ export FLASK_ENV=development +$ flask run + +(On Windows you need to use ``set`` instead of ``export``). + +While you can attempt to flip the environment and debug flag separately in +the Flask config from the config file this is strongly discouraged as +those flags are often loaded early and changing them late might not apply +to all systems and extensions. Builtin Configuration Values ---------------------------- diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index cc149839..6b0ee7ad 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -65,10 +65,10 @@ that tells Flask where to find the application instance:: export FLASK_APP=yourapplication If you are outside of the project directory make sure to provide the exact -path to your application directory. Similarly you can turn on "debug -mode" with this environment variable:: +path to your application directory. Similarly you can turn on the +development features like this:: - export FLASK_DEBUG=true + export FLASK_ENV=development In order to install and run the application you need to issue the following commands:: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index d3f3dea0..284f6d76 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -130,13 +130,16 @@ That is not very nice and Flask can do better. If you enable debug support the server will reload itself on code changes, and it will also provide you with a helpful debugger if things go wrong. -To enable debug mode you can export the ``FLASK_DEBUG`` environment variable +To enable all development features (and to disable the debug mode) you can +export the ``FLASK_ENV`` environment variable and set it to +``development`` before running the server:: - $ export FLASK_DEBUG=1 + $ export FLASK_ENV=development $ flask run -(On Windows you need to use ``set`` instead of ``export``). +(On Windows you need to use ``set`` instead of ``export`` and on Flask +versions older than 1.0 you need to export ``FLASK_DEBUG=1`` instead). This does the following things: diff --git a/docs/server.rst b/docs/server.rst index f8332ebf..7e03d8df 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -12,12 +12,13 @@ but you can also continue using the :meth:`Flask.run` method. Command Line ------------ -The :command:`flask` command line script (:ref:`cli`) is strongly recommended for -development because it provides a superior reload experience due to how it -loads the application. The basic usage is like this:: +The :command:`flask` command line script (:ref:`cli`) is strongly +recommended for development because it provides a superior reload +experience due to how it loads the application. The basic usage is like +this:: $ export FLASK_APP=my_application - $ export FLASK_DEBUG=1 + $ export FLASK_ENV=development $ flask run This will enable the debugger, the reloader and then start the server on @@ -29,6 +30,13 @@ disabled:: $ flask run --no-reload +.. note:: + + On older Flask version (before 1.0) the :envvar:`FLASK_ENV` + environment variable is not supported and you need to enable the + debug mode separately by setting the :envvar:`FLASK_DEBUG` environment + variable to ``1``. + In Code ------- diff --git a/docs/tutorial/packaging.rst b/docs/tutorial/packaging.rst index 5db921aa..13e11d5c 100644 --- a/docs/tutorial/packaging.rst +++ b/docs/tutorial/packaging.rst @@ -78,11 +78,13 @@ With that out of the way, you should be able to start up the application. Do this on Mac or Linux with the following commands in ``flaskr/``:: export FLASK_APP=flaskr - export FLASK_DEBUG=true + export FLASK_ENV=development flask run (In case you are on Windows you need to use ``set`` instead of ``export``). -The :envvar:`FLASK_DEBUG` flag enables or disables the interactive debugger. +The :envvar:`FLASK_ENV` flag if set to ``development`` turns on all +development features such as enabling the interactive debugger. + *Never leave debug mode activated in a production system*, because it will allow users to execute code on the server! diff --git a/flask/app.py b/flask/app.py index 88ca433c..e84e0a3f 100644 --- a/flask/app.py +++ b/flask/app.py @@ -27,7 +27,7 @@ from .config import Config, ConfigAttribute from .ctx import AppContext, RequestContext, _AppCtxGlobals from .globals import _request_ctx_stack, g, request, session from .helpers import _PackageBoundObject, \ - _endpoint_from_view_func, find_package, get_debug_flag, \ + _endpoint_from_view_func, find_package, get_env, get_debug_flag, \ get_flashed_messages, locked_cached_property, url_for from .logging import create_logger from .sessions import SecureCookieSessionInterface @@ -196,15 +196,6 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.11 config_class = Config - #: The debug flag. Set this to ``True`` to enable debugging of the - #: application. In debug mode the debugger will kick in when an unhandled - #: exception occurs and the integrated server will automatically reload - #: the application if changes in the code are detected. - #: - #: This attribute can also be configured from the config with the ``DEBUG`` - #: configuration key. Defaults to ``False``. - debug = ConfigAttribute('DEBUG') - #: The testing flag. Set this to ``True`` to enable the test mode of #: Flask extensions (and in the future probably also Flask itself). #: For example this might activate test helpers that have an @@ -278,7 +269,8 @@ class Flask(_PackageBoundObject): #: Default configuration parameters. default_config = ImmutableDict({ - 'DEBUG': get_debug_flag(default=False), + 'ENV': None, + 'DEBUG': None, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, @@ -647,7 +639,10 @@ class Flask(_PackageBoundObject): root_path = self.root_path if instance_relative: root_path = self.instance_path - return self.config_class(root_path, self.default_config) + defaults = dict(self.default_config) + defaults['ENV'] = get_env() + defaults['DEBUG'] = get_debug_flag() + return self.config_class(root_path, defaults) def auto_find_instance_path(self): """Tries to locate the instance path if it was not provided to the @@ -790,25 +785,38 @@ class Flask(_PackageBoundObject): rv.update(processor()) return rv - def _reconfigure_for_run_debug(self, debug): - """The ``run`` commands will set the application's debug flag. Some - application configuration may already be calculated based on the - previous debug value. This method will recalculate affected values. - - Called by the :func:`flask.cli.run` command or :meth:`Flask.run` - method if the debug flag is set explicitly in the call. - - :param debug: the new value of the debug flag - - .. versionadded:: 1.0 - Reconfigures ``app.jinja_env.auto_reload``. - """ - self.debug = debug + #: The environment value. This is typically set from outside the + #: process by setting the `FLASK_ENV` environment variable and can be + #: used to quickly switch between different environments like + #: `production` and `development`. If not set this defaults to + #: `production`. + env = ConfigAttribute('ENV') + + def _get_debug(self): + return self.config['DEBUG'] + def _set_debug(self, value): + self._set_debug_value(value) + + #: The debug flag. If this is ``True`` it enables debugging of the + #: application. In debug mode the debugger will kick in when an + #: unhandled exception occurs and the integrated server will + #: automatically reload the application if changes in the code are + #: detected. + #: + #: This value should only be configured by the :envvar:`FLASK_DEBUG` + #: environment variable. Changing it by other means will not yield + #: consistent results. The default value depends on the Flask + #: environment and will be true for the development environment and false + #: otherwise. + debug = property(_get_debug, _set_debug) + del _get_debug, _set_debug + + def _set_debug_value(self, value): + self.config['DEBUG'] = value self.jinja_env.auto_reload = self.templates_auto_reload - def run( - self, host=None, port=None, debug=None, load_dotenv=True, **options - ): + def run(self, host=None, port=None, debug=None, + load_dotenv=True, **options): """Runs the application on a local development server. Do not use ``run()`` in a production setting. It is not intended to @@ -872,11 +880,11 @@ class Flask(_PackageBoundObject): load_dotenv() if debug is not None: - self._reconfigure_for_run_debug(bool(debug)) + self.debug = bool(debug) _host = '127.0.0.1' _port = 5000 - server_name = self.config.get("SERVER_NAME") + server_name = self.config.get('SERVER_NAME') sn_host, sn_port = None, None if server_name: @@ -1055,7 +1063,8 @@ class Flask(_PackageBoundObject): return iter(self._blueprint_order) @setupmethod - def add_url_rule(self, rule, endpoint=None, view_func=None, provide_automatic_options=None, **options): + def add_url_rule(self, rule, endpoint=None, view_func=None, + provide_automatic_options=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the endpoint. diff --git a/flask/cli.py b/flask/cli.py index 1be7957c..2adadcc7 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -25,7 +25,7 @@ import click from . import __version__ from ._compat import getargspec, iteritems, reraise from .globals import current_app -from .helpers import get_debug_flag +from .helpers import get_debug_flag, get_env try: import dotenv @@ -341,9 +341,8 @@ class ScriptInfo(object): else: for path in ('wsgi.py', 'app.py'): import_name = prepare_import(path) - app = locate_app( - self, import_name, None, raise_if_not_found=False - ) + app = locate_app(self, import_name, None, + raise_if_not_found=False) if app: break @@ -357,8 +356,10 @@ class ScriptInfo(object): debug = get_debug_flag() + # Update the app's debug flag through the descriptor so that other + # values repopulate as well. if debug is not None: - app._reconfigure_for_run_debug(debug) + app.debug = debug self._loaded_app = app return app @@ -432,10 +433,8 @@ class FlaskGroup(AppGroup): from :file:`.env` and :file:`.flaskenv` files. """ - def __init__( - self, add_default_commands=True, create_app=None, - add_version_option=True, load_dotenv=True, **extra - ): + def __init__(self, add_default_commands=True, create_app=None, + add_version_option=True, load_dotenv=True, **extra): params = list(extra.pop('params', None) or ()) if add_version_option: @@ -610,6 +609,13 @@ def run_command(info, host, port, reload, debugger, eager_loading, """ from werkzeug.serving import run_simple + if get_env() == 'production': + click.secho('Warning: Detected a production environment. Do not ' + 'use `flask run` for production use.', + fg='red') + click.secho('Use a production ready WSGI server instead', + dim=True) + debug = get_debug_flag() if reload is None: reload = bool(debug) @@ -629,6 +635,7 @@ def run_command(info, host, port, reload, debugger, eager_loading, # we won't print anything. if info.app_import_path is not None: print(' * Serving Flask app "%s"' % info.app_import_path) + print(' * Env %s' % get_env()) if debug is not None: print(' * Forcing debug mode %s' % (debug and 'on' or 'off')) @@ -649,11 +656,11 @@ def shell_command(): import code from flask.globals import _app_ctx_stack app = _app_ctx_stack.top.app - banner = 'Python %s on %s\nApp: %s%s\nInstance: %s' % ( + banner = 'Python %s on %s\nApp: %s [%s]\nInstance: %s' % ( sys.version, sys.platform, app.import_name, - app.debug and ' [debug]' or '', + app.env, app.instance_path, ) ctx = {} diff --git a/flask/helpers.py b/flask/helpers.py index 412d9caf..705ea3e1 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -46,10 +46,20 @@ _os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] if sep not in (None, '/')) -def get_debug_flag(default=None): +def get_env(): + val = os.environ.get('FLASK_ENV') + if not val: + val = 'production' + return val + + +def get_debug_flag(): val = os.environ.get('FLASK_DEBUG') if not val: - return default + env = get_env() + if env == 'development': + return True + return False return val.lower() not in ('0', 'false', 'no') diff --git a/tests/test_helpers.py b/tests/test_helpers.py index d497c40d..1ddde116 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -21,7 +21,7 @@ from werkzeug.http import http_date, parse_cache_control_header, \ import flask from flask._compat import StringIO, text_type -from flask.helpers import get_debug_flag +from flask.helpers import get_debug_flag, get_env def has_encoding(name): @@ -886,7 +886,7 @@ class TestSafeJoin(object): class TestHelpers(object): @pytest.mark.parametrize('debug, expected_flag, expected_default_flag', [ - ('', None, True), + ('', False, False), ('0', False, False), ('False', False, False), ('No', False, False), @@ -898,7 +898,18 @@ class TestHelpers(object): assert get_debug_flag() is None else: assert get_debug_flag() == expected_flag - assert get_debug_flag(default=True) == expected_default_flag + assert get_debug_flag() == expected_default_flag + + @pytest.mark.parametrize('env, ref_env, debug', [ + ('', 'production', False), + ('production', 'production', False), + ('development', 'development', True), + ('other', 'other', False), + ]) + def test_get_env(self, monkeypatch, env, ref_env, debug): + monkeypatch.setenv('FLASK_ENV', env) + assert get_debug_flag() == debug + assert get_env() == ref_env def test_make_response(self): app = flask.Flask(__name__)