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

Написание простой программы CLI: Python vs Go

Как я уже упоминал в предыдущем посте, в настоящее время я являюсь мастером Scrum в команде DND Digital HR Appdev …. Tagged с Python, Go, CLI.

Как я упоминал в Предыдущий пост В настоящее время я являюсь мастером Scrum из команды DND Digital HR Appdev. Одной из моих обязанностей является выполнение ежедневных встреч, где каждый член команды рассказывает о том, чем они задумали, и есть ли у них какие -либо препятствия. Поскольку мы распределенная команда, у нас есть наши подставки через аудио -звонок. Когда я впервые начал запускать подставки, я обнаружил, что когда мы делали обновления попкорна, между обновлениями всегда была неловкая пауза, потому что никто не хотел идти дальше. Я быстро решил иметь определенный порядок в стендах, но также рандомизировать порядок, чтобы все было разнообразно. Это было довольно просто реализовать в жестко -кодированном скрипте Python, который использует random.shuffle () :

import random
from datetime import date

members = ["Alice", "Bob", "Carol", "David"]
random.shuffle(members)
print(f"# {date.today()}:\n")
[print(name) for name in members]

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

$ python standup-script-py
# 2021-04-27

David
Bob
Alice
Carol

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

Несколько недель назад я начал учиться. Мне это нравится, много . GO кажется очень C-подобным, без ручных снимков управления памятью и небольшого синтаксиса проще, чем даже лексика C. В рамках моего путешествия я подумал, что было бы интересным упражнением, чтобы переписать мою небольшую программу рандомизатора в Go, со следующими дополнительными требованиями:

Это был последний результат: https://github.com/jidicula/random-standup Анкет

Он использует командный список Toml, который выглядит так:

[Subteam-1]
members = [
        "Alice",                # TOML spec allows whitespace to break arrays
        "Bob",
        "Carol",
        "David"
        ]

["Subteam 2"]                   # Keys can have whitespace in quoted strings
members = ["Erin", "Frank", "Grace", "Heidi"]

["Empty Subteam"]               # Subteam with 0 members won't be printed

["Subteam 3"]
members = [
        "Ivan",
        "Judy",
        "Mallory",
        "Niaj"
]

При обращении к выводу программа выводит:

$ random-standup example-roster.toml
# 2021-03-27
## Subteam-1
Alice
David
Bob
Carol

## Subteam 2
Grace
Heidi
Frank
Erin

## Subteam 3
Judy
Niaj
Ivan
Mallory

Я подумал, что было бы еще более интересным упражнением, чтобы попытаться написать тот же инструмент в Python, просто чтобы сравнить процесс написания инструмента CLI (и поэтому у меня есть причина написать пост блога). Реализация Python этого инструмента можно увидеть Здесь Анкет Он принимает тот же файл TOML, что и реализация GO, и вызывается таким же образом.

Структура проекта

Реализация GO очень проста в этом отношении. Действительно, go в замешательстве не имеет никаких требований относительно того, где должны быть расположены файлы и папки, несмотря на некоторые группы, которые утверждать иное Анкет Единственный настоящий код GO в моем репо – 2 .go файлы (1 для программы и 1 для ее тестов) и go.mod и go.sum Манифестные файлы. Была только одна липкая точка, с которой я столкнулся, где я изначально неправильно определил имя модуля в go.mod – Это должно соответствовать имени репозитория ( github.com/jidicula/random-standup ).

Для меня выяснить это с Python было нелегко, особенно при использовании pyproject.toml Для спецификации зависимости . Я решил использовать Поэзия Чтобы организовать мои зависимости и настройки проекта (подробнее об этом позже), а поэзия имеет строгому команду ( поэзия новая ) для создания «рекомендованной» структуры проекта, которая выглядит несколько похоже на это:

foo-bar
├── README.rst
├── foo_bar
│   └── __init__.py
├── pyproject.toml
└── tests
    ├── __init__.py
    └── test_foo_bar.py

(Этот симпатичный выход FileTree предоставлен Tree , который также доступен через Homebrew.)

Казалось, это был формат, более ориентированный на пакет Python, предназначенный для библиотеки, импортируемой другими проектами – вероятно, для инструмента CLI. После некоторого копания я вместо этого следовал за Структура, рекомендованная Управлением по упаковке Python :

packaging_tutorial/
├── LICENSE
├── pyproject.toml
├── README.md
├── setup.cfg
├── setup.py  # optional, needed to make editable pip installs work
├── src/
│   └── example_pkg/
│       └── __init__.py
└── tests/

