python / python/typing

Support decorators that modify parameter types and preserve keywords

オープン
#1,505 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

topic: feature
主要言語
Python
スター
1.8k
フォーク
302
平均マージ
23時間
マージ済み PR(30日)
8

説明

For osandov/drgn#364, I'd like to be able to define a decorator that modifies the parameter types of the functions it wraps, but also preserves keywords.

For a toy example, imagine a decorator that wraps a function taking an int as the first parameter (possibly as a keyword argument) so that it can also take a str that is automatically converted to an int. I can do this manually with overloads:

import inspect
import functools
from typing import Any, Callable, TypeVar, overload


R = TypeVar("R")


def takes_int_or_str(f: Callable[..., R]) -> Callable[..., R]:
    param = next(iter(inspect.signature(f).parameters))

    @functools.wraps(f)
    def wrapper(*args: Any, **kwds: Any) -> R:
        if param in kwds:
            if isinstance(kwds[param], str):
                kwds[param] = int(kwds[param])
        elif isinstance(args[0], str):
            return f(int(args[0]), *args[1:], **kwds)
        return f(*args, **kwds)

    return wrapper


@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> int: ...
@takes_int_or_str
def f(x: int) -> int:
    return x * x


f(1)
f("2")
f(x="3")

But this is tedious and error-prone if you have a lot of functions using the decorator. ParamSpec almost gets me what I want, but it doesn't preserve keywords:

import inspect
import functools
from typing import Any, Callable, Concatenate, ParamSpec, TypeVar


P = ParamSpec("P")
R = TypeVar("R")


def takes_int_or_str(
    f: Callable[Concatenate[int, P], R]
) -> Callable[Concatenate[int | str, P], R]:
    param = next(iter(inspect.signature(f).parameters))

    @functools.wraps(f)
    def wrapper(*args: Any, **kwds: Any) -> R:
        if param in kwds:
            if isinstance(kwds[param], str):
                kwds[param] = int(kwds[param])
        elif isinstance(args[0], str):
            return f(int(args[0]), *args[1:], **kwds)
        return f(*args, **kwds)

    return wrapper


@takes_int_or_str
def f(x: int) -> int:
    return x * x


f(1)
f("2")
# error: Unexpected keyword argument "x" for "f"  [call-arg]
f(x="3")

This seems somewhat related to #1273, although I want to specify the transformation by parameter, not by input type.

P.S. I used Callable here, but my real use cases need a Protocol with __call__ because they have more complicated signatures.

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

この issue のデコレータの例から始め、要求されている動作を関連する issue #1273 と比較してください。パラメータベースの変換が ParamSpec、キーワード引数、Callable、Protocol のシグネチャとどのように相互作用すべきかを検討してください。指定された位置引数呼び出しとキーワード呼び出しをサポートできるだけ十分に動作が仕様化されていれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
developer-experience
issue の種類
機能追加
難易度
5/5
見積もり時間
1週間以上
活発さ
停滞
明瞭さ
おおむね明確
初心者へのやさしさ
30/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。