43

有没有办法让 Python 静态分析器(例如在 PyCharm 中,其他 IDE 中)在对象上的 Typehints 上进行检测argparse.Namespace?例子:

parser = argparse.ArgumentParser()
parser.add_argument('--somearg')
parsed = parser.parse_args(['--somearg','someval'])  # type: argparse.Namespace
the_arg = parsed.somearg  # <- Pycharm complains that parsed object has no attribute 'somearg'

如果我删除内联注释中的类型声明,PyCharm 不会抱怨,但它也不会拾取无效属性。例如:

parser = argparse.ArgumentParser()
parser.add_argument('--somearg')
parsed = parser.parse_args(['--somearg','someval'])  # no typehint
the_arg = parsed.somaerg   # <- typo in attribute, but no complaint in PyCharm.  Raises AttributeError when executed.

有任何想法吗?


更新

奥斯汀下面回答的启发,我能找到的最简单的解决方案是使用namedtuples

from collections import namedtuple
ArgNamespace = namedtuple('ArgNamespace', ['some_arg', 'another_arg'])

parser = argparse.ArgumentParser()
parser.add_argument('--some-arg')
parser.add_argument('--another-arg')
parsed = parser.parse_args(['--some-arg', 'val1', '--another-arg', 'val2'])  # type: ArgNamespace

x = parsed.some_arg  # good...
y = parsed.another_arg  # still good...
z = parsed.aint_no_arg  # Flagged by PyCharm!

虽然这是令人满意的,但我仍然不喜欢重复参数名称。如果参数列表显着增长,更新两个位置将是乏味的。理想的方法是以某种方式从对象中提取参数,parser如下所示:

parser = argparse.ArgumentParser()
parser.add_argument('--some-arg')
parser.add_argument('--another-arg')
MagicNamespace = parser.magically_extract_namespace()
parsed = parser.parse_args(['--some-arg', 'val1', '--another-arg', 'val2'])  # type: MagicNamespace

我无法在argparse模块中找到任何可以使这成为可能的东西,而且我仍然不确定是否有任何静态分析工具可以足够聪明地获得这些值并且不会使 IDE 陷入停顿。

仍在搜索中...


更新 2

根据 hpaulj 的评论,我能找到的最接近上述方法的“神奇地”提取已解析对象的属性的方法是dest从每个解析器的_actions 中提取属性:

parser = argparse.ArgumentParser()
parser.add_argument('--some-arg')
parser.add_argument('--another-arg')
MagicNamespace = namedtuple('MagicNamespace', [act.dest for act in parser._actions])
parsed = parser.parse_args(['--some-arg', 'val1', '--another-arg', 'val2'])  # type: MagicNamespace

但这仍然不会导致属性错误在静态分析中被标记。如果我通过电话,这也是正确namespace=MagicNamespaceparser.parse_args

4

6 回答 6

23

类型化参数解析器正是为此目的而制作的。它环绕argparse。您的示例实现为:

from tap import Tap


class ArgumentParser(Tap):
    somearg: str


parsed = ArgumentParser().parse_args(['--somearg', 'someval'])
the_arg = parsed.somearg

这是它的图片。 在此处输入图像描述

它在 PyPI 上,可以安装:pip install typed-argument-parser

完全披露:我是这个库的创建者之一。

于 2019-08-16T12:49:09.230 回答
19

考虑定义一个扩展类来argparse.Namespace提供你想要的类型提示:

class MyProgramArgs(argparse.Namespace):
    def __init__():
        self.somearg = 'defaultval' # type: str

然后使用namespace=将其传递给parse_args

def process_argv():
    parser = argparse.ArgumentParser()
    parser.add_argument('--somearg')
    nsp = MyProgramArgs()
    parsed = parser.parse_args(['--somearg','someval'], namespace=nsp)  # type: MyProgramArgs
    the_arg = parsed.somearg  # <- Pycharm should not complain
于 2017-02-16T16:38:47.783 回答
5

我对 PyCharm 如何处理这些类型提示一无所知,但了解Namespace代码。

argparse.Namespace是一个简单的类;本质上是一个带有一些方法的对象,可以更轻松地查看属性。为了便于单元测试,它有一个__eq__方法。您可以阅读argparse.py文件中的定义。

以最parser一般的方式与命名空间交互 - 使用getattr, setattr, hasattr. 因此,您几乎可以使用任何dest字符串,即使是您无法通过.dest语法访问的字符串。

确保不要混淆add_argument type=参数;这是一个功能。

按照其他答案中的建议使用您自己的namespace类(从头开始或子类)可能是最佳选择。文档中对此进行了简要描述。 命名空间对象。虽然我已经建议过几次来处理特殊的存储需求,但我还没有看到这样做太多。所以你必须进行实验。

如果使用子解析器,使用自定义命名空间类可能会中断,http: //bugs.python.org/issue27859

注意处理默认值。argparse大多数操作的默认默认值是None。如果用户没有提供此选项,则在解析后使用它来做一些特殊的事情很方便。

 if args.foo is None:
     # user did not use this optional
     args.foo = 'some post parsing default'
 else:
     # user provided value
     pass

这可能会妨碍类型提示。无论您尝试何种解决方案,请注意默认设置。


Anamedtuple不能作为Namespace.

首先,自定义命名空间类的正确使用是:

nm = MyClass(<default values>)
args = parser.parse_args(namespace=nm)

