Рубрики
Без рубрики

Сделай мипи

Первоначально опубликовано в моем блоге. Вступление Это продолжение того, что мне не нужно … Tagged с Python, Mypy, типы.

Первоначально опубликовано на Мой блог Анкет

Это продолжение Мне не нужны типы статья.

Я оставил тизер, объясняя, что я приведу конкретные примеры, используя проект Python, так что вот.

Как вы уже догадались, я собираюсь поговорить об аннотациях типа и mypy Статический тип Шекер.

Вот краткий обзор того, как работает Mypy, если вы еще не знакомы с этим.

Давайте воспользуемся довольно надуманным примером:

def is_odd(num):
    return num % 2 == 0

if is_odd("this sentence contains %s"):
    print("ok")

У нас есть функция is_odd , что, очевидно, работает только с номерами, и мы называем это строкой.

Но так как строка содержит %s Python с радостью предположит, что мы пытаемся его отформатировать, и ошибка останется незамеченной. is_odd просто вернет ложь, потому что мы Также разрешено сравнивать строки и числа с == в Python.

Без аннотаций типа Mypy ничего не обнаруживает:

$ mypy foo.py


Но если мы добавим аннотации типа и повторно запустите Mypy, мы получим сообщение об ошибке:

def is_odd(num: int) -> bool:
    return num % 2 == 0

if is_odd("this sentence contains %s"):
    print("ok")
$ mypy foo.py
error: Argument 1 to "is_odd" has incompatible type "str"; expected "int"

Таким образом, вы можете использовать Mypy в цикле:

  • Начните с аннотирования нескольких функций.
  • Запустите Mypy на своем исходном коде.
  • Если он обнаруживает некоторые ошибки, либо исправьте аннотации, либо код.
  • Вернуться к первым шагом

Это называется «постепенное набор».

Цель эксперимента состоит в том, чтобы проверить, стоит ли пройти проблему с добавлением аннотаций типа.

Некоторые определения

Позвольте мне определить, что я имею в виду под «стоимостью того».

Есть много вещей об аннотациях типов, таких как:

  • Это облегчает внешним участникам понять код
  • Это помогает во время рефакторинга
  • Это облегчает обслуживание крупных проектов

Это может быть правдой, но их трудно измерить.

Итак, я спросил себя кое -что еще:

  • Один из них: даже со всем остальным (Линтеры, тесты, отзывы …), были ли ошибки, которые поймали бы только аннотации типа?
  • Два: были ли изменения, необходимые для того, чтобы запустить Mypy без ошибок, улучшающих качество кода?

Я знаю, что второй вопрос немного субъективен. Я все еще собираюсь использовать эту метрику, потому что это действительно для меня.

Выбор проекта

Чтобы проверить эту гипотезу, мне нужен был существующий проект. Я использовал TSRC , инструмент командной строки, который мы используем на работе Управлять несколькими репозиториями GIT и автоматизировать автоматизацию обзора GitHub и Gitlab 1 Анкет

Я выбрал это, потому что:

  • Это не слишком большой, поэтому эксперимент не займет слишком много времени.
  • Это не слишком мало, поэтому у нас будет достаточно экспериментальных данных.

Протокол

Итак, вот протокол, который я использовал:

  • Пройдите через постепенное петлю для печати, пока все не будет аннулировано типом.
  • Запишите каждый нетривиальный патч. (Это все, что не просто добавление аннотаций)
  • Когда петля будет закончен, посмотрите на каждый патч и спросите, была ли обнаружена ошибка, и улучшил ли она качество кода.

Прежде чем продолжить, я должен сказать вам, что я использовал Mypy с тремя важными вариантами:

  • -Strict : что означает, что Mypy издает ошибку для каждый Отсутствующие аннотации.
  • -ИГИНЕР-пропуски-импорты : TSRC зависит от библиотек, для которых не существует заглушки типа.
  • -Strict-Optional : Если у вас есть строка, которая не может, вы должны использовать Необязательно [Str] вместо Просто str Анкет Я выбрал это, потому что:

    • Ошибки относительно никого довольно часты
    • -Strict-Optional Будет по умолчанию в будущем выпуске Mypy.