Ключ здесь заключается в том, что исходный код пакета находится в project_name/src/package_name/some_name.py и его тесты в project_name/tests/test_some_name.py , с беглым __init__.py В каталогах, содержащих .py файлы Потребившись на мои шаги для этого положения, я также наткнулся на Эта рекомендованная структура от путеводителя автостопщика по Python:

foo
├── LICENSE
├── README.rst
├── docs
│   ├── conf.py
│   └── index.rst
├── requirements.txt
├── sample
│   ├── __init__.py
│   ├── core.py
│   └── helpers.py
├── setup.py
└── tests
    ├── test_advanced.py
    └── test_basic.py

В целом, очень похоже на то, что я пошел, за исключением SRC/ каталог, который, кажется, мало что делает, и заменяет pyproject.toml и Poetry.lock с setup.py и Требования.txt Анкет В то время я не пытался исследовать различные варианты, так как не был уверен, сможет ли поэзия построить колеса с различными структурами проекта.

Все это, чтобы сказать, упаковка и структура файлов не были настолько очевидны с Python, как и с Go.

Упаковка и публикация

Go был смехотворно прост в этой области. Все, что нужно для листинга на pkg.go.dev является действительным go.mod файл. У сайта также есть другие Рекомендации , как стабильная версия с тегом и файл лицензии. Реестр пакетов GO не требует дополнительной аутентификации – когда вы перемещаетесь в pkg.go.dev/github.com/username/repo-name , он побуждает вас запустить автопопуляцию записи пакета:

(Вы можете добавить тег версии в конце URL, например, @v1.0.0 , чтобы автоматически выпустить вновь выпущенную версию вашего пакета.)

Есть также некоторые другие программные способы запустить добавление пакета в реестр, перечисленный Здесь Анкет

Еще раз, Python был не таким простым, как Go. Мой выбор использовать Поэзия Конечно, упростил процесс – мне просто нужно было запустить Poetry Publish -Build и следуйте по подсказкам для имени пользователя PYPI и аутентификации пароля. Это еще проще в моих конфигурации CI, так как поэзия и PYPI позволяют аутентификации на основе токенов – My Github Actions Workflow Publishing Step Похоже:

      - name: Publish to PyPI
        env:
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
        run: |
          poetry config pypi-token.pypi $PYPI_TOKEN
          poetry publish --build

Если бы я делал это в Python до того, как узнал о Go, этот процесс казался бы довольно простым. Основной проблемой, которую я сталкиваюсь с даже этим упрощенным процессом, нуждается в учетной записи PYPI. Это якобы помогает предотвратить атаки цепочки поставок, такие как путаница зависимости , где аккаунт тенит название внутреннего пакета, или опечатать популярный пакет в надежде, что разработчик с жирным покрытием слишком спешит. Тем не менее, я не уверен, что PYPI действительно делает какие -либо проверки на вредоносные пакеты – мой пакет не был проверен, насколько мне известно. С другой стороны, Go взял довольно разумный путь отложения любых проблем безопасности для кузницы (то есть Github, Gitlab, Bitbucket и т. Д.), Разделяя исходный код модуля и не имея собственного этапа аутентификации. Поскольку модули GO названы их местоположением (Forge и Imername), а также само именем пакета, это немного сложнее в тени и название компании Internal Package, опубликованное в pkg.go.dev Анкет Кроме того, опечатка, безусловно, все еще возможно.

Поэзия также обрабатывает чрезвычайно грязный ландшафт Python обработки зависимостей и виртуальных сред. Он имеет простую функциональность для настройки виртуальной среды и отделения развития от основных зависимостей, придерживаясь PEP 631 Спецификация для pyproject.toml Анкет Прежде чем появилась поэзия, большинство проектов выбирают использование Требования.txt и пип -Установка оттуда или использование setup.py , или используя другие реализации Python, такие как Anaconda. Ни один из них не может обрабатывать все 3 из: блокирование версий зависимостей, разрешение версий нескольких пакетов, имеющих одинаковую зависимость, и настройка виртуальной среды. Настройка этого проекта с использованием поэзии просто включает в себя:

$ poetry shell      # creates virtual environment
$ poetry install    # installs main and dev dependencies

Если бы я не использовал поэзию, мне, скорее всего, придется использовать Setuptools Для конфигурации упаковки – я не знаю много об этом процессе, но это кажется более сложным, просто благодаря тому, что у него больше шагов (что обычно означает более широкую поверхность для ошибок). Учебное пособие по упаковке Python впервые рекомендует использовать Pip сам для Создание распределительных архивов из проекта, основанного на setup.cfg (предпочтительнее) или setup.py (Рекомендуется против), затем используя шпагат для Загрузка артефактов сборки . Я не знаком с этим инструментом, но тот факт, что нет единого подхода, блаженного пипи, является причиной путаницы само по себе.

Распределение

