Flask 源码阅读-开胃菜

简介: flask项目大名鼎鼎,应该不需要多做介绍了吧。我把它称之为python服务开发的TOP2项目,另外一个就是django了,不需要比较孰优孰劣,我的观点是各有千秋,各自应用于不同的场景,都需要深入理解,熟练掌握。本次源码选择的版本是 1.1.2,我会采用慢读法,尽自己最大努力把它讲透。本篇是开胃菜,主要分析flask的命令行工具的实现。

flask项目大名鼎鼎,应该不需要多做介绍了吧。我把它称之为python服务开发的TOP2项目,另外一个就是django了,不需要比较孰优孰劣,我的观点是各有千秋,各自应用于不同的场景,都需要深入理解,熟练掌握。本次源码选择的版本是 1.1.2,我会采用慢读法,尽自己最大努力把它讲透。本篇是开胃菜,主要分析flask的命令行工具的实现。


在正式开篇之前,和大家唠叨一点题外话,不喜欢废话的朋友可以直接跳过本段。网上,码农圈经常有各种对比讨论,最经典的梗是: “php是最好的语言!”。我没有嘲笑php的意思,我也用过一段时间的php,做个几个项目。对于这类问题, “小孩才做选择题,成年人都是我都要”  这是开玩笑了,我们重点应该是放在精通上,不应该关闭自己的视野。学习好flask,对精通django有帮助;精通了django,对flask也能够举一反三。如果把自己限定到某一个框架上,有些可惜,毕竟没有一个职业叫做Django/flask-web开发。精力允许的情况下,还是都掌握更好。


flask 示例



示例 hello.py 如下:


from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
    return 'Hello, World!'
复制代码


flask模块可以自启动,启动前需要设置FLASK_APP和FLASK_ENV两个环境变量,然后使用 run 命令:


$ export FLASK_APP=hello.py
$ export FLASK_ENV=development
$ python -m flask run
复制代码


控制台会显示启动成功信息:


* Serving Flask app "hello.py" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 986-558-971
复制代码


眼尖的朋友会发现,这和werkzeug的run_simple一样。没错,flask的cli就是使用werkzegu的run_simple实现的。


flask 命令行工具



模块的启动入口是 __main__.py ,关于 __main__.py 的介绍可以查看附录的参考链接。在__main__.py主要是主要调用了 cli.py 的main函数:


# cli.py
cli = FlaskGroup(
    help="""\
A general utility script for Flask applications.
Provides commands from Flask, extensions, and the application. Loads the
application defined in the FLASK_APP environment variable, or from a wsgi.py
file. Setting the FLASK_ENV environment variable to 'development' will enable
debug mode.
\b
  {prefix}{cmd} FLASK_APP=hello.py
  {prefix}{cmd} FLASK_ENV=development
  {prefix}flask run
""".format(
        cmd="export" if os.name == "posix" else "set",
        prefix="$ " if os.name == "posix" else "> ",
    )
)
def main(as_module=False):
    # TODO omit sys.argv once https://github.com/pallets/click/issues/536 is fixed
    cli.main(args=sys.argv[1:], prog_name="python -m flask" if as_module else None)
复制代码


cli工具的继承关系是: FlaskGroup->AppGroup->click.Group, 使用 click 模块构建的命令行工具。


Click 是一个 Python 包,用于以可组合的方式使用尽可能少的代码创建漂亮的命令行界面。它是“命令行界面创建工具包”,开箱即用同时高度可配置的。


基类AppGroup主要扩展了command方法,使用app_context来装饰所有命令:


# AppGroup
def with_appcontext(f):
    @click.pass_context
    def decorator(__ctx, *args, **kwargs):
        with __ctx.ensure_object(ScriptInfo).load_app().app_context():
            return __ctx.invoke(f, *args, **kwargs)
    return update_wrapper(decorator, f)
class AppGroup(click.Group):
    def command(self, *args, **kwargs):
        wrap_for_ctx = kwargs.pop("with_appcontext", True)
        def decorator(f):
            if wrap_for_ctx:
                f = with_appcontext(f)
            return click.Group.command(self, *args, **kwargs)(f)
        return decorator
复制代码


app_context是flask非常重要的特性,我们以后再详细分析,现在先记住所有的cmd都在app_context的上下文环境中。


FlaskGroup主要收集扩展命令,代码可以看到总共收集了3个命令:


