Gunicorn: v20 问题:无法在“app”中找到应用程序对象“create_app()”

创建于 2019-11-09  ·  47评论  ·  资料来源: benoitc/gunicorn

我忽略了固定我的 gunicorn 版本,今天早上当我重新部署我的应用程序时,运行命令开始中断,它自动升级到 20.0.0.0。

将我的 gunicorn 版本降级回 19.9 解决了该问题。

这是我用来运行我的应用程序的命令:

gunicorn 'app:create_app()' --workers 4 --threads 4 --bind 0.0.0.0:$PORT

错误是:

Failed to find application object 'create_app()' in 'app'
( Feedback Requested FeaturApp Investigation

最有用的评论

固定在主人。 感谢@davidism的补丁!

处理的所有案例都在此测试中: https :

所有47条评论

我也遇到过这个问题,即
Failed to find application object 'create_app()' in 'app'
并固定到版本 19.9.0 解决了这个问题。

我最初虽然修复是从以下位置更改 gunicorn 命令:
gunicorn --bind 0.0.0.0:$PORT app:create_app()
到:
gunicorn --bind 0.0.0.0:$PORT app:create_app
(注意 create_app 后面的括号现在不见了)。 最初,一切似乎都很好:

网站_1 | [2019-11-10 19:18:54 +0000] [1] [INFO]启动gunicorn 20.0.0
网站_1 | [2019年11月10日十九时18分54秒0000] [1] [INFO]听力于: http://0.0.0.0 :8000(1)
网站_1 | [2019-11-10 19:18:54 +0000] [1] [INFO] 使用工人:同步
网站_1 | [2019-11-10 19:18:54 +0000] [11] [INFO] 使用 pid 启动 worker:11

但可惜这只是海市蜃楼,因为当您尝试加载您的烧瓶网站/端点时,它会说:

[2019-11-10 19:20:28 +0000] [11] [ERROR] 错误处理请求 /
网站_1 | 回溯(最近一次调用最后一次):
网站_1 | 文件“/usr/local/lib/python3.7/site-packages/gunicorn/workers/sync.py”,第134行,句柄
网站_1 | self.handle_request(listener, req, client, addr)
网站_1 | 文件“/usr/local/lib/python3.7/site-packages/gunicorn/workers/sync.py”,第175行,在handle_request中
网站_1 | respiter = self.wsgi(环境,resp.start_response)
网站_1 | 类型错误:create_app() 需要 0 个位置参数,但给出了 2 个

这显然是 gunicorn 20.0.0 版的问题。

它必须与此更改有关: https :

在您这边修复它的一种方法是在您的主模块中导出myapp = create_app()并且十个以app:myapp开头。 这应该有效,如果无效,请告诉我。

我会看看是否需要在那里做些什么。 @berkerpeksag为什么需要删除eval

它必须与此更改有关: 3701ad9#diff-0b90f794c3e9742c45bf484505e3db8dR377 via #2043 。

在您这边修复它的一种方法是在您的主模块中导出myapp = create_app()并且十个以app:myapp开头。 这应该有效,如果无效,请告诉我。

我会看看是否需要在那里做些什么。 @berkerpeksag为什么需要删除eval

我已经在我的应用程序中进行了这个更改并修复了崩溃问题,Gunicorn 现在可以通过将create_app()的结果保存在一个变量中并将其导出来运行我的应用程序,以便它可以在我的 Gunicorn 运行命令中使用

# app.py
def create_app():
    ...

my_app = create_app()

gunicorn "app:my_app" --workers 8

我可以确认执行上面建议的@benoitc@jrusso1020解决了问题。 谢谢大家!

如果在启动时需要传递参数,我看不到修复工作,例如:

gunicorn --chdir hcli_core path "hcli_ core:HCLI (" hcli_core sample hfm ").connector"。

参数传递在 19.9.0 中有效,但在 20.0.0 中失败。

@benoitc以防万一知道, flask 文档在使用 gunicorn 时推荐app:create_app()模式。 我怀疑您的一些新用户由于构建 Flask 应用程序而首先尝试使用 gunicorn,他们将尝试使用这些文档中现已失效的建议(至少这是我的经验)。

我可以联系该团队要求他们更新,但是我会等待@berkerpeksagexec的下降进行权衡,以防万一将其重新引入。

@tjwaterman99好吧,我不确定我是否喜欢以这种方式将参数传递给应用程序。 我不认为这是个好主意。 参数应该通过 env 传递。

我们自己的 Flask 用法示例正在执行我所描述的操作。 我认为目前的方式更容易处理。 想法?

抄送@tilgovi @berkerpeksag ^^

FWIW 我们也遇到了这个问题。

我预计会有很多人遵循“应用程序工厂”Flask 模式。
肯定有解决方法,但至少变更日志应该将其称为重大变更。

我认为我们从未有意支持诸如app:callable()app:callable(some, args)用法。 我会说这是在以前的实现中使用eval()一个不幸的副作用。

当前的实现非常接近 Django 的import_string()所做的:

https://github.com/django/django/blob/master/django/utils/module_loading.py#L7 -L24

我很乐意改进文档,添加发行说明,并提出更具描述性的错误消息。

我认为我们从未有意支持诸如app:callable () 和app:callable (some, args) 之类的用法。 我会说这是在之前的实现中使用 eval() 的一个不幸的副作用。

是的我同意。 就我所见,我们从不支持以这种方式启动应用程序。

对于更明确的错误,我是 +1。 如果应用程序名称不是简单名称,也许我们应该引发错误?

请记住,这是主要 wsgi 框架(flask)之一的公共文档中提到的显式行为,并且之前已得到您的项目的支持。 删除 eval 可以防止应用程序的延迟启动,如果应用程序 1) 由库提供,并且 2) 会产生非平凡的设置成本,则这是一个问题。 如果没有安全性或其他原因导致 eval 不合适,那么继续支持您现有的行为不是更简单吗?

