|
8 | 8 | :copyright: (c) 2015 by Armin Ronacher. |
9 | 9 | :license: BSD, see LICENSE for more details. |
10 | 10 | """ |
| 11 | +from __future__ import print_function |
11 | 12 |
|
12 | 13 | import ast |
13 | 14 | import inspect |
|
22 | 23 | import click |
23 | 24 |
|
24 | 25 | from . import __version__ |
25 | | -from ._compat import iteritems, reraise |
| 26 | +from ._compat import getargspec, iteritems, reraise |
26 | 27 | from .globals import current_app |
27 | 28 | from .helpers import get_debug_flag |
28 | | -from ._compat import getargspec |
| 29 | + |
| 30 | +try: |
| 31 | + import dotenv |
| 32 | +except ImportError: |
| 33 | + dotenv = None |
29 | 34 |
|
30 | 35 |
|
31 | 36 | class NoAppException(click.UsageError): |
@@ -394,21 +399,31 @@ class FlaskGroup(AppGroup): |
394 | 399 | For information as of why this is useful see :ref:`custom-scripts`. |
395 | 400 |
|
396 | 401 | :param add_default_commands: if this is True then the default run and |
397 | | - shell commands wil be added. |
| 402 | + shell commands wil be added. |
398 | 403 | :param add_version_option: adds the ``--version`` option. |
399 | | - :param create_app: an optional callback that is passed the script info |
400 | | - and returns the loaded app. |
| 404 | + :param create_app: an optional callback that is passed the script info and |
| 405 | + returns the loaded app. |
| 406 | + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` |
| 407 | + files to set environment variables. Will also change the working |
| 408 | + directory to the directory containing the first file found. |
| 409 | +
|
| 410 | + .. versionchanged:: 1.0 |
| 411 | + If installed, python-dotenv will be used to load environment variables |
| 412 | + from :file:`.env` and :file:`.flaskenv` files. |
401 | 413 | """ |
402 | 414 |
|
403 | | - def __init__(self, add_default_commands=True, create_app=None, |
404 | | - add_version_option=True, **extra): |
| 415 | + def __init__( |
| 416 | + self, add_default_commands=True, create_app=None, |
| 417 | + add_version_option=True, load_dotenv=True, **extra |
| 418 | + ): |
405 | 419 | params = list(extra.pop('params', None) or ()) |
406 | 420 |
|
407 | 421 | if add_version_option: |
408 | 422 | params.append(version_option) |
409 | 423 |
|
410 | 424 | AppGroup.__init__(self, params=params, **extra) |
411 | 425 | self.create_app = create_app |
| 426 | + self.load_dotenv = load_dotenv |
412 | 427 |
|
413 | 428 | if add_default_commands: |
414 | 429 | self.add_command(run_command) |
@@ -472,12 +487,75 @@ def list_commands(self, ctx): |
472 | 487 | return sorted(rv) |
473 | 488 |
|
474 | 489 | def main(self, *args, **kwargs): |
| 490 | + # Set a global flag that indicates that we were invoked from the |
| 491 | + # command line interface. This is detected by Flask.run to make the |
| 492 | + # call into a no-op. This is necessary to avoid ugly errors when the |
| 493 | + # script that is loaded here also attempts to start a server. |
| 494 | + os.environ['FLASK_RUN_FROM_CLI'] = 'true' |
| 495 | + |
| 496 | + if self.load_dotenv: |
| 497 | + load_dotenv() |
| 498 | + |
475 | 499 | obj = kwargs.get('obj') |
| 500 | + |
476 | 501 | if obj is None: |
477 | 502 | obj = ScriptInfo(create_app=self.create_app) |
| 503 | + |
478 | 504 | kwargs['obj'] = obj |
479 | 505 | kwargs.setdefault('auto_envvar_prefix', 'FLASK') |
480 | | - return AppGroup.main(self, *args, **kwargs) |
| 506 | + return super(FlaskGroup, self).main(*args, **kwargs) |
| 507 | + |
| 508 | + |
| 509 | +def _path_is_ancestor(path, other): |
| 510 | + """Take ``other`` and remove the length of ``path`` from it. Then join it |
| 511 | + to ``path``. If it is the original value, ``path`` is an ancestor of |
| 512 | + ``other``.""" |
| 513 | + return os.path.join(path, other[len(path):].lstrip(os.sep)) == other |
| 514 | + |
| 515 | + |
| 516 | +def load_dotenv(path=None): |
| 517 | + """Load "dotenv" files in order of precedence to set environment variables. |
| 518 | +
|
| 519 | + If an env var is already set it is not overwritten, so earlier files in the |
| 520 | + list are preferred over later files. |
| 521 | +
|
| 522 | + Changes the current working directory to the location of the first file |
| 523 | + found, with the assumption that it is in the top level project directory |
| 524 | + and will be where the Python path should import local packages from. |
| 525 | +
|
| 526 | + This is a no-op if `python-dotenv`_ is not installed. |
| 527 | +
|
| 528 | + .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme |
| 529 | +
|
| 530 | + :param path: Load the file at this location instead of searching. |
| 531 | + :return: ``True`` if a file was loaded. |
| 532 | +
|
| 533 | + .. versionadded:: 1.0 |
| 534 | + """ |
| 535 | + |
| 536 | + if dotenv is None: |
| 537 | + return |
| 538 | + |
| 539 | + if path is not None: |
| 540 | + return dotenv.load_dotenv(path) |
| 541 | + |
| 542 | + new_dir = None |
| 543 | + |
| 544 | + for name in ('.env', '.flaskenv'): |
| 545 | + path = dotenv.find_dotenv(name, usecwd=True) |
| 546 | + |
| 547 | + if not path: |
| 548 | + continue |
| 549 | + |
| 550 | + if new_dir is None: |
| 551 | + new_dir = os.path.dirname(path) |
| 552 | + |
| 553 | + dotenv.load_dotenv(path) |
| 554 | + |
| 555 | + if new_dir and os.getcwd() != new_dir: |
| 556 | + os.chdir(new_dir) |
| 557 | + |
| 558 | + return new_dir is not None # at least one file was located and loaded |
481 | 559 |
|
482 | 560 |
|
483 | 561 | @click.command('run', short_help='Runs a development server.') |
@@ -512,13 +590,6 @@ def run_command(info, host, port, reload, debugger, eager_loading, |
512 | 590 | """ |
513 | 591 | from werkzeug.serving import run_simple |
514 | 592 |
|
515 | | - # Set a global flag that indicates that we were invoked from the |
516 | | - # command line interface provided server command. This is detected |
517 | | - # by Flask.run to make the call into a no-op. This is necessary to |
518 | | - # avoid ugly errors when the script that is loaded here also attempts |
519 | | - # to start a server. |
520 | | - os.environ['FLASK_RUN_FROM_CLI_SERVER'] = '1' |
521 | | - |
522 | 593 | debug = get_debug_flag() |
523 | 594 | if reload is None: |
524 | 595 | reload = bool(debug) |
|
0 commit comments