Перейдите на компиляции в один нативный двоичный файл – он включает в себя все, что ему нужно для своей времени выполнения, без необходимости ссылаться на какие -либо системные библиотеки (если вы не выбрате более длительный маршрут и явно ссылались на них). Самая ясная прочность этого подхода, включенного в батареи, заключается в том, что компилятор GO позволяет вам перекрестно компилировать 44 пары ОС/Архитектуры, используя Goos и Goarch переменные среды! Это означает, что вы можете просто запустить нативный бинар на любой из этих 44 комбинаций ОС и архитектуры без каких -либо дополнительных зависимостей. Вы можете увидеть все пары ОС/Арки, запустив Go Tool Dist List . Этот подход, включающий батареи, имеет четкий недостаток. Простая программа Hello, World будет содержать:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Когда он составлен с GO 1.16.3 на MacOS 10.15.7 на процессоре Intel, The Hello, World Binary, как известно, взвешивает колоссальные 1,9 МБ. Первый выпуск реализации GO Случайный Standup около 3 МБ Анкет

Реализация Python не так проста в распределении по такому широкому разнообразию оборудования. Поскольку Python является интерпретированным языком, для выполнения исходного кода Python требуется время выполнения Python (с правильной версией!), Чтобы уже установить на хост -машине. Установка и настройка, которая может быть утомительной или иногда невозможной для встроенных систем – даже на персональных компьютерах управление несколькими версиями Python – это головная боль Анкет На фронте размера вопрос переполнения стека предполагает, что интерпретатор Python – это Примерно 1 МБ в размере Анкет В сочетании с 4 КБ размер колеса из Случайный Standup-Py v1.0.0, мы занимаем менее половины пространства биназного внедрения GO, причем 99,6% этого пространства идут на время выполнения, которое можно повторно использоваться для других программ.

Тестирование

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

tests := map[string]struct {
    teamMembers []string
    teamName    string
    want        string
}{
    "four names": {
        []string{"Alice", "Bob", "Carol", "David"},
        "Subteam 1",
        "## Subteam 1\nCarol\nBob\nAlice\nDavid\n"},
}

Затем я бы вышел через карту, настраивая жгут тестирования на каждой итерации. Внутри жгута тестирования я вызываю тестируемую функцию и проверяю, является ли результат возврата Shuffleteam (Tests ["Четыре имена"]. совпадения Тесты [“Четыре имена”]. Хочу :

for name, tt := range tests {
    t.Run(name, func(t *testing.T) {
        rand.Seed(0)
        got := shuffleTeam(tt.teamMembers, tt.teamName)
        if got != tt.want {
            t.Errorf("%s: got %s, want %s", name, got, tt.want)
        }
    })
}

Дэйв Чейни написал Отличный пост-блог о тестировании на столе где он рекомендует использовать карту для хранения тестовых случаев по 2 причинам:

  1. Ключи карты указывают имя тестового примера, поэтому вы можете легко найти, какой случай не удался.
  2. Заказ итерации карты не определен в Go , что означает, что использование карты может помочь вам вырезать условия, когда тесты проходят только в определенном порядке.

Все модульные тесты можно просто запустить с Перейти тест – Не требуется 3-й стороны инструментов. В то же время встроен еще более прохладный инструмент: тестовый охват. Роб Пайк написал о Тестовое покрытие инструментов в go Это просто блестяще в дизайне и функциях. TL; DR – это то, что вы можете запустить:

$ go test -coverprofile=coverage.out
PASS
coverage: 55.8% of statements
ok      github.com/jidicula/random-standup  0.030s
$ go tool cover -html=coverage.out

Вторая команда откроет окно браузера, отображающего исходный код программы, кодируемый по покрытию:

Если вы бежите

$ go test -covermode=count -coverprofile=count.out
PASS
coverage: 55.8% of statements
ok      github.com/jidicula/random-standup  0.010s
$ go tool cover -html=count.out

Вы получаете тепловую карту испытательного покрытия, где интенсивность цвета указывает, сколько раз линия покрывается единичным тестированием:

(Конечно, вы также можете получить текстовый вывод для покрытия.)

Python, к сожалению, не имеет встроенной встроенной тестовой поддержки (он имеет Unittest , но это немного громоздко), что привело к росту 3-й части (заметите шаблон?) Инструмент питест как фактический стандарт тестирования. Pytest довольно легко настроить тесты, хотя. Правила обнаружения теста указаны Здесь Но, по сути, Pytest будет выполнять любую функцию с тест Префикс в любом файле, который соответствует тест _*. py или *_test.py Анкет В этих тестовых функциях утверждение Заявления используются для определения и проверки тестовых случаев – если они не сработают, весь тест не удается:

def test_standup_cli():
    runner = CliRunner()
    result = runner.invoke(standup, ["example-roster.toml"])
    assert result.exit_code == 0
    assert str(date.today()) in result.output
    assert "## Subteam-1" in result.output

Этот тест вызывается запуска:

$ pytest
============================= test session starts ==============================
platform darwin -- Python 3.8.2, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: /Users/johanan/prog/random-standup-py
collected 1 item                                                               

tests/test_random_standup.py .                                           [100%]

============================== 1 passed in 0.07s ===============================

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

Получение тестового покрытия не так просто, как в ходе – хотя я не пытался это для этой программы, на Другие проекты Я использовал дополнительные услуги 3-й стороны, такие как Coiplls которые имеют 3-й стороны пакеты Для вычисления охвата.

Настройка CLI

Go имеет 2 встроенные варианты для создания интерфейса CLI: Операционные системы. Args , что является переменной в ОС Пакет, который содержит кусок строк, представляющих аргументы CLI в программе, или флаг Пакет, который предоставляет некоторые удобные утилиты для анализа флагов CLI, а также аргументов. Например, флаг. Arg (0) Отпечатает первый не FLAG-аргумент, переданный в программу. флаг также имеет Использование () Функция, которая может быть затенена на пользовательский вывод справочной помощи, который напечатан на stdout При прохождении флагов -h или -Хельп ( Использование определяется за пределами main () здесь):

func main() {
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "%s\n", usage)
    }

    flag.Parse()
    if flag.NArg() < 1 {
        flag.Usage()
        os.Exit(1)
    }

    file := flag.Arg(0)

    // rest of main()
}

