Первоначально опубликовано на Мой блог Анкет
Это продолжение Мне не нужны типы статья.
Я оставил тизер, объясняя, что я приведу конкретные примеры, используя проект 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
, манифест клонирован внутри
Анкет
Таким образом, содержимое 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, оставаясь, делая относительно несколько ложных срабатываний, все еще находили несоответствия, несколько ошибок и даже проблемы дизайна.
Итак, мой совет для того, чтобы вы использовали его, если у вас есть шанс. Ваше здоровье!
Спасибо, что прочитали это далеко:)
Я хотел бы услышать, что вы скажете, поэтому, пожалуйста, оставьте комментарий ниже или прочитайте Страница обратной связи Для большего количества способов связаться со мной.
Если вы хотите узнать больше, не стесняйтесь просматривать Документация , или прочитайте ВВЕДЕНИЕ ПОСТ . ↩
Сухой выступает за «Не повторяйся» ↩
Оригинал: “https://dev.to/dmerejkowsky/giving-mypy-a-go-1be0”