pycharm Python:为argparse键入提示,命名空间对象

zd287kbt  于 2022-11-08  发布在  PyCharm
关注(0)|答案(7)|浏览(142)

有没有办法让Python静态分析器(例如在PyCharm和其他IDE中)在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.

有什么想法吗?

更新

受下面Austin's answer的启发,我能找到的最简单的解决方案是使用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的注解,我所能找到的与上述方法最接近的东西是从解析器的每个_action中提取dest属性,该方法将“神奇地”提取解析对象的属性。

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

但是这仍然不会导致在静态分析中标记属性错误。如果我在parser.parse_args调用中传递namespace=MagicNamespace,也是如此。

rkkpypqq

rkkpypqq1#

Typed argument parser正是为此目的而创建的。它 Package 了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
充分披露:我是这个图书馆的创建者之一。

5t7ly7z5

5t7ly7z52#

请考虑定义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
368yc8dk

368yc8dk3#

我不知道PyCharm是如何处理这些类型提示的,但我理解Namespace代码。
argparse.Namespace是一个简单类;本质上是一个对象,它有几个方法,使它更容易查看属性。为了便于单元测试,它有一个__eq__方法。你可以在argparse.py文件中阅读定义。
parser以最通用的方式与命名空间交互--getattrsetattrhasattr。因此,几乎可以使用任何dest字符串,即使是那些不能用.dest语法访问的字符串。
请确保您没有混淆add_argumenttype=参数;这是一个函数
使用你自己的namespace类(从头开始或子类化)可能是最好的选择。这在文档中有简要的描述。命名空间对象。我还没有看到它做了很多,尽管我已经建议过几次它来处理特殊的存储需求。所以你必须进行实验。
如果使用子解析器,则使用自定义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

这可能会妨碍类型提示。无论你尝试什么解决方案,都要注意默认值。
namedtuple不能像Namespace那样工作。
首先,自定义Namespace类的正确用法是:

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

也就是说,初始化该类的一个示例,并将其作为参数传递。返回的args将是同一个示例,并通过解析设置了新的属性。
第二,命名元组只能创建,不能更改。

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

命名空间必须与getattrsetattr一起使用。
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]

动作dest是一般属性名称。但type不是该属性的预期静态类型。它是一个可能会也可能不会转换输入字串的函数。这里的None表示输入字串会储存成原来的样子。
因为静态类型和argparse需要不同的信息,所以没有一种简单的方法可以从另一个生成一个。
我认为最好的办法是创建自己的参数数据库,可能是在字典中,然后使用自己的实用函数从数据库中创建Namespace类和parsesr。
假设dd是一个字典,其中包含了所需的键,那么我们可以创建一个参数:

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

您或其他人将必须从这样的字典中提供一个Namespace类定义来设置default(easy)和静态类型(hard?)。

pnwntuvh

pnwntuvh4#

如果你在一个情况下,你可以从头开始有有趣的解决方案,如

然而,在我的案例中,它们并不是理想的解决方案,因为:
1.我有许多现有的基于argparse的CLI,我无法使用这种args-inferred-from-types方法全部重写它们。
1.当从类型推断参数时,支持普通argparse支持的所有高级CLI功能可能会很棘手。
1.在多个CLI中重用公共参数定义通常比在普通命令式参数解析中更容易。
因此,我开发了一个小型库typed_argparse,它允许引入类型化参数,而不需要太多的重构,其思想是添加一个从特殊的TypedArg类派生的类型,然后简单地 Package 普通的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。

ev7lccsx

ev7lccsx5#

大多数的答案都涉及到使用另一个包来处理输入。只有在没有像我将要提出的这样简单的解决方案时,这才是一个好主意。

步骤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_argsMyProgramArgs类型,因此它确切地知道哪些字段可用以及它们的类型是什么。

zzwlnbp8

zzwlnbp86#

另一种方法,如果你有很少的参数,这可能是理想的,如下所示。
首先创建一个设置解析器并返回名称空间的函数。例如:

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())

从main开始,静态类型检查器将正确识别变量ab,尽管不再有包含所有参数的对象,这可能是好事也可能是坏事,具体取决于您的用例。

mf98qq94

mf98qq947#

一个super的解决方案只提示parse_args方法的NameSpace返回值。

import argparse
from typing import Type

class NameSpace(argparse.Namespace, Type):
    name: str

class CustomParser(argparse.ArgumentParser):
    def parse_args(self) -> NameSpace:
        return super().parse_args()

parser = CustomParser()

parser.add_argument("--name")

if __name__ == "__main__":
    args = parser.parse_args()
    print(args.name)

相关问题