如果有人遇到类似的情况,从 Python 3.7 开始,适当的解决方法是通过创建模块级__getattr__来伪造模块级变量,根据这个 PEP 。 这将允许延迟启动(a la 应用程序工厂)而不会影响 gunicorn 20.0.0 中的重大更改。

好吧,我们从来没有真正支持过这种行为,我们的文档或示例都没有使用它。 这不适合命令行。

但这确实是一个突破性的变化,出乎意料。 然后我会赞成放回eval并警告用户有关已弃用的行为。 也许也为了替换它并让人们使用“工厂”设计模式,我们可以添加一个设置--init-args

gunicorn -b :8000 --init-args="arg1,arg2"  app:factory_method

或者类似的东西。 想法?

@benoitc支持带有显式命令行标志的工厂方法会很棒😄 也许是这样的:

$ gunicorn -b :8000 \
  --callable \
  --callable-arg "abc" \
  --callable-arg "xyz" \
  --callable-kwarg "key" "value" \
  app:factory_method

(或者可能是另一个基本名称,例如--factory

我一直遇到此更改的问题,因为我再也无法轻松运行测试了。 因为 (i) 我的应用程序依赖于环境变量,(ii) 测试集合加载所有模块(用于 doctests)和 (iii) 我不能再推迟应用程序构建直到导入之后,我无法在不添加长字符串的情况下测试我的项目每个测试命令之前的环境变量,并且测试比以前花费的时间更长。

由于我使用的是 Python 3.7,我认为我可以使用模块级别的__getattr__解决这个问题,但是对于 3.7 之前的版本,我认为除了降级之外没有任何解决方案可以解决这个问题。

我认为支持带有命令行标志的工厂方法可以解决这个问题。 如果我错过了一个明显的解决方案,其他建议也将不胜感激🙃

@tjwaterman99好吧,我不确定我是否喜欢以这种方式将参数传递给应用程序。 我不认为这是个好主意。 参数应该通过 env 传递。

我们自己的 Flask 用法示例正在执行我所描述的操作。 我认为目前的方式更容易处理。 想法?

我同意,我认为通过环境传递参数更直观,并鼓励用户将他们的配置集中在一个地方。 然而,支持可调用对象/工厂至少对 Flask 很重要,也许其他框架也很重要。

+1 用于发出警告,并在弃用exec下一个版本之前提供有关如何将 Gunicorn 与工厂一起使用的说明。

不幸的是,这发生了。 我们有两种应对方式。 我们可以改变这种行为,或者我们可以帮助每个人迁移。

如果我们要改变这种行为,从 PyPI 中撤出发布可能是有意义的,但我认为这太激烈了。 Gunicorn 从未记录或建议这种用法。

因此,我建议我们只是帮助大家适应,并为给您带来的不便表示歉意。

我们应该通过 PR 联系 Flask 以更新他们的文档。 我很乐意这样做。 我认为其他人已经在这里记录了迁移路径。

我将补充一些建议,即拥有一个 _separate_ 模块或脚本来导入应用程序工厂、调用它并导出它会很有用。 这可以作为 Gunicorn 入口点,并且可以从 doctest 和其他工具中省略,以便在开发中运行这些工具时不会触发不需要的导入。 像__main__.pyweb.py脚本之类的东西可以解决这个问题。

将来,即使我们认为发布应该是安全的,我们也应该提供候选发布。 我们本可以在发布候选中发现这一点,然后有机会在我们的发布说明中记录重大更改,或者在一个周期内弃用它。

我认为在命令行上添加对初始化参数的支持没有意义。 这个版本为时已晚; 我们已经支持高级用例的自定义应用程序; 许多框架都有自己推荐的将设置传递给应用程序的方法。 Gunicorn 应该不需要提供自己的。 尝试添加参数来解决此问题会扩大未来此类重大更改的表面积。 我们应该尽量减少 Gunicorn 的 CLI 界面。

我们应该通过 PR 联系 Flask 以更新他们的文档。 我很乐意这样做。 我认为其他人已经在这里记录了迁移路径。

我看到@bilalshaikh42已经在https://github.com/pallets/flask/pull/3421上做到了这一点

(此处是 Flask 维护者之一)

虽然我完全同意在那里摆脱eval但我认为应该明确支持应用工厂! 应用程序工厂的全部意义在于避免具有可导入的app对象(因为使用它通常会导致循环依赖地狱)。

flask run cli(仅用于开发)中,我们实际上添加了对应用工厂的明确支持,因为它们太常见了。

当然,创造了wsgi.pyfrom myapp. import make_app; app = make_app()容易。 但是我要么需要单独维护这个文件(这很不方便,因为现在pip install myapp不会安装运行它所需的一切),或者把它放在我的包中(这意味着现在你可以从内部导入它应用程序本身是错误的)

在 Flask 中,我们采用了一种明确的方式来检查可调用的应用程序工厂并调用它,而无需求助于eval - 也许您也可以考虑这样的事情? 如果你想要更少的魔法,你甚至可以使用不同的 CLI args 来指向应用程序和指向应用程序工厂。

将来,即使我们认为发布应该是安全的,我们也应该提供候选发布。 我们本可以在发布候选中发现这一点,然后有机会在我们的发布说明中记录重大更改,或者在一个周期内弃用它。

不确定 RC 是否真的有帮助 - 通常人们不会使用--pre安装/升级(也是因为它的工作方式很糟糕 - 它不仅影响明确指定的包,而且影响所有嵌套的依赖关系,无论多深,因此,依赖项的某些依赖项很容易导致预发布损坏),因此任何没有固定版本的人在实际发布之前都不会发现任何损坏。

就其价值而言, zope.hookable提供了一种简单的方法来实现类似懒惰的工厂方法,而基本上没有开销(由于可选的 C 扩展)。 但是,它对传递额外的参数没有任何作用。

# app.py
from zope.hookable import hookable

def make_app():
    def _(environ, start_response, value=b'Hello'):
        start_response('200 OK',
                       [('Content-Type', 'text/plain')])
        return [value]
    return _

<strong i="8">@hookable</strong>
def app(environ, start_response):
    real_app = make_app()
    app.sethook(real_app)
    return real_app(environ, start_response, b"First time")
$ gunicorn app:app
[2019-11-12 05:53:47 -0600] [12457] [INFO] Starting gunicorn 20.0.0
[2019-11-12 05:53:47 -0600] [12457] [INFO] Listening at: http://127.0.0.1:8000 (12457)
[2019-11-12 05:53:47 -0600] [12457] [INFO] Using worker: sync
[2019-11-12 05:53:47 -0600] [12460] [INFO] Booting worker with pid: 12460
...
% http localhost:8000
HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain
Date: Tue, 12 Nov 2019 11:53:49 GMT
Server: gunicorn/20.0.0
Transfer-Encoding: chunked

First time

% http localhost:8000
HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain
Date: Tue, 12 Nov 2019 11:53:51 GMT
Server: gunicorn/20.0.0
Transfer-Encoding: chunked

Hello

当然,创建一个包含from myapp. import make_app; app = make_app()wsgi.py from myapp. import make_app; app = make_app()很容易。 但是我要么需要单独维护这个文件(这很不方便,因为现在pip install myapp不会安装运行它所需的一切),或者把它放在我的包中(这意味着现在你可以从内部导入它应用程序本身是错误的)

项目中有wsgi.py另一个原因是错误的:有些工具会导入项目中的所有模块; 例如。 pytest 在寻找 doctests 时会这样做。

另一个 Flask 维护者在这里。 @ThiefMaster已经说了我想说的一切,所以我主要是重申我对该功能的支持。

我同意摆脱eval ,我在flask run避免了它。 您可以添加先前行为的更受约束的版本。 如果命令行选项有括号,假设它是一个返回真实应用程序的工厂。 使用literal_eval解析括号的内容,然后使用解析的参数调用工厂。

我认为没有wsgi.py文件的工厂模式非常有价值。 我想帮助找到一种方法将它保留在 Gunicorn 中。

有人想为类似工厂的应用程序字符串的literal_eval组合一个 PR 吗? 这将在gunicorn.util.import_app

需要添加测试,但这里是 Flask 的代码移植到 Gunicorn: https :

@davidism如果您有兴趣,这里有一个函数可能对从应用程序工厂加载应用程序有用(使用 doctests 😄)。 它使用 Python 的内置 AST 解析器来区分属性名称和函数调用(而不是正则表达式)。 它还支持工厂函数中的关键字参数。 一切仍然使​​用ast.parseast.literal_eval进行评估,因此没有eval调用:

import ast
from types import ModuleType
from typing import Any


def get_app(module: ModuleType, obj: str) -> Any:
    """
    Get the app referenced by ``obj`` from the given ``module``.

    Supports either direct named references or app factories, using `ast.literal_eval` for safety.

    Example usage::

        >>> import collections
        >>> get_app(collections, 'Counter')
        <class 'collections.Counter'>
        >>> get_app(collections, 'Counter()')
        Counter()
        >>> get_app(collections, 'import evil_module')  # doctest: +ELLIPSIS
        Traceback (most recent call last):
          ...
        ValueError: Could not parse 'import evil_module' as a reference to a module attribute or app factory.
        >>> get_app(collections, '(lambda: sys.do_evil)()')
        Traceback (most recent call last):
            ...
        ValueError: App factories must be referenced by a simple function name
        >>> get_app(collections, '(1, 2, 3)')
        Traceback (most recent call last):
            ...
        ValueError: Could not parse '(1, 2, 3)' as a reference to a module attribute or app factory.
    """
    # Parse `obj` to an AST expression, handling syntax errors with an informative error
    try:
        # Note that mode='eval' only means that a single expression should be parsed
        # It does not mean that `ast.parse` actually evaluates `obj`
        expression = ast.parse(obj, mode='eval').body
    except SyntaxError as syntax_error:
        raise ValueError("Could not parse '{}' as a reference to a module attribute or app factory.".format(obj)) from syntax_error

    # Handle expressions that just reference a module attribute by name
    if isinstance(expression, ast.Name):
        # Expression is just a name, attempt to get the attribute from the module
        return getattr(module, expression.id)

    # Handle expressions that make a function call (factory)
    if isinstance(expression, ast.Call):
        # Make sure the function name is just a name reference
        if not isinstance(expression.func, ast.Name):
            raise ValueError("App factories must be referenced by a simple function name")

        # Extract the function name, args and kwargs from the call
        try:
            name = expression.func.id
            args = [ast.literal_eval(arg) for arg in expression.args]
            kwargs = {keyword.arg: ast.literal_eval(keyword.value) for keyword in expression.keywords}
        except ValueError as value_error:
            raise ValueError("Could not evaluate factory arguments, please ensure that arguments include only literals.") from value_error

        # Get and call the function, passing in the given arguments:
        return getattr(module, name)(*args, **kwargs)

    # Raise an error, we only support named references and factory methods
    raise ValueError("Could not parse '{}' as a reference to a module attribute or app factory.".format(obj))

如果决定仅支持零参数应用工厂,则可以删除所有参数处理代码。 它仍然可以很好地安全区分名称和工厂调用(并且在用户尝试将参数传递给工厂时向用户提供特定的错误消息很有用)

@ThiefMaster我仍然不相信我们应该支持这种模式。 它如何有用? 如果确实需要,为什么不使用环境变量来传递自定义参数或配置?

当然,创建一个包含来自 myapp 的 wsgi.py。 导入 make_app; app = make_app() 很简单。 但我要么需要单独维护这个文件(这很不方便,因为现在 pip install myapp 不会安装运行它所需的一切),或者把它放在我的包中(这意味着现在你可以从应用程序内部导入它)这是错误的)

