import code import os import sys from importlib import import_module from pathlib import Path from typing import Any, Callable, Iterable, List, Optional, TYPE_CHECKING import click try: from dotenv import load_dotenv except ImportError: pass from .__about__ import __version__ from .helpers import get_debug_flag if TYPE_CHECKING: from .app import Quart # noqa: F401 class NoAppException(click.UsageError): def __init__(self) -> None: super().__init__( 'Could not locate a Quart application as the QUART_APP environment ' 'variable has either not been set or provided or does not point to ' 'a valid application.\n' 'Please set it to module_name:app_name or module_name:factory_function()\n' 'Note `quart` is not a valid module_name.' ) class ScriptInfo: def __init__( self, app_import_path: Optional[str]=None, create_app: Optional[Callable]=None, ) -> None: self.load_dotenv_if_exists() self.app_import_path = app_import_path or os.environ.get('QUART_APP') self.create_app = create_app self.data: dict = {} self._app: Optional['Quart'] = None def load_app(self) -> 'Quart': if self._app is None: if self.create_app is not None: self._app = self.create_app() else: try: module_name, app_name = self.app_import_path.split(':', 1) except ValueError: module_name, app_name = self.app_import_path, 'app' except AttributeError: raise NoAppException() module_path = Path(module_name).resolve() sys.path.insert(0, str(module_path.parent)) if module_path.is_file(): import_name = module_path.with_suffix('').name else: import_name = module_path.name try: module = import_module(import_name) except ModuleNotFoundError as error: if error.name == import_name: raise NoAppException() else: raise try: self._app = eval(app_name, vars(module)) except NameError: raise NoAppException() from .app import Quart if not isinstance(self._app, Quart): self._app = None raise NoAppException() if self._app is None: raise NoAppException() self._app.debug = get_debug_flag() return self._app def load_dotenv_if_exists(self) -> None: if os.environ.get('QUART_SKIP_DOTENV') == '1': return if not Path(".env").is_file() and not Path(".quartenv").is_file(): return try: if Path(".env").is_file(): load_dotenv() if Path(".quartenv").is_file(): load_dotenv(dotenv_path=Path(".") / ".quartenv") except NameError: print( # noqa: T001 "* Tip: There are .env files present. " "Do \"pip install python-dotenv\" to use them." ) pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) class AppGroup(click.Group): def group(self, *args: Any, **kwargs: Any) -> Any: kwargs.setdefault('cls', AppGroup) return super().group(self, *args, **kwargs) def get_version(ctx: Any, param: Any, value: Any) -> None: if not value or ctx.resilient_parsing: return message = f"Quart {__version__}" click.echo(message, color=ctx.color) ctx.exit() version_option = click.Option( ['--version'], help='Show the Quart version', expose_value=False, callback=get_version, is_flag=True, is_eager=True, ) class QuartGroup(AppGroup): def __init__( self, add_default_commands: bool=True, create_app: Optional[Callable]=None, add_version_option: bool=True, *, params: Optional[List]=None, **kwargs: Any, ) -> None: params = params or [] if add_version_option: params.append(version_option) super().__init__(params=params, **kwargs) self.create_app = create_app if add_default_commands: self.add_command(run_command) self.add_command(shell_command) def get_command(self, ctx: click.Context, name: str) -> click.Command: """Return the relevant command given the context and name. .. warning:: This differs substaintially from Flask in that it allows for the inbuilt commands to be overridden. """ info = ctx.ensure_object(ScriptInfo) command = None try: command = info.load_app().cli.get_command(ctx, name) except NoAppException: pass if command is None: command = super().get_command(ctx, name) return command def list_commands(self, ctx: click.Context) -> Iterable[str]: commands = set(click.Group.list_commands(self, ctx)) info = ctx.ensure_object(ScriptInfo) commands.update(info.load_app().cli.list_commands(ctx)) return commands def main(self, *args: Any, **kwargs: Any) -> Any: kwargs.setdefault('obj', ScriptInfo(create_app=self.create_app)) return super().main(*args, **kwargs) @click.command('run', short_help='Start and run a development server.') @click.option('--host', '-h', default='127.0.0.1', help='The interface to serve on.') @click.option('--port', '-p', default=5000, help='The port to serve on.') @pass_script_info def run_command(info: ScriptInfo, host: str, port: int) -> None: app = info.load_app() app.run(debug=True, host=host, port=port, use_reloader=True) @click.command('shell', short_help='Open a shell within the app context.') @pass_script_info def shell_command(info: ScriptInfo) -> None: app = info.load_app() context = {} context.update(app.make_shell_context()) banner = f"Python {sys.version} on {sys.platform} running {app.import_name}" code.interact(banner=banner, local=context) cli = QuartGroup( help="""\ Utility functions for Quart applications. This will load the app defined in the QUART_APP environment variable. The QUART_APP variable follows the Gunicorn standard of `module_name:application_name` e.g. `hello:app`. \b {prefix}{cmd} QUART_APP=hello:app {prefix}{cmd} QUART_DEBUG=1 {prefix}quart run """.format( cmd='export' if os.name == 'posix' else 'set', prefix='$ ' if os.name == 'posix' else '> ', ), ) def main(as_module: bool=False) -> None: args = sys.argv[1:] if as_module: name = 'python -m quart' sys.argv = ['-m', 'quart'] + args else: name = None cli.main(args=args, prog_name=name) if __name__ == '__main__': main(as_module=True)