{"@attributes":{"version":"2.0"},"channel":{"title":"python on Ceda EI's Blog","link":"https:\/\/cedaei.com\/tags\/python\/","description":"Recent content in python on Ceda EI's Blog","generator":"Hugo -- gohugo.io","language":"en-us","copyright":"CC-BY-SA 4.0","lastBuildDate":"Sun, 19 Sep 2021 00:00:00 +0000","item":{"title":"Improving Python Dependency Management With pipx and Poetry","link":"https:\/\/cedaei.com\/posts\/python-poetry-pipx\/","pubDate":"Sun, 19 Sep 2021 00:00:00 +0000","guid":"https:\/\/cedaei.com\/posts\/python-poetry-pipx\/","description":"<p>Over time, how I develop applications in python has changed noticeably. I will\ndivide the topic into three sections and see how they tie into each other at\nthe end.<\/p>\n<ul>\n<li>Development<\/li>\n<li>Packaging<\/li>\n<li>Usage<\/li>\n<\/ul>\n<h2 id=\"development\">Development<\/h2>\n<p>Under development, the issues I will focus on are the following:<\/p>\n<ul>\n<li>Dependency Management<\/li>\n<li>Virtualenvs and managing them<\/li>\n<\/ul>\n<p>Historically, the way to do dependency management was through\n<code>requirements.txt<\/code>.  I found <code>requirements.txt<\/code> hard to manage. In that setup,\nadding a dependency and installing it was two steps:<\/p>\n<ul>\n<li>Add the package <code>bar<\/code> to <code>requirements.txt<\/code><\/li>\n<li>Either do <code>pip install bar<\/code> or <code>pip install -r requirements.txt<\/code><\/li>\n<\/ul>\n<p>While focused on development, I would often forget one or both of these steps.\nAlso, the lack of a lock file was a small downside for me (could be a much\nlarger downside for others).  The separation between <code>pip<\/code> and\n<code>requirements.txt<\/code> can also easily lead you to accidentally depend on packages\ninstalled on your system or in your virtualenv but not specified in your\n<code>requirements.txt<\/code>.<\/p>\n<p>Managing virtualenvs was also difficult. As a virtualenv and a project are not\nrelated, you need a directory structure. Otherwise, you can&rsquo;t tell which\nvirtualenv is being used for which project. You can use the same virtualenvs\nfor multiple projects, but that partially defeats the point of virtualenvs and\nmakes <code>requirements.txt<\/code> more error-prone (higher chances of forgetting to add\npackages to it). The approach generally used is one of the following two:<\/p>\n<pre><code>foo\/\n\u251c\u2500\u2500 foo_src\/\n\u2514\u2500\u2500 foo_venv\/\n<\/code><\/pre>\n<p>or<\/p>\n<pre><code>foo_src\/\n\u2514\u2500\u2500 venv\/\n<\/code><\/pre>\n<p>I preferred the second one as the first one nests the source code one\ndirectory deeper.<\/p>\n<h3 id=\"a-new-standard---pyprojecttoml\">A new standard - <code>pyproject.toml<\/code><\/h3>\n<p>In <a href=\"https:\/\/www.python.org\/dev\/peps\/pep-0518\/\" \n  \n   target=\"_blank\" rel=\"noreferrer noopener\" \n>PEP-518<\/a>\n, python standardized\nthe <code>pyproject.toml<\/code> file which allows users to choose alternate build systems\nfor package generation.<\/p>\n<p>One such project that provides an alternate build system is\n<a href=\"https:\/\/python-poetry.org\/\" \n  \n   target=\"_blank\" rel=\"noreferrer noopener\" \n>Poetry<\/a>\n. Poetry hits the nail on the head and\nsolves my major gripes with traditional tooling.<\/p>\n<h3 id=\"poetry-and-virtualenvs\">Poetry and virtualenvs<\/h3>\n<p>Poetry manages the virtualenvs automatically and keeps track of which project\nuses which virtualenv automatically. Working on an existing project which uses\npoetry is as simple as this:<\/p>\n<pre><code class=\"language-bash\">$ git clone https:\/\/gitlab.com\/ceda_ei\/verlauf\n$ poetry install\n<\/code><\/pre>\n<p>The <code>poetry install<\/code> command sets up the virtualenv, install all the required\ndependencies inside that, and sets up any commands accordingly (I will get to\nthis soon).  To activate the virtualenv, simply run:<\/p>\n<pre><code class=\"language-bash\">. &quot;$(poetry env info --path)\/bin\/activate&quot;\n<\/code><\/pre>\n<p>I wrap this in a small function which lets me toggle it quickly:<\/p>\n<pre><code class=\"language-bash\">function poet() {\n\tPOET_MANUAL=1\n\tif [[ -v VIRTUAL_ENV ]]; then\n\t\tdeactivate\n\telse\n\t\t. &quot;$(poetry env info --path)\/bin\/activate&quot;\n\tfi\n}\n<\/code><\/pre>\n<p>Running <code>poet<\/code> activates the virtualenv if it is not active and deactivates it if\nit is active. To make things even easier, I automatically activate and\ndeactivate the virtualenv as I enter and leave the project directory.  To do\nso, simply drop this in your <code>.bashrc<\/code>.<\/p>\n<pre><code class=\"language-bash\">function find_in_parent() {\n\tlocal path\n\tIFS=&quot;\/&quot; read -ra path &lt;&lt;&lt;&quot;$PWD&quot;\n\tfor ((i=${#path[@]}; i &gt; 0; i--)); do\n\t\tlocal current_path=&quot;&quot;\n\t\tfor ((j=1; j&lt;i; j++)); do\n\t\t\tcurrent_path=&quot;$current_path\/${path[j]}&quot;\n\t\tdone\n\t\tif [[ -e &quot;${current_path}\/$1&quot; ]]; then\n\t\t\techo &quot;${current_path}\/&quot;\n\t\t\treturn\n\t\tfi\n\tdone\n\treturn 1\n}\n\nfunction auto_poet() {\n\tret=&quot;$?&quot;\n\tif [[ -v POET_MANUAL ]]; then\n\t\treturn $ret\n\tfi\n\tif find_in_parent pyproject.toml &amp;&gt; \/dev\/null; then\n\t\tif [[ ! -v VIRTUAL_ENV ]]; then\n\t\t    if BASE=&quot;$(poetry env info --path)&quot;; then\n\t\t\t. &quot;$BASE\/bin\/activate&quot;\n\t\t\tPS1=&quot;&quot;\n\t\t    else\n\t\t\tPOET_MANUAL=1\n\t\t    fi\n\t\tfi\n\telif [[ -v VIRTUAL_ENV ]]; then\n\t\tdeactivate\n\tfi\n\treturn $ret\n}\n\nPROMPT_COMMAND=&quot;auto_poet;$PROMPT_COMMAND&quot;\n<\/code><\/pre>\n<p>This ties in well with the <code>poet<\/code> function; if you use <code>poet<\/code> anytime in a bash\nsession, activation switches from automatic to manual and changing directories\nno longer auto-toggles the virtualenv.<\/p>\n<p><img src=\"https:\/\/cedaei.com\/images\/auto_poet.webp\" alt=\"auto_poet and poet in action\"><\/p>\n<h3 id=\"poetry-and-dependency-management\">Poetry and dependency management<\/h3>\n<p>Instead of using <code>requirements.txt<\/code>, poetry stores the dependencies inside\n<code>pyproject.toml<\/code>.  Poetry is more strict compared to <code>pip<\/code> in resolving\nversioning issues.  Dependencies and dev-dependencies are stored inside\n<code>tool.poetry.dependencies<\/code> and <code>tool.poetry.dev-dependencies<\/code> respectively.\nHere is an example of a <code>pyproject.toml<\/code> for a project I am working on.<\/p>\n<pre><code class=\"language-toml\">[tool.poetry]\nname = &quot;bells&quot;\nversion = &quot;0.3.0&quot;\ndescription = &quot;Bells is a program for keeping track of sound recordings.&quot;\nauthors = [&quot;Ceda EI &lt;ceda_ei@webionite.com&gt;&quot;]\nlicense = &quot;GPL-3.0&quot;\nreadme = &quot;README.md&quot;\nhomepage = &quot;https:\/\/gitlab.com\/ceda_ei\/bells.git&quot;\nrepository = &quot;https:\/\/gitlab.com\/ceda_ei\/bells.git&quot;\n\n[tool.poetry.dependencies]\npython = &quot;&gt;=3.7,&lt;3.11&quot;\nclick = &quot;^8.0.1&quot;\nquestionary = &quot;^1.10.0&quot;\nsounddevice = &quot;^0.4.2&quot;\nSoundFile = &quot;^0.10.3&quot;\nnumpy = &quot;^1.21.2&quot;\n\n[tool.poetry.dev-dependencies]\n\n[build-system]\nrequires = [&quot;poetry-core&gt;=1.0.0&quot;]\nbuild-backend = &quot;poetry.core.masonry.api&quot;\n\n# I will talk about this section soon\n[tool.poetry.scripts]\nbells = &quot;bells.__main__:main&quot;\n<\/code><\/pre>\n<p>One of the upsides of poetry is that you don&rsquo;t have to manage the dependencies\nin <code>pyproject.toml<\/code> file yourself. Poetry adds an <code>npm<\/code>-like interface for\nadding and removing dependencies.  To add a dependency to your project, simply\nrun <code>poetry add bar<\/code> and it will add it to your <code>pyproject.toml<\/code> file and\ninstall it in the virtualenv as well. To remove a dependency, just run <code>poetry remove bar<\/code>. For development dependencies, just add the <code>--dev<\/code> flag to the\ncommands.<\/p>\n<h2 id=\"packaging\">Packaging<\/h2>\n<p>Since poetry replaces the build system, we can now configure the build using\npoetry via <code>pyproject.toml<\/code>. Inside <code>pyproject.toml<\/code>, the <code>tool.poetry<\/code> section\nstores all the build info needed; <code>tool.poetry<\/code> contains the metadata,\n<code>tool.poetry.dependencies<\/code> contains the dependencies, <code>tool.poetry.source<\/code>\ncontains private repository details (in case, you don&rsquo;t want to use PyPi).<\/p>\n<p>One of the options is <code>tool.poetry.scripts<\/code>. It contains scripts that the\nproject exposes. This replaces <code>console_scripts<\/code> in <code>entry_points<\/code> of\n<code>setuptools<\/code>.<\/p>\n<p>For example,<\/p>\n<pre><code class=\"language-toml\">[tool.poetry.scripts]\nfoobar = &quot;foo.bar:main&quot;\n<\/code><\/pre>\n<p>This will add a script named <code>foobar<\/code> in your <code>PATH<\/code>. Running that is\nequivalent to running the following script<\/p>\n<pre><code class=\"language-python\">from foo.bar import main\n\nif __name__ == &quot;__main__&quot;:\n    main()\n<\/code><\/pre>\n<p>For further details, check the\n<a href=\"https:\/\/python-poetry.org\/docs\/pyproject\/\" \n  \n   target=\"_blank\" rel=\"noreferrer noopener\" \n>reference<\/a>\n.<\/p>\n<p>Poetry also removes the need for manually doing editable installs (<code>pip install -e .<\/code>).  The package is automatically installed as editable when you run\n<code>poetry install<\/code>. Any scripts specified in <code>tool.poetry.scripts<\/code> are\nautomatically available in your <code>PATH<\/code> when you activate the <code>venv<\/code>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1<\/a><\/sup><\/p>\n<p>To build the package, simply run <code>poetry build<\/code>. This will generate a wheel and\na tarball in the dist folder.<\/p>\n<p>To publish the package to PyPi (or another repo), simply run <code>poetry publish<\/code>.\nYou can combine the build and publish into one command with <code>poetry publish --build<\/code>.<\/p>\n<p><img src=\"https:\/\/cedaei.com\/images\/poetry_build.webp\" alt=\"example of poetry build\"><\/p>\n<h2 id=\"usage\">Usage<\/h2>\n<p>This part is more user-facing rather than dev-facing. If you want to use two\npackages globally that expose some scripts to the user, (e.g. <code>awscli<\/code>,\n<code>youtube-dl<\/code>, etc.) the general approach to do so is to run something like <code>pip install --user youtube-dl<\/code>. This install the package at the user level and\nexposes the script through <code>~\/.local\/bin\/youtube-dl<\/code>. However, this installs\nall the packages at the same user level. Hypothetically, if you have two\npackages <code>foo<\/code> and <code>bar<\/code> which have conflicting dependencies, this causes an\nissue. If you run,<\/p>\n<pre><code class=\"language-bash\">$ pip install foo\n$ pip install bar\n$ bar # works\n$ foo # breaks because of dependency mismatch\n<\/code><\/pre>\n<p>While installing <code>bar<\/code>, <code>pip<\/code> will install the dependencies for <code>bar<\/code> which\nwill break <code>foo<\/code> after warning you<sup id=\"fnref:2\"><a href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\">2<\/a><\/sup>.<\/p>\n<p>To solve this, there is <a href=\"https:\/\/github.com\/pypa\/pipx\" \n  \n   target=\"_blank\" rel=\"noreferrer noopener\" \n><code>pipx<\/code><\/a>\n. Pipx installs\neach package in a separate virtualenv without requiring the user to activate\nsaid virtualenv before using the package.<sup id=\"fnref:3\"><a href=\"#fn:3\" class=\"footnote-ref\" role=\"doc-noteref\">3<\/a><\/sup><\/p>\n<p>In the same scenario as before, doing the following works just fine.<\/p>\n<pre><code class=\"language-bash\">$ pipx install foo\n$ pipx install bar\n$ bar # works\n$ foo # also works\n<\/code><\/pre>\n<p>In this scenario, both <code>bar<\/code> and <code>foo<\/code> are installed in separate virtualenvs so\nthe dependency conflict doesn&rsquo;t matter.<\/p>\n<h2 id=\"some-more-things-from-my-bashrc\">Some more things from my bashrc<\/h2>\n<pre><code class=\"language-bash\">\nfunction wrapper_no_poet() {\n\tlocal last_env\n\tif [[ -v VIRTUAL_ENV ]]; then\n\t\tlast_env=&quot;$VIRTUAL_ENV&quot;\n\t\tdeactivate\n\tfi\n\t&quot;$@&quot;\n\tret=$?\n\tif [[ -v last_env ]]; then\n\t\t. &quot;$last_env\/bin\/activate&quot;\n\tfi\n\treturn $ret\n}\n\nalias wnp='wrapper_no_poet'\nalias pm='POET_MANUAL=1'\n<\/code><\/pre>\n<p>Prefixing any command with <code>wnp<\/code> runs it outside the virtualenv if a virtualenv\nis active. Running <code>pm<\/code> turns off automatic virtualenv activation.<\/p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"fn:1\" role=\"doc-endnote\">\n<p>This also allows for a nice switch between the development and production\nversions of the app. Essentially, when the virtualenv is active, you are\nusing the development script while when it is deactivated, you are using\nthe global (likely production) version.&#160;<a href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;<\/a><\/p>\n<\/li>\n<li id=\"fn:2\" role=\"doc-endnote\">\n<p>To be precise, it will warn you that it broke <code>foo<\/code> but will still\ncontinue with the installation&#160;<a href=\"#fnref:2\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;<\/a><\/p>\n<\/li>\n<li id=\"fn:3\" role=\"doc-endnote\">\n<p>For development, poetry also provides <code>poetry run<\/code> which runs a file\nwithout having to activate the virtualenv.&#160;<a href=\"#fnref:3\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;<\/a><\/p>\n<\/li>\n<\/ol>\n<\/section>\n"}}}