Сначала посмотрим на тривиальные патчи:

Правдивая строка

def find_workspace_path() -> Path:
    head = os.getcwd()
     tail = True
     while tail:
         tsrc_path = os.path.join(head, ".tsrc")
         if os.path.isdir(tsrc_path):
             return Path(head)
         else:
            head, tail = os.path.split(head)
     raise tsrc.Error("Could not find current workspace")

Эта функция начинается с проверки, есть .tsrc Скрытый каталог в рабочем пути. Если нет, он проходит через каждый родительский каталог и проверяет, если .tsrc каталог здесь. Если он достигает корня файловой системы (второе значение OS.Path.split нет), это поднимает исключение.

Mypy не понравилась это хвост Начался как логический, а затем был назначен на строку.

Мы можем исправить это, используя непустую строку со значением, которое раскрывает намерение:

- tail = True
+ tail = "a truthy string"
     while tail:

Другой способ – использовать такой тип, как Союз [Bool, Str] Но это было бы более запутанно, я думаю.

В любом случае, я не уверен, что качество кода улучшилось там. Нет смысла для Mypy.

save_config

class Options:
    def __init__ (self, url, shallow=False) -> None:
        self.shallow: bool = shallow
        self.url: str = url

def save_config(options: Options):
    config = dict()
    config["url"] = options.url
    config["shallow"] = options.shallow

    with self.cfg_path.open("w") as fp:
        ruamel.yaml.dump(config, fp)

