Python: Typehints для объектов argparse.Namespace

Есть ли способ, чтобы статические анализаторы 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.

Любые идеи?


Update

Вдохновленный ответом Остина ниже, самым простым решением, которое я смог найти, является использование 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 из каждого из парсеров _action s.

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.

Ответ 1

Рассмотрим определение класса расширения для 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

Ответ 2

Я ничего не знаю о том, как PyCharm обрабатывает эти типы, но понимает код Namespace.

argparse.Namespace - простой класс; по существу, объект с несколькими методами, которые облегчают просмотр атрибутов. И для простоты unittesting он имеет метод __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

Это может повлиять на тип подсказок. Независимо от того, какое решение вы попробуете, обратите внимание на значения по умолчанию.


A namedtuple не будет работать как 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

Пространство имен должно работать с getattr и 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]

Действия dest - это нормальное имя атрибута. Но type не является ожидаемым статическим типом этого атрибута. Это функция, которая может или не может преобразовать входную строку. Здесь None означает, что входная строка сохраняется как есть.

Поскольку статическая типизация и argparse требуют различной информации, нет простого способа генерировать один из другого.

Я думаю, что самое лучшее, что вы можете сделать, это создать свою собственную базу данных параметров, возможно, в словаре, и создать для них как класс пространства имен, так и парсескр с помощью собственных служебных функций.

Скажем, dd - словарь с необходимыми ключами. Затем мы можем создать аргумент с помощью:

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

Вам или кому-то придется придумать определение класса пространства имен, которое устанавливает default (простой) и статический тип (жесткий?) из такого словаря.

Ответ 3

Типизированный аргумент парсера был создан именно для этой цели. Обертывание argparse. Ваш пример реализован как:

from tap import Tap


class ArgumentParser(Tap):
    somearg: str


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

Вот фотография этого в действии. enter image description here

Полное раскрытие: я один из создателей этой библиотеки.