也就是说,您初始化该类的一个实例,并将其作为参数传递。返回的args将是相同的实例,具有通过解析设置的新属性。

其次,namedtuple 只能创建,不能更改。

In [72]: MagicSpace=namedtuple('MagicSpace',['foo','bar'])
In [73]: nm = MagicSpace(1,2)
In [74]: nm
Out[74]: MagicSpace(foo=1, bar=2)
In [75]: nm.foo='one'
...
AttributeError: can't set attribute
In [76]: getattr(nm, 'foo')
Out[76]: 1
In [77]: setattr(nm, 'foo', 'one')    # not even with setattr
...
AttributeError: can't set attribute

命名空间必须与getattrand一起使用setattr

另一个问题namedtuple是它没有设置任何类型的type信息。它只是定义字段/属性名称。所以静态类型不需要检查。

虽然从 中很容易获得预期的属性名称,但parser您无法获得任何预期的类型。

对于一个简单的解析器:

In [82]: parser.print_usage()
usage: ipython3 [-h] [-foo FOO] bar
In [83]: [a.dest for a in parser._actions[1:]]
Out[83]: ['foo', 'bar']
In [84]: [a.type for a in parser._actions[1:]]
Out[84]: [None, None]

Actionsdest是正常的属性名称。但type不是该属性的预期静态类型。它是一个可能会也可能不会转换输入字符串的函数。这None意味着输入字符串按原样保存。

因为静态类型argparse需要不同的信息,所以没有一种简单的方法可以从另一个生成一个。

我认为您能做的最好的事情是创建自己的参数数据库,可能在字典中,并使用您自己的实用程序函数创建命名空间类和解析器。

假设dd是带有必要键的字典。然后我们可以创建一个参数:

parser.add_argument(dd['short'],dd['long'], dest=dd['dest'], type=dd['typefun'], default=dd['default'], help=dd['help'])

您或其他人将不得不提出一个命名空间类定义,该类定义default从这样的字典中设置(简单)和静态类型(硬?)。

于 2017-02-16T16:55:42.390 回答
2

如果您处于可以从头开始的情况,那么有一些有趣的解决方案,例如

但是,就我而言,它们不是理想的解决方案,因为:

  1. 我有许多基于argparse.
  2. 从类型推断 args 时,支持普通argparse支持的所有高级 CLI 功能可能会很棘手。
  3. 与替代方案相比,在简单的命令式 argparse 中重用多个 CLI 中的公共 arg 定义通常更容易。

因此,我研究了一个小型库typed_argparse,它允许引入类型化的 args 而无需太多重构。这个想法是添加一个从特殊TypedArg类派生的类型,然后简单地包装普通argparse.Namespace对象:

# Step 1: Add an argument type.
class MyArgs(TypedArgs):
    foo: str
    num: Optional[int]
    files: List[str]


def parse_args(args: List[str] = sys.argv[1:]) -> MyArgs:
    parser = argparse.ArgumentParser()
    parser.add_argument("--foo", type=str, required=True)
    parser.add_argument("--num", type=int)
    parser.add_argument("--files", type=str, nargs="*")
    # Step 2: Wrap the plain argparser result with your type.
    return MyArgs(parser.parse_args(args))


def main() -> None:
    args = parse_args(["--foo", "foo", "--num", "42", "--files", "a", "b", "c"])
    # Step 3: Done, enjoy IDE auto-completion and strong type safety
    assert args.foo == "foo"
    assert args.num == 42
    assert args.files == ["a", "b", "c"]

这种方法稍微违反了单一来源的真实原则,但该库执行完整的运行时验证以确保类型注释与 argparse 类型匹配,并且它只是迁移到类型化 CLI 的一个非常简单的选项。

于 2021-08-19T22:33:45.100 回答
1

这些答案中的大多数都涉及使用另一个包来处理打字。仅当没有像我将要提出的那样简单的解决方案时,这才是一个好主意。

步骤 1. 类型声明

首先,在数据类中定义每个参数的类型,如下所示:

from dataclasses import dataclass

@dataclass
class MyProgramArgs:
    first_var: str
    second_var: int

步骤 2. 参数声明

然后,您可以使用匹配的参数设置您的解析器。例如:

import argparse

parser = argparse.ArgumentParser("This CLI program uses type hints!")
parser.add_argument("-a", "--first-var")
parser.add_argument("-b", "--another-var", type=int, dest="second_var")

步骤 3. 解析参数

最后,我们以静态类型检查器将知道每个参数的类型的方式解析参数:

my_args = MyProgramArgs(**vars(parser.parse_args())

现在类型检查器知道它my_args的类型MyProgramArgs,因此它确切地知道哪些字段可用以及它们的类型是什么。

于 2022-02-08T14:09:26.657 回答
0

如果您的论点很少,另一种可能是理想的方法如下。

首先创建一个函数来设置解析器并返回命名空间。例如:

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("-a")
    parser.add_argument("-b", type=int)
    return parser.parse_args()

然后定义一个 main 函数,该函数采用您在上面单独声明的参数;像这样。

def main(a: str, b: int):
    print("hello world", a, b)

当你调用你的 main 时,你会这样做:

if __name__ == "__main__":
    main(**vars(parse_args())

从您的主要开始,您将拥有变量a并被b静态类型检查器正确识别,尽管您将不再拥有包含所有参数的对象,这取决于您的用例,这可能是好事还是坏事。

于 2022-02-09T18:03:52.717 回答