Мы используем save_config Чтобы сериализовать «объект значения» (параметры

Mypy увидела первые две строки, config () , config ["url"]. Ul и неправильно выведен это конфигурация был диктом от струн до струн.

Тогда он жаловался на конфигурация ["мелкий"] Это было назначено логическому.

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

- config = dict()
+ config: Dict[str, Any] = dict()

Это немного раздражает, но тип аннотации делает его более ясным Что за конфигурация является. 1 пункт для Mypy.

Кодирование названий проектов

Когда вы используете API Gitlab, вам часто приходится использовать «идентификатор проекта». Док говорит, что вы можете использовать / строка, если это «кодировка URL», как это:

Get info about the Foo project in the FooFighters namespace:
GET /api/v4/projects/FooFighters%2Foo

На Python наивный подход не работает:

import urllib.parse

>>> urllib.parse.quote("FooFighters/Foo")
FooFighters/Foo

Вместо этого вы должны указать список «безопасных» символов, то есть символы, которые вы Не нужно кодировать.

Док говорит, что значение по умолчанию составляет “/”.

Таким образом, вот что мы используем в tsrc.gitlab :

project_id = "%s/%s" % (namespace, project)
encoded_project_id = urllib.parse.quote(project_name, safe=list())

Mypy увидела, что значение по умолчанию было строкой, поэтому он пожаловался, что мы использовали список вместо этого. Мы исправили это:

- encoded_project_name = urllib.parse.quote(project_name, safe=list())
+ encoded_project_name = urllib.parse.quote(project_name, safe="")

Здесь Mypy заставила нас следовать неявной конвенции. Есть два способа представить список символов в Python. Реальный список: ['a', 'b', 'c'] , или строка: "ABC" Анкет Авторы urllib.quote () Функция решила использовать вторую форму, так что мы тоже следуем этой конвенции.

Другая победа для Mypy.

Обнаружены ошибки

Мы уже используем много инструментов в надежде поймать ошибки:

  • Каждый запрос на тягу был рассмотрен другими людьми
  • Два статических анализатора (Pylint и Pyflakes) были запускаются для каждого запроса на привлечение
  • Сложность McCabe была измерена для каждой функции и метода кодовой базы, и ей не разрешалось превышать 10.
  • TDD использовался на протяжении всей разработки проекта
  • Тестовое покрытие уже было на 80%

Несмотря на это, Mypy обнаружила ошибки, которые тестируют, все линтерс и все рецензенты не нашли.

Вот несколько из них. Не стесняйтесь пытаться найти ошибку самостоятельно, ответ будет дан после ⁂ символа.

harder_stream_errors

class GitLabAPIError(GitLabError):
    def __init__ (self, url: str, status_code: int, message: str) -> None:
        ...

def handle_stream_errors(response: requests.models.Response) -> None:
     if response.status_code >= 400:
        raise GitLabAPIError(
            response.url, "Incorrect status code:", response.status_code)

Этот код завершает вызовы в API Gitlab и организует конкретный Gitlabapierror Чтобы быть поднятым, когда мы делаем запрос «потока». (Например, для загрузки артефакта Gitlab CI):

Проблема здесь в том, что мы перевернули status_code и Сообщение параметр. Легко исправить:

- raise GitLabAPIError(
- response.url, "Incorrect status code:", response.status_code)
+ raise GitLabAPIError(
+ response.url, response.status_code, "Incorrect status code")

Ошибка не была поймана, потому что рассматриваемый код был фактически копировать/вставлен из сценария CI (и вы обычно не пишете тесты для сценариев CI).

Нам на самом деле нам не нужны потоковые ответы нигде в TSRC, так что это на самом деле мертвый код (и теперь он ушел из базы кода)

harder_json_errors

def handle_json_errors(response: requests.models.Response):

    try:
         json_details = response.json()
     except ValueError:
     json_details["error"] = (
         "Expecting json result, got %s" % response.text
     )

    ...

    url = response.url
    if 400 <= status_code < 500:
        for key in ["error", "message"]:
            if key in json_details:
                raise GitLabAPIError(url, status_code, json_details[key])
         raise GitLabAPIError(url, status_code, json_details)
    if status_code >= 500:
        raise GitLabAPIError(url, status_code, response.text)

Это немного интереснее. Он расположен недалеко от предыдущего и обрабатывает ошибки для вызовов в Gitlab API, который возвращает объекты JSON.

Нам, конечно, придется поймать 500 ошибок, которые, надеюсь, не часто случаются.

В случае кода состояния между 400 и 499 мы знаем, что в запросе была проблема, но мы должны сообщить пользователю, почему запрос был отклонен.

Большую часть времени Gitlab API возвращает объект JSON, содержащий Ошибка или Сообщение Ключ, но иногда ни один из ключей не встречается в возвращенном объекте, и иногда текст ответа даже не является действительным кодом JSON.

Таким образом, мы должны проверить оба ключа в объекте JSON, и если не найдено (если мы выйдем из петли), просто сохраните весь ответ JSON в исключении.

Ошибка во втором Поднимите gitlabapierror . Мы передаем весь объект, где Gitlabapierror Ожидается строка.

Исправление было:

- raise GitLabAPIError(url, status_code, json_details)
+ raise GitLabAPIError(url, status_code, json.dumps(json_details, indent=2))

Опять же, это было трудно поймать с тестами. Дело, когда JSON вернулся Gitlab, сделал не содержать Ошибка или Сообщение Ключ произошел только один раз в срок службы проекта (который объясняет, почему код был написан), поэтому ручные QA и модульные тесты не нужно было проверять этот путь кода.

В любом случае, обратите внимание, что мы не слепо написали что -то вроде str (json_details) Чтобы преобразовать объект JSON в строку. Мы обнаружили, что он использовался в сообщении, отображаемом для конечного пользователя, поэтому мы используем json.dumps (json_details),) Чтобы убедиться, что сообщение содержит аккуратно отступающий JSON и легко читается.

Localmanifest

class LocalManifest:
    def __init__ (self, workspace_path: Path) -> None:
        hidden_path = workspace_path.joinpath(".tsrc")
        self.clone_path = hidden_path.joinpath("manifest")
        self.cfg_path = hidden_path.joinpath("manifest.yml")
        self.manifest: Optional[tsrc.manifest.Manifest] = None

    def update(self) -> None:
        ui.info_2("Updating manifest")
        tsrc.git.run_git(self.clone_path, "fetch")
        tsrc.git.run_git(self.clone_path, "reset", "--hard", branch)

    def load(self) -> None:
        yml_path = self.clone_path.joinpath("manifest.yml")
        if not yml_path.exists():
            message = "No manifest found in {}. Did you run `tsrc init` ?"
            raise tsrc.Error(message.format(yml_path))
        self.manifest = tsrc.manifest.load(yml_path)

    @property
    def copyfiles(self) -> List[Tuple[str, str]]:
        return self.manifest.copyfiles

    def get_repos(self) -> List[tsrc.Repo]:
        assert self.manifest, "manifest is empty. Did you call load()?"
        return self.manifest.get_repos(groups=self.active_groups)