class FlaskGroup(AppGroup):
    def __init__(
        self,
        add_default_commands=True,
        create_app=None,
        add_version_option=True,
        load_dotenv=True,
        set_debug_flag=True,
        **extra
    ):
        ...
        AppGroup.__init__(self, params=params, **extra)
        ...
        if add_default_commands:
            self.add_command(run_command)
            self.add_command(shell_command)
            self.add_command(routes_command)
        self._loaded_plugin_commands = False
复制代码


  • run_command 启动flask应用程序
  • shell_command 使用shell环境方式调试flask应用程序
  • routes_command 查看flask应用程序的路由注册信息


在FlaskGroup中,如果安装了 dotenv 可以使用 .env 文件来控制开发环境/正式环境, 比如数据库配置之类,会非常方便。


Python-dotenv 从.env文件中读取键值对,并可以将它们设置为环境变量。它有助于遵循12要素原则开发应用程序 。


FlaskGroup的main函数中会加载flask-app,主要使用ScriptInfo实现:


class ScriptInfo(object):
    def __init__(self, app_import_path=None, create_app=None, set_debug_flag=True):
        #: Optionally the import path for the Flask application.
        self.app_import_path = app_import_path or os.environ.get("FLASK_APP")
        ...
    def load_app(self):
        ...
        if self.create_app is not None:
            app = call_factory(self, self.create_app)
        else:
            if self.app_import_path:
                path, name = (
                    re.split(r":(?![\\/])", self.app_import_path, 1) + [None]
                )[:2]
                import_name = prepare_import(path)
                app = locate_app(self, import_name, name)
            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)
                    if app:
                        break
        ...
        self._loaded_app = app
        return app
复制代码


在ScriptInfo中会读取FLASK_APP的环境变量,然后会导入类文件,定位app, 然后加载app:


def locate_app(script_info, module_name, app_name, raise_if_not_found=True):
    __traceback_hide__ = True  # noqa: F841
    try:
        __import__(module_name)
    except ImportError:
        ...
    module = sys.modules[module_name]
    if app_name is None:
        return find_best_app(script_info, module)
    else:
        return find_app_by_string(script_info, module, app_name)
复制代码


  • 使用 __import__ 动态加载模块
  • 使用 sys.modules[module_name] 获取加载后的模块


app加载有find_best_app和find_app_by_string两种方法, 默认使用的是find_best_app方法。


find_best_app


由于app的script模块已经导入,优先在这个模块中查找名字为 app 或者 application 的变量,这是一种约定:


def find_best_app(script_info, module):
    from . import Flask
    # Search for the most common names first.
    for attr_name in ("app", "application"):
        app = getattr(module, attr_name, None)
        if isinstance(app, Flask):
            return app
    ...
    # Otherwise find the only object that is a Flask instance.
    matches = [v for v in itervalues(module.__dict__) if isinstance(v, Flask)]
    if len(matches) == 1:
        return matches[0]
    ...
    for attr_name in ("create_app", "make_app"):
    app_factory = getattr(module, attr_name, None)
    if inspect.isfunction(app_factory):
        try:
            app = call_factory(script_info, app_factory)
            if isinstance(app, Flask):
                return app
复制代码


如果没有app或者application的变量定义,则转而寻找其它是flask对象的变量。


如果上述两种方法都找不到app变量,最后查找名字为create_app和make_app的工厂函数, 使用工厂函数创建flask-app。


比如flask示例的flaskr项目中,就提供的是create_app的工厂函数:


# flaskr/__init__.py
def create_app(test_config=None):
    app = Flask(__name__, instance_relative_config=True)
    ....
    return app
复制代码


routes-command


routes命令最简单,使用方法如下:


$ flask routes --all-methods
Endpoint  Methods             Rule
--------  ------------------  -----------------------
hello     GET, HEAD, OPTIONS  /
static    GET, HEAD, OPTIONS  /static/<path:filename>
复制代码


routes_command函数实现routers指令:


@with_appcontext
def routes_command(sort, all_methods):
    """Show all registered routes with endpoints and methods."""
    rules = list(current_app.url_map.iter_rules())
    ignored_methods = set(() if all_methods else ("HEAD", "OPTIONS"))
    if sort in ("endpoint", "rule"):
        rules = sorted(rules, key=attrgetter(sort))
    elif sort == "methods":
        rules = sorted(rules, key=lambda rule: sorted(rule.methods))
    rule_methods = [", ".join(sorted(rule.methods - ignored_methods)) for rule in rules]
    headers = ("Endpoint", "Methods", "Rule")
    widths = (
        max(len(rule.endpoint) for rule in rules),
        max(len(methods) for methods in rule_methods),
        max(len(rule.rule) for rule in rules),
    )
    widths = [max(len(h), w) for h, w in zip(headers, widths)]
    row = "{{0:<{0}}}  {{1:<{1}}}  {{2:<{2}}}".format(*widths)
    click.echo(row.format(*headers).strip())
    click.echo(row.format(*("-" * width for width in widths)))
    for rule, methods in zip(rules, rule_methods):
        click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip())