我不明白,为什么必须单独维护这样的文件?

如果它在包中,那么它是可导入的。 因此,如果您有一个更大的项目,那么最终有人会直接导入它,而不是使用current_app等。也就是说,在处理包含此类错误的 PR 时需要做更多的工作。

如果它在包外,则在执行pip install时将无法获得它。


FWIW,我真的不在乎传递参数。 通常不需要这些(环境变量确实是要走的路)。 但至少能够指向可调用的应用程序工厂而不是应用程序对象是非常有用的!

如果确实需要,为什么不使用环境变量来传递自定义参数或配置?

pytest加载项目中的每个模块以查找测试。 如果您有一个依赖于环境变量或配置文件的全局app=Flask()对象,则在运行测试时将加载该对象。 无需设置环境变量或配置文件即可运行测试非常有用。 应用工厂模式非常适合这一点。

由于一些流行的 Flask 教程,带有参数模式的工厂有点常见,这就是我在flask run支持它的原因。 我同意最好使用环境来配置应用程序,所以我会用更精简的版本来支持不带参数调用工厂。

# an app is a name only
$ gunicorn module:app

# a factory has (), but no args allowed
$ gunicorn module:factory()

@tilgovi我同意。 我的主要问题是我们没想到这会破坏任何人,因此为什么我建议放回 eval(或更安全的东西)并弃用它。 另一方面,是的,这种行为从未得到支持,并且是使用eval的不幸结果:/