После того, как вы запустите tsrc init git@example.com/manifest.git , манифест клонирован внутри /. tsrc/manifest Анкет

Таким образом, содержимое manifest.yml в /. tsrc/manifest/manifest.yml> Анкет

Localmanifest Класс представляет этот манифестный репозиторий.

Вот что происходит, когда вы бежите TSRC Sync :

  • local_manifest.update () называется: хранилище в /. tsrc/manifest> обновляется запуска git fetch; git reset -hard Origin/Master
  • local_manifest.load () называется: manifest.yml Файл анализируется, и его содержание хранится в self.manifest атрибут.
  • Тогда Синсер Классовые звонки local_manifest.get_repos () Чтобы узнать список репозиториев для клона или синхронизации.
  • Наконец, Filecopier Класс использует local_manifest.copyfiles Для выполнения файлов копий.

Это не на самом деле ошибка, но Mypy заставила нас признать, что Localmanifest.manifest начинается как нет, и получает свою реальную ценность только после .load () был вызван.

У нас уже есть утверждение на месте в get_repos () , но Mypy заставила нас добавить аналогичную проверку в копирование Getter:

    @property
    def copyfiles(self) -> List[Tuple[str, str]]:
+       assert self.manifest, "manifest is empty. Did you call load()?"
        return self.manifest.copyfiles

Гитсервер

Некоторые из тестов для TSRC-это то, что мы называем сквозными тестами:

Вот как они работают:

  • Мы создаем группу нового временного каталога для каждого теста
  • В нем мы создаем SRV Справочник с репозиториями Bare GIT
  • Затем мы создаем Работа каталог И мы запускаем команды TSRC оттуда.

Таким образом, нам не нужно издеваться над файловыми системами или командами GIT (что выполнимо, но довольно сложно), и все легко отладить, потому что в случае проблемы мы можем просто CD в справочный справочник и осмотрите состояние репозиториев GIT вручную.

В любом случае, эти тесты написаны с тестовым помощником под названием Gitserver Анкет

Вы можете использовать этот класс для создания репозиториев GIT, нажимать файлы на некоторые филиалы и т. Д.

Вот как выглядит помощник:

    def add_repo(self, name: str, default_branch="master") -> str:
        ...
        url = self.get_url(name)
        return url

    def push_file(self, name: str, file_path: str, *,
                  contents="", message="") -> None:
        ...
        full_path = ...
        full_path.parent.makedirs_p()
        full_path.touch()
        if contents:
            full_path.write_text(contents)
        commit_message = message or ("Create/Update %s" % file_path)
        run_git("add", file_path)
        run_git("commit", "--message", commit_message)
        run_git("push", "origin", "--set-upstream", branch)

    def tag(self, name: str, tag_name: str) -> None:
        run_git("tag", tag_name)
        run_git("push", "origin", tag_name)

    def get_tags(self, name: str) -> List[str]:
         src_path = self.get_path(name)
         rc, out = tsrc.git.run_git_captured(src_path, "tag", "--list")
         return out

Вы можете использовать это так:

def test_tsrc_sync(tsrc_cli: CLI, git_server: GitServer) -> None:
    git_server.add_repo("foo/bar")
    git_server.add_repo("spam/eggs")
    manifest_url = git_server.manifest_url
    tsrc_cli.run("init", manifest_url)
    git_server.push_file("foo/bar", "bar.txt", contents="this is bar")

    tsrc_cli.run("sync")

    bar_txt_path = workspace_path.joinpath("foo", "bar", "bar.txt")
    assert bar_txt_path.text() == "this is bar"

