【问题标题】:Detect if route is not outermost / wrong "order" of decorators in Flask检测路线是否不是Flask中装饰器的最外层/错误“顺序”
【发布时间】:2019-04-17 06:15:24
【问题描述】:

由于@route 装饰器必须使用给予装饰器的当前回调来注册视图,它has to be the outermost decorator 在处理请求时接收要调用的正确函数。

这可能会导致视图已被装饰,但由于装饰器的顺序错误,因此不会调用被装饰的函数。如果用于装饰需要用户登录,具有特定角色或具有特定标志的视图,则该检查将被静默省略。

我们当前的解决方法是让标准操作是拒绝访问资源,然后要求装饰器允许访问。在这种情况下,如果在处理请求时没有调用装饰器,则请求将失败。

但是在某些用例中这会变得很麻烦,因为它需要您装饰所有视图,除了少数应该被豁免的视图。对于纯粹的分层布局,这可能有效,但对于检查单个标志,结构可能会变得复杂。

是否有适当的方法来检测我们在装饰层次结构中的有用位置被调用? IE。我们能否检测到尚未将route 装饰器应用于我们要包装的函数?

# wrapped in wrong order - @require_administrator should be after @app.route
@require_administrator
@app.route('/users', methods=['GET'])

实现为:

def require_administrator(func):
    @functools.wraps(func)
    def has_administrator(*args, **kwargs):
        if not getattr(g, 'user') or not g.user.is_administrator:
            abort(403)

        return func(*args, **kwargs)

    return has_administrator

在这里我想检测我的自定义装饰器是否在@app.route 之后被包装,因此在处理请求时永远不会被调用。

使用functools.wraps在所有方面都将被包装的函数替换为新的,所以查看要被包装的函数的__name__会失败。这也发生在装饰器包装过程的每一步。

我尝试查看tracebackinspect,但没有找到任何合适的方法来确定序列是否正确。

更新

我目前最好的解决方案是根据已注册的端点集检查调用的函数名称。但是由于 Route() 装饰器可以更改端点的名称,因此在这种情况下,我也必须为我的装饰器支持它,并且如果不同的函数使用与当前相同的端点名称,它将静默通过函数。

它还必须迭代注册的端点集,因为我无法找到一种简单的方法来检查端点名称是否存在(通过尝试使用它构建 URL 并捕获异常可能更有效)。

def require_administrator_checked(func):
    for rule in app.url_map.iter_rules():
        if func.__name__ == rule.endpoint:
            raise DecoratorOrderError(f"Wrapped endpoint '{rule.endpoint}' has already been registered - wrong order of decorators?")

    # as above ..