Python имеет несколько встроенных вариантов для CLI: sys.argv , что аналогично для Go’s Операционные системы. Args и довольно низкоуровневый в функциональности или Argparse , который может обрабатывать флаг и анализ аргументов. Нажмите это еще один пакет из третьей стороны, который упрощает настройку CLI, используя декораторы для функции, представляющей команду CLI:

@click.command()
@click.argument("rosterfile")
def standup(rosterfile):
    """random-standup is a tool for randomizing the order of team member
    updates in a standup meeting.
    """
    print(date.today())
    with open(rosterfile, "r") as f:
        roster = f.read()

    parsed_roster = parse(roster)

@click.command () Декоратор поворачивает Standup () функционируйте в команду CLI, и @click.argument ("spistfile") дает имя необходимым входным аргументам для команды, которая вводится в сообщение справки. Справочное сообщение построено из Docstring функции и может быть вызвано с помощью -h или -Хельп Флаги:

$ standup --help
Usage: standup [OPTIONS] ROSTERFILE

  random-standup is a tool for randomizing the order of team member updates
  in a standup meeting.

Options:
  --help  Show this message and exit.

Если были добавлены дополнительные варианты с использованием @click.option () Декоратор, они также появятся в Варианты: Список в сообщении справки.

Click также предоставляет хороший интерфейс для тестирования CLI Black-Box, как мы видели ранее в тестировании.

В целом, я был очень впечатлен предложениями GO для создания простого инструмента CLI. Я структурировал эту часть, чтобы продемонстрировать, как его встроенные варианты столь же хороши или лучше, чем у Python, где вам часто приходится достигать многих сторонних пакетов, чтобы упростить структуру или получить основную функциональность. Также явно отлично преуспевает в инструментах: его основная поддержка тестирования, управления зависимостями и кросс-компиляции находится в нескольких милях от всего, что есть у Python.

Основным преимуществом использования качественной функции, встроенной в язык, является возможность уменьшить поверхность зависимости для простой программы, которая значительно упрощает обслуживание. Для моей программы GO, у меня есть только одна зависимость от 3-й части для анализа Toml ( go-toml ), которая сама зависит только от Go-Spew для красивой печати его структур данных на основе дерева. График зависимостей для реализации Python гораздо сложнее, даже если он имеет только 3 ядра (есть больше для форматирования и линирования) Зависимости 3-й стороны: Нажмите, Pytest и Toml Kit.

Самым большим недостатком, который я вижу для использования для написания простой программы CLI, является ее довольно обязательная семантика – мне все равно быстрее перейти от мысли к коду в Python, о чем свидетельствует мой первый сценарий для переворачивания списка. Однако по мере увеличения увеличения размера программы, сложности или производительности или, если вы хотите даже несколько улучшений в качестве жизни, таких как тестирование или поддержка многоплатформы, я вижу, как вытягивает Python.

Вы нашли этот пост полезным? Купи мне напиток или спонсируй меня Здесь !

Оригинал: “https://dev.to/jidicula/writing-a-simple-cli-program-python-vs-go-59kf”