@davidism有趣。 但是它与使用可调用对象作为应用程序有何不同?

不明白你的意思,能举个更具体的例子吗? 工厂返回 WSGI 应用程序,它本身不是 WSGI 应用程序。

@davidism我的意思是这样的


def make_app():
  from mymodule.application import MainApp
  return MainApp()

application = make_app()

然后有人跑gunicorn -b :8000 somemodule:application

这导致application在导入代码时总是被评估,违背了工厂的目的。

一个 WSGI 可调用对象也可以是一个类实例,所以也许这就是本意:

class Application:
    _app = None
    def __call__(self, environ, start_response):
        if self._app is None:
            from wherever import make_app
            self._app = make_app()
        return self._app(environ, start_response)

application = Application()

zope.hookable示例本质上是相同的,只是在稳态条件下开销更少。)

拥有一个创建真正 WSGI 应用程序的 WSGI 应用程序并不理想。 现在,它是对代理到真正入口点的每个请求的额外函数调用。 设置应该在第一个请求之前完成,但现在它被推迟到那个时候,使得第一个请求花费(可能很多)更长的时间。

这里有问题的功能是一个工厂函数,它根据运行时/环境配置创建该对象,这对于解耦应用程序部分、避免循环导入和更容易的测试隔离非常有用。 在代码中的某处明确调用工厂类型会破坏解耦目的,因为我保证用户会认为“哦,我现在应该导入这个应用程序对象”,而他们应该使用 Flask 中可用的功能。