复制代码


  • 确保执行with_appcontext,在前文有介绍with_appcontext。可见appcontext的重要性。
  • 代码整体就做了一件事,收集app的url_map


shell-command


shell指令提供一个命令行方式运行app:


$ flask shell                                                                        
Python 3.8.5 (v3.8.5:580fbb018f, Jul 20 2020, 12:11:27) 
[Clang 6.0 (clang-600.0.57)] on darwin
App: ch21-flask.hello [development]
Instance: /Users/yoo/work/yuanmahui/python/instance
>>> app
<Flask 'ch21-flask.hello'>
>>> dir(app)
['__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', ... ,'update_template_context', 'url_build_error_handlers', 'url_default_functions', 'url_defaults', 'url_map', 'url_map_class', 'url_rule_class', 'url_value_preprocessor', 'url_value_preprocessors', 'use_x_sendfile', 'view_functions', 'wsgi_app']
>>> app.url_map
Map([<Rule '/' (OPTIONS, HEAD, GET) -> hello>,
 <Rule '/static/<filename>' (OPTIONS, HEAD, GET) -> static>])
>>> locals()
{'app': <Flask 'ch21-flask.hello'>, 'g': <flask.g of 'ch21-flask.hello'>,...}
复制代码


可以看到,在交互式终端中可以查看 appurl_map 。 locals变量中带有app和g两个属性,协助进行调试程序。


shell_command的实现:


@with_appcontext
def shell_command():
    """Run an interactive Python shell in the context of a given
    Flask application.  The application will populate the default
    namespace of this shell according to it's configuration.
    This is useful for executing small snippets of management code
    without having to manually configure the application.
    """
    import code
    from .globals import _app_ctx_stack
    app = _app_ctx_stack.top.app
    banner = "Python %s on %s\nApp: %s [%s]\nInstance: %s" % (
        sys.version,
        sys.platform,
        app.import_name,
        app.env,
        app.instance_path,
    )
    ctx = {}
    # Support the regular Python interpreter startup script if someone
    # is using it.
    startup = os.environ.get("PYTHONSTARTUP")
    if startup and os.path.isfile(startup):
        with open(startup, "r") as f:
            eval(compile(f.read(), startup, "exec"), ctx)
    ctx.update(app.make_shell_context())
    code.interact(banner=banner, local=ctx)
复制代码


shell主要利用code模块的 interact 实现,在之前介绍德国锤子werkzeug时也有过介绍。


在make_shell_context中可以看到app和g两个属性设置:


def make_shell_context(self):
    rv = {"app": self, "g": g}
    for processor in self.shell_context_processors:
        rv.update(processor())
    return rv
复制代码


利用参考shell命令可以实现程序热更功能,以后有机会再详细介绍。


run-command


最复杂和重要的就是run命令了, run_command的注释很清晰的介绍了功能:


@pass_script_info
def run_command(
    info, host, port, reload, debugger, eager_loading, with_threads, cert, extra_files
):
    """Run a local development server.
    This server is for development purposes only. It does not provide
    the stability, security, or performance of production WSGI servers.
    The reloader and debugger are enabled by default if
    FLASK_ENV=development or FLASK_DEBUG=1.
    """
    ...
    show_server_banner(get_env(), debug, info.app_import_path, eager_loading)
    app = DispatchingApp(info.load_app, use_eager_loading=eager_loading)
    from werkzeug.serving import run_simple
    run_simple(
        host,
        port,
        app,
        use_reloader=reload,
        use_debugger=debugger,
        threaded=with_threads,
        ssl_context=cert,
        extra_files=extra_files,
    )
复制代码


  • show_server_banner展示启动服务的banner信息
  • 使用DispatchingApp包装业务app
  • 使用werkzeug的run_simple启动app,证实了前面我们通过banner的猜测


DispatchingApp实现也不复杂:


class DispatchingApp(object):
    def __init__(self, loader, use_eager_loading=False):
        self.loader = loader
        self._app = None
        self._lock = Lock()
        self._bg_loading_exc_info = None
        if use_eager_loading:
            self._load_unlocked()
        else:
            self._load_in_background()
    def _load_in_background(self):
        def _load_app():
            __traceback_hide__ = True  # noqa: F841
            with self._lock:
                try:
                    self._load_unlocked()
                except Exception:
                    self._bg_loading_exc_info = sys.exc_info()
        t = Thread(target=_load_app, args=())
        t.start()
    def _load_unlocked(self):
        __traceback_hide__ = True  # noqa: F841
        self._app = rv = self.loader()
        self._bg_loading_exc_info = None
        return rv
    ...
    def __call__(self, environ, start_response):
        __traceback_hide__ = True  # noqa: F841
        if self._app is not None:
            return self._app(environ, start_response)
        self._flush_bg_loading_exception()
        with self._lock:
            if self._app is not None:
                rv = self._app
            else:
                rv = self._load_unlocked()
            return rv(environ, start_response)
复制代码


  • 使用线程方式加载app或者直接加载app,这个功能会和reload配合使用,一种是饥渴加载一种是懒加载。
  • call函数是flask-app的wsgi接口,接收environ和start_response两个核心参数,是请求响应的入口。


小结



本文我们主要理解了flask-cli的实现逻辑,当然要完全弄懂flask-cli,还需要深入理解click和dotenv的实现,不过掌握到这个程度,我个人认为已经够用了。我们简单小结一下flask-cli的功能:


  1. 提供shell,routes和run三个命令帮助开发flask应用程序
  2. 用户应用程序使用动态加载的方式导入
  3. flask推荐创建flask-app或者提供create_app方式提供应用程序入口


参考链接




目录
相关文章
扒源码 - 一个请求在flask中经历了什么
客户端发起一个请求,Flask 都干了什么呢?url 如何与视图进行绑定的?
|
存储 小程序 关系型数据库
python微信小程序看图猜成语源码flask
启动Idiom/flask下的venv虚拟环境,运行python manage.py runserver命令启动Flask。然后打开微信开发者工具并扫码登录,选择flask/weapp-idiom小程序,加载完成后进入小程序登录页面,
257 0
|
编解码 Python
Python 技术篇 - 修改源码解决中文主机名导致的flask、socket服务起不来问题: ‘utf-8‘ codec can‘t decode byte 0xc0 in position...
Python 技术篇 - 修改源码解决中文主机名导致的flask、socket服务起不来问题: ‘utf-8‘ codec can‘t decode byte 0xc0 in position...
402 0
Python 技术篇 - 修改源码解决中文主机名导致的flask、socket服务起不来问题: ‘utf-8‘ codec can‘t decode byte 0xc0 in position...
|
Python
Flask 源码阅读-下篇 |Python 主题月
flask项目大名鼎鼎,不需要多做介绍。我把它称之为python服务开发的TOP2项目,另外一个就是django。这两个项目各有千秋,各自有不同的应用场景,都需要深入理解,熟练掌握。本次源码选择的版本是 1.1.2,我会采用慢读法,尽自己最大努力把它讲透。
195 0
|
Python
Flask 源码阅读-正菜 |Python 主题月
flask项目大名鼎鼎,不需要多做介绍。我把它称之为python服务开发的TOP2项目,另外一个就是django。这两个项目各有千秋,各自有不同的应用场景,都需要深入理解,熟练掌握。本次源码选择的版本是 1.1.2,我会采用慢读法,尽自己最大努力把它讲透。
165 0
|
SQL Python
flask-sqlalchemy的paginate源码分析
今天遇到一个问题,用flask-sqlalchemy的paginate做分页的时候,发现联表查询时分页出来结果数据少了很多,直接all()出来就没问题,把sql单独执行发现是联表查时有重复数据,group一下就好了,但是all()结果没有重复的,all()还给滤重了?而且paginate还是在滤重前做的limit,所以去重后结果就少了,而且还影响了total的值,趁这个机会看一下flask-sqlalchemy的源码吧
|
Python
Python Web Flask源码解读(一)——启动流程
Python Web Flask源码解读(一)——启动流程0x00 什么是WSGIWeb Server Gateway Interface它由Python标准定义的一套Web Server与Web Application的接口交互规范。
1385 0
|
Python Go 前端开发
源码解析flask的路由系统
当我们新建一个flask项目时,pycharm通常已经为项目定义了一个基本路由 @app.route('/') def hello_world(): return 'Hello World!' 此时在浏览器中输入地址http://127.
950 0
|
JSON 数据格式 Python
源码解析Flask的配置文件
在flask里,我们常在主文件中定义某些配置,比如: app.debug = True app.secret_key = 'helloworld!!' 实际上,flask中默认可以进行可选的配置项有很多。
983 0