Как добавить чеки для определенного типа - PullRequest
2 голосов
/ 20 июня 2019

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

Например, яможет иметь ColorValue = NewType('ColorValue', int), где ColorValue должен иметь диапазон от 0 до 255.

Затем я хотел бы, чтобы типограф проверял меня, если я на самом деле не совпадаютехнические характеристики типа.Например, что-то вроде:

red: ColorValue = 300 # value of 300 is not compatible with type ColorValue

В идеале, я хотел бы иметь возможность установить что-то подобное с помощью

ColorValue = NewType('ColorValue', int, check=lambda value: 0 <= value <= 255)

Есть ли способ проверки типа проверкиконкретные свойства?

РЕДАКТИРОВАТЬ:

Для ясности, Я бы хотел, чтобы эта проверка выполнялась с помощью проверки типов, таких как mypy или pytype, и я не хочу, чтобы ошибкапроисходит только во время выполнения.

1 Ответ

0 голосов
/ 20 июня 2019

Рассмотрим следующий модуль с именем "restrict.py"

def restrict(cls, cache=[]):  
    cache.append(cls)
    return cls

def static_check(file, restrict):
    import re        
    cache = restrict.__defaults__[0]   
    with open(file) as f:
        lines = f.readlines()
    for cls in cache:
        c = cls.__name__
        for lix, l in enumerate(lines):
            m = re.findall(f"{c}[^=)]*\)", l)
            for m in m:
                try:
                    print("Attempting", m)
                    strargs = m.split(c)[1]
                    cmd = f"cls{strargs}"
                    eval(cmd)
                    print(m, "in line", lix, "evaluated")
                except ValueError as e:
                    print(m, "in line", lix,"threw",e)

и другой модуль с именем main.py, который вы хотите проверить

from restrict import restrict, static_check

@restrict
class Restricted():
    def __new__(cls, x:int=None) -> int:
        if x is None:
            raise ValueError("Unspecified initialization")
        elif x < 0:
            raise(ValueError("<0"))
        elif x > 255:
            raise(ValueError(">255"))
        return int(x)

def output_foo(x):
    Restricted(-1)
    return Restricted(999)

Restricted(1)

if __name__ == "__main__":
    static_check(__file__, restrict)   

, работающий python main.py изтерминал напечатает вас

Attempting Restricted()
Restricted() in line 5 threw Unspecified initialization
Attempting Restricted(-1)
Restricted(-1) in line 16 threw <0
Attempting Restricted(999)
Restricted(999) in line 17 threw >255
Attempting Restricted(1)
Restricted(1) in line 19 evaluated

Отсутствие проверки static_check с помощью условия if __name__ == "__main__" позволит вам проверить во время импорта.

СТАРЫЙ ОТВЕТ

Вы можете проверить навремя анализа, например, предположим, что у вас есть файл с именем restricted.py со следующим кодом:

class Restricted():

    def __new__(cls, x):
        import sys
        lineno = sys._getframe().f_back.f_lineno
        if x < 0:
            print(f"Value {x} is too low in line {lineno}")
        if x > 255:
            print(f"Value {x} is too high in line {lineno}")
        return int(x)

def invalid_foo(x:Restricted=Restricted(300)):
    return x

def valid_foo(x:Restricted=Restricted(222)):
    return x

, который печатает Value 300 is too high in line 13, когда вы импортируете модуль / анализируете код, например, из bash с python restricted.pyв дополнение к mypy restricted.py.

По-видимому, ни mypy, ни pytype не печатали сообщение самостоятельно, поэтому кажется, что они на самом деле не импортируют модуль, а вместо этого анализируют файл напрямую.Можно объединить проверку типов и импорт в bash с tpcheck() { mypy $1 && python $1; }, а затем вы можете вызвать tpcheck restricted.py, чтобы сделать и то, и другое.

Просто заметьте: NewType на самом деле не создает новый класс.как говорит _doc__: «Во время выполнения NewType (name, tp) возвращает фиктивную функцию, которая просто возвращает свой аргумент».

Альтернативной опцией может быть автоматическое создание модульных тестов, например, с auger-python.Например, когда мы добавили следующий код в предыдущий фрагмент:

def output_foo():
    return Restricted(999)

if __name__ == '__main__':
    import auger

    with auger.magic([Restricted]):
        output_foo()

tpcheck также показал мне ошибку в output_foo, то есть Value 999 is too high in line 22.Обратите внимание, что я обнаружил ошибку в шнеке, которую мне пришлось исправить вручную (см. https://github.com/laffra/auger/issues/23).. Кроме того, mypy пожаловалась на отсутствие импорта для шнека, поэтому мне пришлось переопределить tpcheck() { mypy $1 --ignore-missing-imports && python3 $1; }.

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

...