此时,我们所要求的只是“如果导入字符串以括号结尾,则调用导入的名称以获取应用程序”。

我认为我们有很多方法可以解决这个问题,但这仅意味着我们不是目标受众。 我知道我可以做一些事情,比如将一个包外的脚本作为我的容器的入口点,并配置 pytest 来忽略它,等等,但我们想关心那些正在学习教程并且可能会遇到这种情况的人不明白回溯。

一个非常有限的“如果应用程序对象可以用零参数调用,然后将其作为工厂调用”模式可能会起作用,但如果可调用对象实际上是一个 WSGI 应用程序,该应用程序装饰得很糟糕并且不会很容易地从内省中显示其参数. 如果我们想大方,我们应该支持我们之前拥有的一切,同时避免eval ,所以我认为这是我们应该做的道路。

我真的很感谢大家的所有建议并帮助解决这个问题。

我喜欢@davidism@connorbrinton使用literal_eval

这会导致在导入代码时总是对应用程序进行评估,从而违背了工厂的目的。

那么它会在运行时初始化应用程序并返回工作人员使用的可调用对象。 那不是那么不同。

我对这种模式的主要保留是它鼓励人们运行一些预生成代码,这可能会打破对 HUP 或 USR2 的期望。 它也打破了当前的用户界面。 它是否适用于 gunicorn 的未来用法?

