python / python/cpython

ContextDecorator documentation is unclear.

未关闭
#134,537 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

docs stdlib
主要语言
Python
星标
77.2k
派生
35.9k
PR 合并指标
PR 指标待抓取

描述

Bug report

Bug description:
Overview:

ContextDecorator can not be safely used on functions that make recursive calls, or may be used with multithreading, even if those context managers support use in sequential with statements. The documentation suggests otherwise, and could be clarified. Additionally, this functionality could be added via a class method.

Detailed description

The documentation for ContextDecorator states:

This change is just syntactic sugar for any construct of the following form:

def f():
    with cm():
        # Do stuff

ContextDecorator lets you instead write:

@cm()
def f():
    # Do stuff

However, ContextDecorator is closer in functionality to the following:

cm = cm()
def f():
    with cm:
        # Do stuff

The documentation does contain the following warning. However, the wording could be more clear, especially given the syntactic sugar example.

Note As the decorated function must be able to be called multiple times, the underlying context manager must support use in multiple with statements. If this is not the case, then the original construct with the explicit with statement inside the function should be used.

Repro Code

This code demonstrates the issue:

import contextlib
import time
class timed(contextlib.ContextDecorator):
  def __enter__(self):
    self._start_time = time.monotonic()

  def __exit__(self, *exc):
    print(f"Execution took  {time.monotonic() - self._start_time:.0f} seconds")

@timed()
def my_func(recurse_once = False):
  time.sleep(1)
  if recurse_once:
    my_func()

my_func(recurse_once = True)

Expected output:

Execution took 1 seconds
Execution took 2 seconds

Actual output:

Execution took 1 seconds
Execution took 1 seconds

Potential fixes

Implementing one or more of these fixes could alleviate the issue.

1. Clarify the syntactic sugar section to show that all function invocations share a single instance of CM.

The "syntactic sugar" section could be changed to read:

ContextDecorator lets you instead write:

@cm()
def f():
    # Do stuff

Which is equivalent to:

cm = cm()
def f():
    with cm:
        # Do stuff
2. Clarify the warning note

The warning note could be changed to make it more clear that separate functions share state in the context manager.

Note The underlying context manager is instantiated once when the function definition is evaluated: this instance is shared between all calls to the function. As the decorated function must be able to be called multiple times, the underlying context manager must support use in multiple with statements. If this is not the case, then the original construct with the explicit with statement inside the function should be used.

3. Provide a method for wrapping functions with stateful context managers

For example:

def wrap_with_context(cm_factory, *cm_args, **cm_kwargs):
  """Wraps the decorated function with a context created by cm_factory."""

  def wrap(func):
    @functools.wraps(func)
    def inner(*func_args, **func_kwargs):
      with cm_factory(*cm_args, **cm_kwargs):
        return func(*func_args, **func_kwargs)
    return inner
  return wrap

The previous example now works as expected:

@wrap_with_context(timed)
def my_func2(recurse_once = False):
  time.sleep(1)
  if recurse_once:
    my_func2()

my_func2(recurse_once = True)

Actual output:

Execution took 1 seconds
Execution took 2 seconds

4. Add class method to ContextDecorator that acts as a decorator and a factory.
class ContextDecorator:
...
  @classmethod
  def wrap(cls, *cls_args, **cls_kwargs):
    def enclose(func):
      @functools.wraps(func)
      def inner(*args, **kwds):
        with cls(*cls_args, **cls_kwargs):
          return func(*args, **kwds)
      return inner
    return enclose

This allows usage that is similar to existing, but instantiates a separate CM for each function invocation:

@timed.wrap()
def my_func2(recurse_once = False):
  time.sleep(1)
  if recurse_once:
    my_func2()
CPython versions tested on:

3.13

Operating systems tested on:

macOS

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从链接的 Python 文档中的 ContextDecorator 部分开始,尤其关注语法糖示例和警告说明。明确说明底层的一个上下文管理器实例会在函数调用之间共享,并解释何时需要显式的 with 语句;修订后的文档应准确描述递归使用和并发使用。

由索引模型根据 Issue 内容生成。

评估

技术栈
python
领域
documentation
Issue 类型
文档
难度
2/5
预计耗时
1-3 小时
活跃度
停滞
描述清晰度
基本清楚
新手友好度
45/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。