Ошибка в get_tags :

    def get_tags(self):
         rc, out = tsrc.git.run_git(src_path, "tag", raises=False)
-        return out
+        return out.splitlines()

get_tags Методы также являются мертвым кодом. У этого есть интересная история.

Необходимость в Gitserver Помощник произошел, когда я начал писать сквозные тесты для TSRC и обнаружил, что мне нужна тестовая структура, чтобы написать эти сквозные тесты.

Было жизненно важно, чтобы Гитсервер Реализация использует чистый код.

  • Один, мы должны быть уверены Gitserver нет ошибок. В противном случае, мы можем застрять в поисках ошибок в нашем производственном коде, когда тесты не стержат неудачу.
  • По той же причине, Gitserver должно быть легко изменить. В TSRC почти наверняка будут добавлены функции, которые потребуют адаптации Gitserver код, чтобы правильно проверить его.

Так что иметь чистую реализацию Гитсервер Я, конечно, использовал лучшую технику, о которой я знаю: TDD.

Вы можете найти их в test_test_helpers.py файл.

Во всяком случае, я писал сквозное тест для TSRC, в котором участвовали теги.

Я подумал: «Хорошо, мне нужен .tag () Метод в Гитсервер . Так что мне также нужен get_tags () Метод проверки тега был фактически натолкнут ». Так я написал get_tags Метод, забыв написать неудачный тест для Gitserver Во -первых (это все еще происходит после 5 лет TDD, так что не волнуйтесь, если это случится с вами.). В этот момент у меня было только сквозное тест, поэтому я заставил его пройти и полностью забыл о get_tags () метод Ну что ж.

Исполнители и задачи

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

Например:

tsrc sync
* (1/2) foo/bar
remote: Counting objects: 3, done.
...
Updating 62a5d28..39eb3bd
error: The following untracked files would be overwritten by merge:
    bar.txt
Please move or remove them before you merge.
Aborting
* (2/2) spam/eggs
Already up to date.
Error: Synchronize workspace failed
* foo/bar: updating branch failed

Или:

$tsrc foreach
:: Running `ls foo` on every repo
* (1/2) foo
$ ls foo
bar.txt
* (2/2) spam
$ ls foo
ls: cannot access 'foo': No such file or directory
Error: Running `ls foo` on every repo failed
* spam

Итак, чтобы сохранить вещи сухими 2 , у нас есть код высокого уровня, который касается только обработки цикла и ошибок:

Во -первых, общий Задача метод

(Обратите внимание, что вы можете иметь свои собственные универсальные типы с Mypy. Это Потрясающе Потому что без этого вы смеются от программистов C ++)

