web-dev-qa-db-ja.com

Python:argparse.Namespaceオブジェクトのタイプヒント

Python静的アナライザー(たとえば、PyCharm、他のIDE))がargparse.NamespaceオブジェクトのTypehintsを取得する方法はありますか?例:

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モジュールで何も見つけることができませんでしたが、anyかどうかまだわかりません静的分析ツールは、これらの値を取得するのに十分賢く、IDEを粉砕停止させないでください。

まだ探している...


アップデート2

Hpauljのコメントによれば、パースされたオブジェクトの属性を「魔法のように」抽出する上記の方法に最も近いものは、パーサーの_actionsから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

ただし、これでも静的分析で属性エラーのフラグが立てられることはありません。これは、namespace=MagicNamespace呼び出しでparser.parse_argsを渡した場合にも当てはまります。

29
Billy

型付き引数パーサー はまさにこの目的のために作成されました。 argparseをラップします。あなたの例は次のように実装されます:

from tap import Tap


class ArgumentParser(Tap):
    somearg: str


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

これが動作中の写真です。 enter image description here

完全な開示:私はこのライブラリの作成者の1人です。

0
Jesse

必要なタイプヒントを提供する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
11
aghast

PyCharmがこれらのタイプヒントをどのように処理するかについては何も知りませんが、Namespaceコードを理解しています。

argparse.Namespaceは単純なクラスです。基本的に、属性の表示を容易にするいくつかのメソッドを持つオブジェクト。ユニットテストを簡単にするために、__eq__メソッドがあります。 argparse.pyファイルで定義を読み取ることができます。

parserは、最も一般的な方法で名前空間と対話します-getattrsetattrhasattr。したがって、.dest構文ではアクセスできないものも含め、ほとんどすべてのdest文字列を使用できます。

add_argumenttype=パラメータを混同しないように注意してください。それは関数です。

他の回答で提案されているように、独自の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

それは型のヒントを邪魔するかもしれません。どのような解決策を試しても、デフォルトに注意してください。


namedtupleNamespaceとしては機能しません。

まず、カスタム名前空間クラスの適切な使用法は次のとおりです。

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

名前空間はgetattrおよびsetattrと連携する必要があります。

namedtupleのもう1つの問題は、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クラスとそこからパーサーの両方を作成することだと思います。

ddが必要なキーを持つ辞書であるとしましょう。次に、次のように引数を作成できます。

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

あなたや他の誰かが、そのような辞書からdefault(簡単)と静的型(難しい?)を設定する名前空間クラス定義を考え出す必要があります。

4
hpaulj