【问题讨论】:

    标签: python flask python-decorators


    【解决方案1】:

    更新 2:请参阅我的其他答案以获得更可重用、更少 hack-y 的解决方案。

    更新: 这是一个绝对不那么骇人听闻的解决方案。但是,它要求您使用 自定义函数而不是app.route。它接受任意数量的装饰器,并按照给定的顺序应用它们,然后确保 app.route 作为最终函数被调用。 这要求您为每个函数使用这个装饰器。

    def safe_route(rule, app, *decorators, **options):
        def _route(func):
            for decorator in decorators:
                func = decorator(func)
            return app.route(rule, **options)(func)
        return _route
    

    然后你可以像这样使用它:

    def require_administrator(func):
        @functools.wraps(func)
        def has_administrator(*args, **kwargs):
            print("Would check admin now")
    
            return func(*args, **kwargs)
    
        return has_administrator
    
    @safe_route("/", app, require_administrator, methods=["GET"])
    def test2():
        return "foo"
    
    test2()
    print(test2.__name__)
    

    打印出来:

    Would check admin now
    foo
    test2
    

    因此,如果所有提供的装饰器都使用 functools.wraps,这也会保留 test2 名称。

    旧答案: 如果您对公认的 hack-y 解决方案感到满意,您可以通过逐行读取文件来进行自己的检查。这是一个非常粗略的功能。您可以对其进行相当多的改进,例如目前它依赖于被称为“应用程序”的应用程序, 函数定义之前至少有一个空行(正常的 PEP-8 行为,但仍然可能是一个问题),...

    这是我用来测试它的完整代码。

    import flask
    import functools
    from itertools import groupby
    
    
    class DecoratorOrderError(TypeError):
        pass
    
    
    app = flask.Flask(__name__)
    
    
    def require_administrator(func):
        @functools.wraps(func)
        def has_administrator(*args, **kwargs):
            print("Would check admin now")
    
            return func(*args, **kwargs)
    
        return has_administrator
    
    
    @require_administrator  # Will raise a custom exception
    @app.route("/", methods=["GET"])
    def test():
        return "ok"
    
    
    def check_route_is_topmost_decorator():
        # Read own source
        with open(__file__) as f:
            content = [line.strip() for line in f.readlines()]
    
        # Split source code on line breaks
        split_by_lines = [list(group) for k, group in groupby(content, lambda x: x == "") if not k]
    
        # Find consecutive decorators
        decorator_groups = dict()
        for line_group in split_by_lines:
            decorators = []
            for line in line_group:
                if line.startswith("@"):
                    decorators.append(line)
                elif decorators:
                    decorator_groups[line] = decorators
                    break
                else:
                    break
    
        # Check if app.route is the last one (if it exists)
        for func_def, decorators in decorator_groups.items():
            is_route = [dec.startswith("@app.route") for dec in decorators]
            if sum(is_route) > 1 or (sum(is_route) == 1 and not decorators[0].startswith("@app.route")):
                raise DecoratorOrderError(f"@app.route is not the topmost decorator for '{func_def}'")
    
    
    check_route_is_topmost_decorator()
    

    这个sn-p会给你以下错误:

    Traceback (most recent call last):
      File "/home/vXYZ/test_sso.py", line 51, in <module>
        check_route_is_topmost_decorator()
      File "/home/vXYZ/test_sso.py", line 48, in check_route_is_topmost_decorator
        raise DecoratorOrderError(f"@app.route is not the topmost decorator for '{func_def}'")
    __main__.DecoratorOrderError: @app.route is not the topmost decorator for 'def test():'
    

    如果您为test() 函数切换装饰器的顺序,它根本什么都不做。

    一个缺点是您必须在每个文件中显式调用此方法。 我不知道这有多可靠,我承认它很丑,如果它坏了我不会承担任何责任,但这是一个开始!我相信一定有更好的方法。

    【讨论】:

    • 不错!不过,在hacky方面有点沉重:-)
    • 是的,我知道,我不会在生产中部署它。但这是一个有趣的问题,我只是想提供一些解决方案。
    • 我现在找到了一种稍微不那么老套的方法,但它仍然不理想(并且在某些情况下它会失败)。感谢您的时间和可能的解决方案!
    • 我会对您的解决方案感兴趣!
    • 我已将其添加为问题的更新 :-) 它涉及检查端点名称(默认情况下为函数的名称)是否已在 url_map 中注册为端点。如果有,则 Route() 装饰器已被评估,我们可以抛出 DecoratorOrderError
    【解决方案2】:

    我正在添加另一个答案,因为现在我有一些东西是最少的 hacky(阅读:我使用检查来读取给定函数的源代码,而不是自己读取整个文件),跨模块工作,并且可以重复用于任何其他应该始终是最后一个的装饰器。您也不必像更新我的其他答案那样对app.route 使用不同的语法。

    这里是如何做到这一点(警告:这是一个相当封闭的开始):

    import flask
    import inspect
    
    
    class DecoratorOrderError(TypeError):
        pass
    
    
    def assert_last_decorator(final_decorator):
        """
        Converts a decorator so that an exception is raised when it is not the last    decorator to be used on a function.
        This only works for decorator syntax, not if somebody explicitly uses the decorator, e.g.
        final_decorator = some_other_decorator(final_decorator) will still work without an exception.
    
        :param final_decorator: The decorator that should be made final.
        :return: The same decorator, but it checks that it is the last one before calling the inner function.
        """
        def check_decorator_order(func):
            # Use inspect to read the code of the function
            code, _ = inspect.getsourcelines(func)
            decorators = []
            for line in code:
                if line.startswith("@"):
                    decorators.append(line)
                else:
                    break
    
            # Remove the "@", function calls, and any object calls, such as "app.route". We just want the name of the decorator function (e.g. "route")
            decorator_names_only = [dec.replace("@", "").split("(")[0].split(".")[-1] for dec in decorators]
            is_final_decorator = [final_decorator.__name__ == name for name in decorator_names_only]
            num_finals = sum(is_final_decorator)
    
            if num_finals > 1 or (num_finals == 1 and not is_final_decorator[0]):
                raise DecoratorOrderError(f"'{final_decorator.__name__}' is not the topmost decorator of function '{func.__name__}'")
    
            return func
    
        def handle_arguments(*args, **kwargs):
            # Used to pass the arguments to the final decorator
    
            def handle_function(f):
                # Which function should be decorated by the final decorator?
                return final_decorator(*args, **kwargs)(check_decorator_order(f))
    
            return handle_function
    
        return handle_arguments
    

    您现在可以用此函数替换 app.route 函数,应用于 app.route 函数。这很重要,必须在使用 app.route 装饰器之前完成,所以我建议在创建应用程序时这样做。

    app = flask.Flask(__name__)
    app.route = assert_last_decorator(app.route)
    
    
    def require_administrator(func):
        @functools.wraps(func)
        def has_administrator(*args, **kwargs):
            print("Would check admin now")
    
            return func(*args, **kwargs)
    
        return has_administrator
    
    
    @app.route("/good", methods=["GET"])  # Works
    @require_administrator
    def test_good():
        return "ok"
    
    @require_administrator
    @app.route("/bad", methods=["GET"])  # Raises an Exception
    def test_bad():
        return "not ok"
    

    我相信这正是您想要的问题。

    【讨论】:

    • 感谢您的多次尝试!这似乎很有希望。我会暂时保留这个问题,看看是否有人有更简单的答案,因为这仍然需要特殊的应用程序处理。
    • 我认为如果不进行任何修改,您将无法完全做到这一点。至少这样你只需要添加一行到“更新”app.route,每个应用只需要添加一次。
    • @MatsLindh 你有没有发现更好的东西?
    • 我目前已经使用我在问题中作为更新添加的版本,因为它至少适用于当前项目
    猜你喜欢
    • 2015-03-28
    • 2012-03-15
    • 2018-05-08
    • 1970-01-01
    • 2016-03-13
    • 1970-01-01
    • 1970-01-01
    • 2011-07-26
    • 1970-01-01
    相关资源
    最近更新 更多