class Task(Generic[T], metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def description(self) -> str:
        pass

    @abc.abstractmethod
    def display_item(self, item: T) -> str:
        pass

    @abc.abstractmethod
    def process(self, item: T) -> None:
        pass

А потом, общий Sequentialexecutor Кто знает, как выполнить данную задачу в списке элементов и отобразить результат:

class SequentialExecutor(Generic[T]):
    def __init__ (self, task: Task[T]) -> None:
        self.task = task
        self.errors: List[Tuple[Any, tsrc.Error]] = list()

    def process(self, items: List[T]) -> None:
        if not items:
            return True
        ui.info_1(self.task.description())

        self.errors = list()
        for item in enumerate(items):
            ...
            self.process_one(item)

        if self.errors:
            self.handle_errors()

    def process_one(self, item: T) -> None:
        try:
            self.task.process(item)
        except tsrc.Error as error:
            self.errors.append((item, error))

    def handle_errors(self) -> None:
        # Display nice error message
        ...
        raise ExecutorFailed()

Таким образом, мы можем унаследовать от Задача для реализации TSRC Sync :

class Syncer(tsrc.executor.Task[Repo]):

    def process(self, repo: Repo) -> None:
        ui.info(repo.src)
        self.fetch(repo)
        self.check_branch(repo)
        self.sync_repo_to_branch(repo)

    def check_branch(self, repo: Repo):
        current_branch = tsrc.git.get_current_branch(repo_path)
        if not current_branch:
            raise tsrc.Error("Not on any branch")

То же самое для TSRC Foreach :

class CmdRunner(tsrc.executor.Task[Repo]):

    def process(self, repo: Repo) -> None:
        full_path = self.workspace.joinpath(repo.src)
        rc = subprocess.call(cmd, ...)
        if rc != 0:
            raise CommandFailed

Вы заметили ошибку?

Ошибка здесь:

    def process(self, items: List[T]) -> None:
         if not items:
             return True

Это результат плохого рефакторинга. Исполнитель используется для отслеживания успеха задачи, просмотрев возвращаемое значение Process () метод

Через некоторое время я узнал, что было бы понятно просто использовать исключения для этого, в основном, когда я реализовал Синсер класс. (Вы можете просто поднять исключение вместо того, чтобы добавить много ifs )

Но ранний вернуть истинность был оставлен. Здесь mypy Нашел что -то, что озадачило бы внешнего читателя. Почти все функционирует или метод, связанный с исполнителями и задачами в TSRC, либо возвращают нет, либо повышают исключение. Что это делает здесь логический?

Конечно, не осталось тестов, которые проверили возвратное значение процесс (Они рефактировали в то же время остальной части кода), поэтому ошибка осталась незамеченной.

run_git

Последнее изменение, требуемое Mypy, также было довольно интересным. Вот сделка.

В TSRC нам часто нужно запустить git Команды, но мы можем запустить их двумя совершенно разными способами:

  • Мы можем либо просто запустить команду и позволить ее выводу непосредственно отображаться пользователю. Это полезно для таких вещей, как git fetch который может занять несколько раз, и для которого выход содержит индикацию прогресса для пользователя.
  • Или нам просто нужно запустить команду, захватить ее вывод и разобраться с ней как значение. Например, если мы хотим получить URL -адрес «Происхождения» удаленного, мы можем позвонить git remote get-url Origin Анкет
  • В обоих случаях мы должны справиться с возможностью того, что команда может потерпеть неудачу.

Вот как выглядела реализация:

def run_git(working_path, *cmd, raises=True):
    """ Run git `cmd` in given `working_path`

    If `raises` is True and git return code is non zero, raise
    an exception. Otherwise, return a tuple (returncode, out)

    """
    git_cmd = list(cmd)
    git_cmd.insert(0, "git")
    options = dict()
    if not raises:
        options["stdout"] = subprocess.PIPE
        options["stderr"] = subprocess.STDOUT

    process = subprocess.Popen(git_cmd, cwd=working_path, **options)

    if raises:
        process.wait()
    else:
        out, _ = process.communicate()
        out = out.decode("utf-8")

    returncode = process.returncode
    if raises:
        if returncode != 0:
            raise GitCommandError(working_path, cmd)
    else:
        return returncode, out

И вот как его использовать:

# run `git fetch`, leaving the output as-is
run_git(foo_path, "fetch")

# run git remote get-url origin
rc, out = run_git(foo_path, "remote", "get-url", "origin", raises=False):
if rc == 0:
    # Handle the case where there is no remote called 'origin'
    ...
else:
    # Do something with out

Вот другое место, которое мы использовали run_git : У нас есть команда по имени Статус TSRC , который отображает сводку всего рабочего пространства. Вот как выглядит его вывод:

tsrc status
* foo master ↓1 commit
* bar devel ↑1 commit

И вот реализация:

class GitStatus:

    def update_remote_status(self):
        _, ahead_rev = run_git(
            self.working_path,
            "rev-list", "@{upstream}..HEAD",
            raises=False
        )
        self.ahead = len(ahead_rev.splitlines())

        _, behind_rev = run_git(
            self.working_path,
            "rev-list", "HEAD..@{upstream}",
            raises=False
        )
        self.behind = len(behind_rev.splitlines())

Уже нашли ошибку?

Код в Update_Remote_status () предполагает, что git Rev-list выводит список коммитов, а затем подсчитайте строки в выводе.

Это работает хорошо, если там это удаленная ветвь настроена:

$ git revlist HEAD..@{upstream}
30f729a6a0ec3926cf063f5f8a3953b89d7b252e
ef564f0ef38a163beb3db52474ac4e256a6c2cd4

Но если не настройка удаленного, команда GIT не удастся с похожим сообщением:

$ git revlist HEAD..@{upstream}
fatal: no upstream configured for branch 'dm/foo'

С тех пор Update_Remote_status () не проверяет код возврата, self.ahead_rev и self.behind_rev оба устанавливаются в 1 и вывод выглядит как:

* foo other-branch ↑1↓1 commit

Упс.

Но это больше, чем просто эта ошибка.

Обратите внимание на тип возвращаемого значения run_git зависит по значению Повышение параметр .

Сначала я попытался аннотировать функцию с помощью Союз Тип, как это:

def run_git(working_path: Path, *cmd: str, raises=True) ->
      Union[Tuple[int, str], None]:
    pass

Но тогда Мипи жаловалась на каждую строку, которая использовалась run_git с Повышение = Ложный :

rc, out = run_git(..., raises=False)

foo.py:39: error: 'builtins.None' object is not iterable

Я подумал об этом и узнал, что это было чище, чтобы разделить run_git в run_git и run_git_captured :

def run_git(working_path: Path, *cmd: str) -> None:
    """ Run git `cmd` in given `working_path`

    Raise GitCommandError if return code is non-zero.
    """
    git_cmd = list(cmd)
    git_cmd.insert(0, "git")

    returncode = subprocess.call(git_cmd, cwd=working_path)
    if returncode != 0:
        raise GitCommandError(working_path, cmd)

def run_git_captured(working_path: Path, *cmd: str,
                     check: bool = True) -> Tuple[int, str]:
    """ Run git `cmd` in given `working_path`, capturing the output

    Return a tuple (returncode, output).

    Raise GitCommandError if return code is non-zero and check is True
    """
    git_cmd = list(cmd)
    git_cmd.insert(0, "git")
    options: Dict[str, Any] = dict()
    options["stdout"] = subprocess.PIPE
    options["stderr"] = subprocess.STDOUT

    returncode = process.returncode
    if check and returncode != 0:
        raise GitCommandError(working_path, cmd, output=out)
    return returncode, out

Правда, есть немного больше кода и слегка дублирования, но:

  • Множественные Если поднимает В первоначальной реализации исчез. Меньше Если В функции всегда победа.
  • Повышение Параметр сделал слишком разные вещи:

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

Теперь намерение о том, хотите ли вы захватить выход или нет, кодируется во имя функции ( run_git_captured вместо run_git ).

Также обратите внимание, что вы больше не можете забыть о проверке кода возврата.

Вы можете либо написать:

_, out = run_git_captured(repo_path, "some-cmd")
# We know `out` is *not* an error message, because if `some-cmd` failed, an
# exception would have been raised before `out` was set.

Или вы можете быть явными в отношении обработки ошибок:

rc, out = run_git_captured(repo_path, "some-cmd", check=False)
if rc == 0:
    # Ditto, out can't be an error message
else:
    # Need to handle error here

Одно это было бы хорошей причиной для использования Mypy, я думаю:)

Ну, mypy Изменения имеют был объединен и CI теперь работает Mypy в строгом режиме по каждому запросу на привлечение.

Мне все еще любопытно о других преимуществах аннотаций типов, которые я не мог проверить (обслуживание, понимание кода, простота рефакторинга…).

Я думаю, мы посмотрим, как идет следующий рефакторинг в TSRC.

Мы увидели, как Mypy, оставаясь, делая относительно несколько ложных срабатываний, все еще находили несоответствия, несколько ошибок и даже проблемы дизайна.

Итак, мой совет для того, чтобы вы использовали его, если у вас есть шанс. Ваше здоровье!

Спасибо, что прочитали это далеко:)

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

  1. Если вы хотите узнать больше, не стесняйтесь просматривать Документация , или прочитайте ВВЕДЕНИЕ ПОСТ . ↩

  2. Сухой выступает за «Не повторяйся» ↩

Оригинал: “https://dev.to/dmerejkowsky/giving-mypy-a-go-1be0”