Все чаще во всевозможных статьях можно встретить использование pipenv вместо virualenv или workon. Если вы до сих пор не знаете что такое pipenv то это статья Alexander VanTol Pipenv: A Guide to the New Python Packaging Tool именно для вас.
Pipenv — это набирающий популярность пакет управления виртуальным окружением для Python, который решает некоторые распространенные проблемы, связанные с типичным рабочим процессом, в котором используется pip, virtualenv и старый добрый файл requirements.txt.
Как написано на официальном сайте: Pipenv — это инструмент, который призван привнести в мир Python лучшее из всех упаковочных миров (bundler, composer, npm, cargo, yarn и т. д.).
Помимо решения некоторых распространенных проблем, он стандартизицирует и упрощает процесс разработки с помощью единого инструмента командной строки.
В этом руководстве мы рассмотрим, какие проблемы решает Pipenv и как управлять зависимостями Python с помощью Pipenv.
Для того чтобы понять преимущества Pipenv, нужно изучить текущие методы работы с пакетами и управления зависимостями в Python.
Давайте начнем с типичной ситуации в работе со сторонними пакетами. Затем мы рассмотрим пример развертывания приложения Python.
requirements.txt
Представьте, что вы работаете над проектом Python, в котором используется сторонний пакет, например такой как flask. Нам необходимо указать этот пакет в списке зависимостей, чтобы другие разработчики и автоматизированные системы могли использовать наше приложение.
Таким образом, мы решаем включить зависимость от flask в файл requirements.txt:
flask
Отлично, локально все работает, и после некоторого улучшения нашего приложения мы решили перенести его в продакшин. И вот тут все становится немного сложнее …
В приведенном выше файле requirements.txt нет указаний, какую версию flask нужно использовать. В этом случае pip install -r requirements.txt по умолчанию установит последнюю версию. Это нормально, если только в последней версии нет изменений интерфейса или поведения, которые нарушают наше приложение.
Для примера предположим, что вышла новая версия flask. Однако она не имеет обратной совместимости с версией, которую мы использовали во время разработки.
Теперь предположим, что мы развертываем свое приложение в рабочей среде и выполняете команду pip install -r requirements.txt. Pip инсталлирует последнюю, несовместимую с предыдущей версией версию flask, и именно так наше приложение выходит из строя ….
«Но эй, это работало на моей машине!» — я сам был, в такой ситуации много раз и это не очень приятное чувство.
На данный момент мы знаем, что версия flask, которую мы использовали во время разработки, работала нормально. Итак, чтобы исправить ситуацию, попытаемся быть немного более конкретным в наших требованиях в requirements.txt. Укажем конкретную версию flask. Это также называется «закреплением» (pinning a dependency) зависимости:
flask==0.12.1
«Закрепление» зависимости flask к конкретной версии гарантирует, что install -r requirements.txt установит точную версию flask, которую мы использовали во время разработки. Но так ли это на самом деле?
Имейте в виду, что у самого flask также есть зависимости (которые устанавливаются автоматически). Тем не менее, сам flask не определяет точные версии для своих зависимостей. Например, он допускает любую версию Werkzeug >= 0.14.
Опять же, ради этого примера, допустим, была выпущена новая версия Werkzeug, и предположим что эта версия вызывает ошибку в нашем приложения.
На этот раз, когда мы выполним pip install -r requirements.txt в рабочей среде, получим flask == 0.12.1, так как мы закрепили это в нашем requirements.txt. Однако, к сожалению, мы так же получили самую последнюю версию Werkzeug которая вызывает у нас ошибку. Опять неожиданная проблема в другом окружение.
Реальная проблема здесь в том, что зависимости нашего проекта не являются детерминированными. Под этим я подразумеваю, что при одинаковых входных данных (файл requirements.txt) pip не всегда создает одну и ту же среду. Получается что, в настоящее время мы не можем легко воспроизвести ту среду, которая есть у нас, на нашей локальной машине.
Типичным решением этой проблемы является использование pip freeze. Эта команда позволяет нам получить точные версии для всех сторонних библиотек, установленных в текущий момент времени, включая pip зависимостей, установленный автоматически. Таким образом, мы можем «заморозить» все зависимости в процессе разработки, чтобы обеспечить одинаковую среду в другом окружением.
Выполнение команды pip freeze приводит к «закреплению» зависимостей, которые мы можем добавить вручную в файл requirements.txt (или автоматически командой: pip freeze > requirements.txt ):
click==6.7 Flask==0.12.1 itsdangerous==0.24 Jinja2==2.10 MarkupSafe==1.0 Werkzeug==0.14.1
С помощью закрепления зависимостей мы можем быть уверены, что пакеты, установленные в нашей производственной среде, будут точно соответствовать пакетам в нашей среде разработки, чтобы ваш проект неожиданно не ломался. Это «решение», к сожалению, приводит к совершенно новому набору проблем.
Теперь, когда мы указали точные версии каждого стороннего пакета, мы несем ответственность за поддержание актуальности этих версий, даже несмотря на то, что они являются зависимыми компонентами flask. Что если в Werkzeug == 0.14.1 обнаружена дыра в безопасности, которую разработчики пакетов сразу же исправили в Werkzeug == 0.14.2 ? Теперь нам нужно обновиться до Werkzeug == 0.14.2, чтобы избежать каких-либо проблем безопасности, возникающих в более ранней версии.
Проблема в том что, мы должны как то узнать, что есть проблема с версией, которая у нас установлена. Затем нам нужно заняться установкой новой версию в нашей производственной среде самим. Но это не то что нам нужно. Правда в том, что нам действительно все равно, какая версия Werkzeug установлена у нас, если она не нарушает наш код. Мы не хотим на себя брать контроль над всеми зависимостями проекта. Нам нужно чтобы последнии версии зависимостей гарантировали, что мы автоматически получаем все необходимые исправления ошибок, исправления безопасности, новые функции, улучшенную оптимизации и так далее.
Реальный вопрос: «Как делать зависимости детерминированными для нашего проекта Python, не беря на себя ответственность за обновление версий?»
Спойлер: простой ответ — использование Pipenv.
Давайте немного переключимся, чтобы поговорить о другой распространенной проблеме, которая возникает, когда вы работаете над несколькими проектами. Представьте, что для ProjectA нужен django == 1.9, а для проекта ProjectB нужен django == 1.10.
По умолчанию Python пытается сохранить все наши сторонние пакеты в общесистемном расположении. Это означает, что каждый раз, когда мы хотим переключиться между ProjectA и ProjectB, мы должны убедиться, что установлена правильная версия django. Это делает переключение между проектами болезненным, потому что нужно удалить и переустановить пакеты, чтобы соответствовать требованиям для каждого проекта.
Стандартное решение заключается в использовании виртуальной среды, которая имеет собственный исполняемый файл Python и стороннее хранилище пакетов. Таким образом, ProjectA и ProjectB соответственно разделены. Теперь мы можем легко переключаться между проектами, поскольку они не используют одно и то же место хранения пакетов. PackageA может иметь любую версию django, которая ему нужна, в своей среде, а PackageB может иметь то, что ему нужно, совершенно отдельно. Самый распространенный инструмент для этого — virtualenv (или venv в Python 3).
В Pipenv встроено управление виртуальной средой, поэтому у нас есть единый инструмент для управления пакетами.
Что я имею в виду под разрешением зависимости? Допустим, у нас есть файл requirements.txt, который выглядит примерно так:
package_a package_b
Допустим, для package_a есть подчиненная зависимость package_c, и для package_a требуется конкретная версия этого пакета: package_c > = 1.0. В свою очередь, package_b имеет ту же зависимость, но нуждается в package_c <= 2.0.
В идеале, когда вы пытаетесь установить package_a и package_b, инструмент установки должен смотреть на требования для package_c (которое > = 1.0 и <= 2.0) и выбрать версию, которая удовлетворяет этим требованиям. Вам нужно, что бы инструмент сам находил эти зависимости. Это то, что я имею в виду под «разрешением зависимости».
К сожалению, pip на данный момент не имеет функционала разрешения зависимостей, но есть открытый issue, на эту проблему.
Как работает pip с описанным выше сценарием:
Но это и вызывает проблему. Если версия package_c, выбранная pip, не соответствует другим требованиям (например, package_b, нуждающемуся в package_c <= 2.0), установка завершится неудачно.
«Решением» этой проблемы является указание диапазона, необходимого для зависимостей (package_c) в файле requirements.txt. Таким образом, pip может разрешить этот конфликт и установить пакет, который отвечает этим требованиям:
package_c>=1.0,<=2.0 package_a package_b
Как и раньше, теперь у нас точные зависимости (package_c). Проблема заключается в том, что если package_a изменит свои требования без нашего ведома, указанные нами требования (package_c> = 1.0, <= 2.0) могут перестать быть актуальными, и установка снова может завершиться неудачей …. Проблема заключается в том, что мы снова несем ответственность за то, чтобы быть в курсе всех требований зависимостей.
В идеале, инструмент установки должен быть достаточно умен, чтобы устанавливать пакеты, которые отвечают всем требованиям, без явного указания версий зависимостей.
Теперь, когда мы определили проблемы, давайте посмотрим, как Pipenv решает их.
Начнем с установки:
$ pip install pipenv
После того, как вы это сделали, вы можете фактически забыть о pip, поскольку Pipenv по сути выступает в качестве его замены. В нем также представлены два новых файла: Pipfile (который предназначен для замены requirements.txt) и Pipfile.lock (который отвечает за детерминированность сборки).
Pipenv использует pip и virtualenv под капотом, что упрощает их использование с помощью одного интерфейса командной строки.
Давайте начнем с создания тестового приложения на Python. Начнем с создания оболочки виртуальной среде, чтобы изолировать разработку этого приложения:
$ pipenv shell
Эта команда создаст виртуальную среду. Pipenv для создания виртуальных сред использует путь по умолчанию. Если вы хотите изменить это поведение в Pipenv, для настройки есть переменные среды.
Вы можете принудительно создать среду Python 2 или 3 с аргументами —two и —three соответственно. В противном случае, Pipenv будет использовать то, что найдет по умолчанию virtualenv.
Если требуется более конкретная версия Python, вы можете использовать аргумент —python с нужной версией.
Например: —python 3.6
Теперь можно установить сторонний пакет, который нам нужен. Стоп, но мы то знаем, что нам нужна версия 0.12.1, а не последняя версия, так что давай установим именно ее:
$ pipenv install flask==0.12.1
Мы должны увидеть что-то вроде следующего в терминале:
Installing flask==0.12.1... Adding flask to Pipfile's [packages]... ✔ Installation Succeeded Pipfile.lock not found, creating... Locking [dev-packages] dependencies... Locking [packages] dependencies... ✔ Success! Updated Pipfile.lock (4af34b)! Installing dependencies from Pipfile.lock (4af34b)... 🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 6/6 — 00:00:01
Обратите внимание, что создаются два файла, Pipfile и Pipfile.lock. Мы рассмотрим их подробнее через секунду. Давайте установим еще один сторонний пакет, numpy. Нам не нужна конкретная версия, поэтому версию указывать не будем:
$ pipenv install numpy
Если вы хотите установить что-то непосредственно из системы контроля версий (VCS), то это тоже можно сделать! Нужно указать местоположения аналогично тому, как вы это делаете с помощью pip. Например, чтобы установить библиотеку requests из системы управления версиями, выполните следующую команду:
$ pipenv install -e git+https://github.com/requests/requests.git#egg=requests
Допустим, у нас также есть несколько модульных тестов, и мы хотим использовать pytest для их запуска. Вам не нужен pytest в рабочей среде, поэтому мы можем указать, что эта зависимость предназначена только для разработки с аргументом —dev:
$ pipenv install pytest --dev
Использование аргумента —dev поместит зависимость в специальную папку [dev-packages] в Pipfile. Зависимости размещенной в этой папке будут устанавливаются только в том случае, если вы укажете аргумент —dev в pipenv install. Это позволит отделить зависимости, необходимые только для разработки, от зависимостей, необходимых для фактической работы базового кода. Ранее это можно было сделать с помощью дополнительных файлов requirements, таких как dev-requirements.txt или test-requirements.txt. Теперь все объединено в один Pip-файл с разными разделами.
Итак, допустим, у нас все работает в локальной среде разработки, и мы готовы приступить к переносу в рабочую среду. Для этого нам нужно заблокировать свою среду, чтобы убедиться, что у нас будет все то же самое в рабочей среде:
$ pipenv lock
Эта команда создаст/обновит файл Pipfile.lock, который вам никогда не придется (и никогда не нужно) редактировать вручную. Вы всегда должны использовать только сгенерированный файл.
Теперь, нам нужно перенести свой код проекта в рабочую среду включая файлы Pipfile и Pipfile.lock . Далее создать там собственную среду окружения командой pipenv shell. И далее установить все зависимости командой:
$ pipenv install --ignore-pipfile
—ignore-pipfile говорит Pipenv игнорировать Pipfile для установки и использовать то, что находится в Pipfile.lock. Учитывая Pipfile.lock, Pipenv создаст ту же среду, которая была у нас, когда мы запустили блокировку зависимостей в pipenv.
В файле блокировки записаны все зависимости, делая снимок всех версий пакетов (аналогично результату pip freeze).
Теперь допустим, что другой разработчик хочет внести некоторые дополнения в наш код. В этой ситуации он склонирует весь код себе на компьютер, включая Pipfile, и воспользуются этой командой для установки всех зависимостей у себя локально:
$ pipenv install --dev
Эта команда установит все зависимости, необходимые для разработки, которые включают в себя как обычные зависимости, так и те, которые вы указали в аргументе —dev во время установки.
Если в файле Pipfile не указывается точная версия, команда install устанавливает последние версии зависимостей.
Это важное замечание, потому что оно решает некоторые из предыдущих проблем, которые мы обсуждали. Для демонстрации, допустим, вышла новая версия одной из наших зависимостей. Поскольку нам не нужна конкретная версия этой зависимости, мы не указываем точную версию в Pipfile. При pipenv install новая версия зависимости будет установлена автоматически.
Теперь мы как бы вносим изменения в код и запускаем несколько тестов, чтобы убедиться, что все по-прежнему работает должным образом. (У нас есть юнит-тесты, верно?) Теперь, как и раньше, мы блокируете свою среду с помощью pipenv lock, и с новой версией зависимости будет сгенерирован обновленный файл Pipfile.lock. Как и раньше, мы можем скопировать этот файл вместе с другими в другую среду и там запустить pipenv install , если необходимо .
Как видно из этого сценария, нам больше не нужно указывать точные версии, которые нам в действительности не нужны, чтобы обеспечить одинаковую среду разработки и производства. Нам также не нужно быть в курсе обновления зависимостей. Этот рабочий процесс с Pipenv, в сочетании с тестированием кода, устраняет проблемы ручного контроля всеми зависимостями.
Pipenv попытается установить подчиненные зависимости, которые удовлетворяют всем требованиям основных зависимостей. Однако, если существуют конфликтующие зависимости (package_a требуется package_c> = 1.0, а package_b требуется package_c < 1.0), Pipenv не сможет создать файл блокировки и выдаст следующую ошибку:
Warning: Your dependencies could not be resolved. You likely have a mismatch in your sub-dependencies. You can use $ pipenv install --skip-lock to bypass this mechanism, then run $ pipenv graph to inspect the situation. Could not find a version that matches package_c>=1.0,package_c<1.0
Как говорится в тексте предупреждения, мы также можем отобразить граф зависимостей, чтобы разобраться с зависимости верхнего уровня и их подчиненными зависимостями:
$ pipenv graph
Эта команда выведет древовидную структуру, показывающую зависимости. Пример вывода:
Flask==0.12.1 - click [required: >=2.0, installed: 6.7] - itsdangerous [required: >=0.21, installed: 0.24] - Jinja2 [required: >=2.4, installed: 2.10] - MarkupSafe [required: >=0.23, installed: 1.0] - Werkzeug [required: >=0.7, installed: 0.14.1] numpy==1.14.1 pytest==3.4.1 - attrs [required: >=17.2.0, installed: 17.4.0] - funcsigs [required: Any, installed: 1.0.2] - pluggy [required: <0.7,>=0.5, installed: 0.6.0] - py [required: >=1.5.0, installed: 1.5.2] - setuptools [required: Any, installed: 38.5.1] - six [required: >=1.10.0, installed: 1.11.0] requests==2.18.4 - certifi [required: >=2017.4.17, installed: 2018.1.18] - chardet [required: >=3.0.2,<3.1.0, installed: 3.0.4] - idna [required: >=2.5,<2.7, installed: 2.6] - urllib3 [required: <1.23,>=1.21.1, installed: 1.22]
Из вывода pipenv graph вы можете увидеть зависимости верхнего уровня, которые мы установили ранее (Flask, numpy, pytest и requests), а под ними можно увидеть пакеты, от которых они зависят.
Кроме того, можно перевернуть дерево, чтобы показать подчиненные зависимости с родителем:
$ pipenv graph --reverse
Перевернутое дерево может быть более полезным, когда вы пытаетесь выяснить конфликтующие подчиненные зависимости.
Pipfile предназначен для замены requirements.txt. Pipenv в настоящее время является эталонной реализацией использования Pipfile. Возможно когда то, сам pip сможет обработать эти файлы. Также стоит отметить, что Pipenv является официальным инструментом управления пакетами, рекомендованным самими разработчиками Python.
Синтаксис Pipfile — это TOML, где файл разделяется на разделы.
[dev-packages] для пакетов только для разработки, [packages] для минимально необходимых пакетов и [requires] для других требований, таких как конкретная версия Python и т.п.
Рассмотрим пример файла:
[[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [dev-packages] pytest = "*" [packages] flask = "==0.12.1" numpy = "*" requests = {git = "https://github.com/requests/requests.git", editable = true} [requires] python_version = "3.6"
В идеале в Pipfile не должно быть никаких суб-зависимостей. Под этим я подразумеваю, что вы должны включать только те пакеты, которые вы на самом деле импортируете и используете. Нет необходимости хранить chardet в вашем Pipfile только потому, что он является зависимостью requests. (Pipenv должен устанавливать его автоматически.) Pipfile должен хранить только зависимости верхнего уровня, необходимые для вашего проекта.
Этот файл включает детерминированные зависимости, с указанными точными требованиями для воспроизведения в виртуальной среды. Он содержит точные версии для пакетов и их хеши для более безопасной проверки. Рассмотрим пример нашего файла. Обратите внимание, что синтаксис этого файла — JSON, и я исключил некоторые части файла с помощью …:
{ "_meta": { ... }, "default": { "flask": { "hashes": [ "sha256:6c3130c8927109a08225993e4e503de4ac4f2678678ae211b33b519c622a7242", "sha256:9dce4b6bfbb5b062181d3f7da8f727ff70c1156cbb4024351eafd426deb5fb88" ], "version": "==0.12.1" }, "requests": { "editable": true, "git": "https://github.com/requests/requests.git", "ref": "4ea09e49f7d518d365e7c6f7ff6ed9ca70d6ec2e" }, "werkzeug": { "hashes": [ "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b", "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c" ], "version": "==0.14.1" } ... }, "develop": { "pytest": { "hashes": [ "sha256:8970e25181e15ab14ae895599a0a0e0ade7d1f1c4c8ca1072ce16f25526a184d", "sha256:9ddcb879c8cc859d2540204b5399011f842e5e8823674bf429f70ada281b3cc6" ], "version": "==3.4.1" }, ... } }
Обратите внимание на точные версии, указанные для каждой зависимости. В этом файле описаны все зависимости включая такие суб-зависимости, как werkzeug, которых нет в нашем Pipfile.
Стоит еще раз отметить, что вы никогда не должны изменять этот файл вручную. Он предназначен для генерации с pipenv lock.
Вы можете открыть установленный пакет в редакторе по умолчанию с помощью следующей команды:
$ pipenv open flask
Эта команда откроет пакет flask в редакторе по умолчанию, или вы можете указать нужную вам программу в переменной окружения EDITOR. Например, я использую Sublime Text, поэтому я устанавливаю EDITOR = subl.
Вы можете запустить команду в виртуальной среде без запуска оболочки:
pipenv run <insert command here>
Проверить наличие уязвимостей безопасности (и требований PEP 508) в вашей среде:
$ pipenv check
Теперь, допустим, вам больше не нужен пакет. Вы можете удалить его командой:
$ pipenv uninstall numpy
Кроме того, допустим, вы хотите полностью стереть все установленные пакеты из вашей виртуальной среды:
$ pipenv uninstall --all
Вы можете заменить —all на —all-dev, чтобы просто удалить пакеты dev.
Pipenv поддерживает автоматическую загрузку переменных среды, когда в каталоге верхнего уровня существует файл .env. Таким образом, когда вы запускаете pipenv shell для открытия виртуальной среды, она загружает переменные среды из этого файла.
Файл .env должен просто содержат пары ключ-значение:
SOME_ENV_CONFIG=some_value SOME_OTHER_ENV_CONFIG=some_other_value
Наконец, вот несколько быстрых команд, чтобы узнать, где что находится.
Команда что бы узнать, по какому пути находится виртуальная среда:
$ pipenv --venv
Команда что бы узнать, по какому пути находится ваш проект:
$ pipenv --where
e
?Если вы запускаете pipenv install, он должен автоматически определить наличия файла requirements.txt и преобразовать его в файл Pipfile, выдав похожее сообщение:
requirements.txt found, instead of Pipfile! Converting… Warning: Your Pipfile now contains pinned versions, if your requirements.txt did. We recommend updating your Pipfile to specify the "*" version, instead.
Обратите внимание на приведенное выше предупреждение.
Если вы указали точные версии в вашем файле requirements.txt, вы, вероятно, захотите изменить свой Pipfile, указав только те версии, которые вам действительно нужны. Это позволит вам получить все преимущества от перехода на Pipenv. Например, допустим, у вас есть следующая зависимость, но вам не нужна точная версия numpy:
[packages] numpy = "==1.14.1"
Если у вас нет особых требований к версии ваших зависимостей, вы можете использовать подстановочный знак *, чтобы сообщить Pipenv, что можно установить любую версию:
[packages] numpy = "*"
Если вы опасаетесь использовать любую версию *, обычно лучше указывать текущую версию, в которой вы уже находитесь, или более старшую:
[packages] numpy = ">=1.14.1"
Вы также можете установить зависимости из файла requirements.txt вручную используя аргумент -r:
$ pipenv install -r requirements.txt
Если у вас есть файл dev-requirements.txt или что-то подобное, вы также можете добавить их в Pipfile. Просто добавьте аргумент —dev, чтобы он был помещен в правильный раздел:
$ pipenv install -r dev-requirements.txt --dev
Кроме того, вы можете пойти другим путем и сгенерировать файлы requirements.txt из Pipfile:
$ pipenv lock -r > requirements.txt $ pipenv lock -r -d > dev-requirements.txt
Определенно. Даже если это просто способ объединить инструменты, которые вы уже используете (pip & virtualenv), в единый интерфейс. Тем не менее, это гораздо больше, чем просто объеденение. Используя Pipfile вы указываете только те зависимости, которые вам действительно нужны.
У вас больше не будет головной боли, от того что вам нужно было самим управлять версиями всего лишь для того, чтобы убедиться, что вы можете клонировать свою среду разработки. С Pipfile.lock вы можете работать над своим проектом со спокойной душой, зная, что вы можете точно воспроизвести свое окружение в любом месте.
В дополнение ко всему, весьма вероятно, что формат Pipfile будет принят и поддержан как официальный инструмент Python, такими как pip, поэтому было бы полезно заранее ознакомится с ним.
Pipfile
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
https://www.reddit.com/r/Python/comments/a3h81m/pipenv_promises_a_lot_delivers_very_little/
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
...
https://chriswarrick.com/blog/2018/07/17/pipenv-promises-a-lot-delivers-very-little/
шалом