无论如何,选择如下:

  1. 我们可以认为这种行为不受支持,没有记录(在 gunicorn 中)。 基于它所做的更改。
  2. 一些用户依赖它,现在我们想要支持这种行为

(1) 是一个艰难的选择,但也是合乎逻辑的路径,考虑到我们从未支持过它。
(2) 某种美学并打破了命令行 UI,它需要一些测试/示例在 gunicorn 中进行测试,使用诸如literal_evals之类的东西

我们可以支持 (2) 但我想进行一些测试。 我们还应该记录它吗?

@tilgovi @jamadden @berkerpeksag @sirkonst你的偏好是什么?

看起来另一个破坏性用例是属性访问,它不会被literal_eval

例如,在 Plotly Dash 中,您使用Dash对象,该对象内部有一个Flask实例作为server属性。 有些人在使用:

gunicorn "module:app.server"

我不确定这是否应该得到支持。 flask run也不支持它。 似乎Dash对象应该有一个传递给Flask.__call____call__方法。 此外, Dash 的文档说要执行server = app.server并指向 Gunicorn,所以这似乎主要是传递了一些不正确信息的情况。

@davidism我今天生病了,但我会在这个星期天看看它,以便在星期一发布。 @tilgovi的建议很好,总体计划是用一个安全的 eval 代替旧的 eval。

我认为我们应该警告用户他正在使用这种初始化。 想法? 抄送@tilgovi

除非您想创建不同的实现,否则我将尝试将我上面链接的分支转换为周六测试的 PR。

@davidism继续。 我会在星期天回来,如果需要,我会审查它:) 谢谢!

有点落后,现在正在处理这个。

@connorbrinton使用ast.parse好主意,我会尝试一下,如果我同意的话,我将把你作为共同作者包括在提交中。

只是想补充一下,有一个有点流行(而且很老)的 Stack Overflow 答案,它引导用户使用 v19 行为,这可能需要根据所做的选择进行更新: https :

固定在主人。 感谢@davidism的补丁!

处理的所有案例都在此测试中: https :

此页面是否有帮助?
0 / 5 - 0 等级