brainsteam.co.uk/new_files/2021/04/01/opinionated-guide-to-virtua.../index.html

235 lines
28 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"><title>An opinionated guide to Python environments in 2021 - Brainsteam</title><meta name="viewport" content="width=device-width, initial-scale=1">
<meta itemprop="name" content="An opinionated guide to Python environments in 2021">
<meta itemprop="description" content="A fairly thorough explanation and exploration of python package and environment managers as of April 2021 with some opinionated setups proposed for different user types at the end."><meta itemprop="datePublished" content="2021-04-12T20:21:11&#43;00:00" />
<meta itemprop="dateModified" content="2021-04-12T20:21:11&#43;00:00" />
<meta itemprop="wordCount" content="2801"><meta itemprop="image" content="https://brainsteam.co.uk/2021/04/01/opinionated-guide-to-virtualenvs/images/feature.jpg">
<meta itemprop="keywords" content="python,devops," /><meta property="og:title" content="An opinionated guide to Python environments in 2021" />
<meta property="og:description" content="A fairly thorough explanation and exploration of python package and environment managers as of April 2021 with some opinionated setups proposed for different user types at the end." />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://brainsteam.co.uk/2021/04/01/opinionated-guide-to-virtualenvs/" /><meta property="og:image" content="https://brainsteam.co.uk/2021/04/01/opinionated-guide-to-virtualenvs/images/feature.jpg"/><meta property="article:section" content="posts" />
<meta property="article:published_time" content="2021-04-12T20:21:11&#43;00:00" />
<meta property="article:modified_time" content="2021-04-12T20:21:11&#43;00:00" />
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content="https://brainsteam.co.uk/2021/04/01/opinionated-guide-to-virtualenvs/images/feature.jpg"/>
<meta name="twitter:title" content="An opinionated guide to Python environments in 2021"/>
<meta name="twitter:description" content="A fairly thorough explanation and exploration of python package and environment managers as of April 2021 with some opinionated setups proposed for different user types at the end."/>
<link href='https://fonts.googleapis.com/css?family=Playfair+Display:700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" media="screen" href="https://brainsteam.co.uk/css/normalize.css" />
<link rel="stylesheet" type="text/css" media="screen" href="https://brainsteam.co.uk/css/main.css" />
<link id="dark-scheme" rel="stylesheet" type="text/css" href="https://brainsteam.co.uk/css/dark.css" />
<script src="https://brainsteam.co.uk/js/feather.min.js"></script>
<script src="https://brainsteam.co.uk/js/main.js"></script>
</head>
<body>
<div class="container wrapper">
<div class="header">
<div class="avatar">
<a href="https://brainsteam.co.uk/">
<img src="/images/avatar.png" alt="Brainsteam" />
</a>
</div>
<h1 class="site-title"><a href="https://brainsteam.co.uk/">Brainsteam</a></h1>
<div class="site-description"><p>The irregular mental expulsions of a PhD student and CTO of Filament, my views are my own and do not represent my employers in any way.</p><nav class="nav social">
<ul class="flat"><li><a href="https://twitter.com/jamesravey/" title="Twitter" rel="me"><i data-feather="twitter"></i></a></li><li><a href="https://github.com/ravenscroftj" title="Github" rel="me"><i data-feather="github"></i></a></li><li><a href="/index.xml" title="RSS" rel="me"><i data-feather="rss"></i></a></li></ul>
</nav></div>
<nav class="nav">
<ul class="flat">
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/tags">Tags</a>
</li>
<li>
<a href="https://jamesravey.me">About Me</a>
</li>
</ul>
</nav>
</div>
<div class="post">
<div class="post-header">
<div class="meta">
<div class="date">
<span class="day">12</span>
<span class="rest">Apr 2021</span>
</div>
</div>
<div class="matter">
<h1 class="title">An opinionated guide to Python environments in 2021</h1>
</div>
</div>
<div class="markdown">
<figure>
<img src="images/feature.jpg"
alt="A person overwhelmed by boxes by Cottonbro"/> <figcaption>
<p>A person overwhelmed by boxes by <a href='https://www.pexels.com/@cottonbro?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels'>Cottonbro</a></p>
</figcaption>
</figure>
<h3 id="note-if-you-dont-want-to-read-the-blah-blah-context-and-history-stuff-then-you-can-jump-to-the-recommendationsrecommended-setups-for-various-use-cases">Note: If you don&rsquo;t want to read the blah-blah context and history stuff then you can <a href="#recommended-setups-for-various-use-cases">jump to the recommendations</a></h3>
<h2 id="the-problem">The Problem</h2>
<p>The need for virtual python environments becomes fairly obvious early in most Python developers' careers when they switch between two projects and realise that they have incompatible dependences (e.g. project1 needs <code>scikit-learn-0.21</code> and project2 needs <code>scikit-learn-0.24</code>). Unlike other mainstream languages like Javascript(Node.js) and Java (with Maven) where dependencies are stored locally to the project, Python dependencies are installed at system or environment level and affect all projects that are using the same environment.</p>
<p>When you run into this problem - you have two choices: you can either play around with the libraries you have installed and risk breaking things for one of your projects and not being able to get it back or you search &ldquo;python incompatible dependencies install both?&rdquo; with a hopeful glimmer in your eye as you hit enter.</p>
<h2 id="virtual-environments-and-package-managers">Virtual Environments and Package Managers</h2>
<p>Virtual environments are the community approved way to manage this issue and likely the first hit you&rsquo;ll get with the above search in your favourite search engine.</p>
<p>There are two related but distinct activities that we need to manage here:</p>
<ol>
<li>If I&rsquo;m working on different projects, I want to be able to quickly switch between them and their supporting Python runtime environments without breaking my setup for other projects. This is what an environment manager does.</li>
<li>I want to be able to quickly and easily install new Python libraries without worrying about their inter-dependencies. Furthermore, I&rsquo;d like to be able to package up my project&rsquo;s list of dependencies so that others can quickly and easily use my code. This is what a package manager does.</li>
</ol>
<p>There are lots of options available to you for both tasks and some tools try to solve both of them for you. Unfortunately this means that there are a large number, partially compatible standards.</p>
<h2 id="virtual-environments-what-are-they">Virtual Environments: What are they?</h2>
<p>A virtual environment is a copy of a Python interpreter, bundled away into a folder with project-specific libraries and dependencies. This allows you to keep your project runtimes logically separated and avoids inter-project dependency conflicts.</p>
<p><img src="images/virtualenv.png" alt="virtualenv.png"></p>
<p>Simply: when you install a library into a virtual environment the files are literally in a separate folder to the dependencies of your other projects. Beautiful simplicity.</p>
<p>So then, how do we manage which libraries are installed in the environment and make sure that they are compatible with each other and the software that we&rsquo;re writing/using?</p>
<h2 id="pip-the-original-python-package-manager">pip: The Original Python Package Manager</h2>
<p><code>pip</code> is the official <a href="https://www.pypa.io/en/latest/index.html">Python Python Packaging Authority</a> package management tool. It&rsquo;s been a recognisable part of a Python developer&rsquo;s arsenal for at least the last 10 years and became part of the standard Python library as of v3.4
in 2014 (although most operating systems distributed it as standard long before then or if not a very easily installable extra).</p>
<p>Whilst it&rsquo;s the official option, <code>pip</code> is very bare bones. It doesn&rsquo;t know or care which environment it is being run in so you have to make sure that you take care of that by using tools like <a href="https://docs.python.org/3/library/venv.html">venv</a> or <a href="https://virtualenv.pypa.io/en/latest/">virtualenv</a>. Furthermore <code>pip</code> doesn&rsquo;t store its list of dependencies in a file by default (you have to manually call <code>pip freeze &gt; requirements.txt</code> to store your pip environment state in a text file every time you install or uninstall stuff) so this is yet another overhead.</p>
<p>Another potential problem with pip is its <a href="https://github.com/pypa/pip/issues/5102">lack of deterministic builds</a> - simply put: if you don&rsquo;t explicitely ask <code>pip</code> to install a particular version of a package or one of that package&rsquo;s dependencies it will download the <em>latest</em> version of that package. That means that there might be a bug introduced because a dependency-of-a-dependency that I installed on my system last month is a different version to the same package for someone who just installed my software today. What a headache!</p>
<p>None of this is particularly ideal - <strong>more manual steps = more stuff you can forget about</strong></p>
<h2 id="pipenv-environment--package-management-swiss-army-knife">Pipenv: Environment + Package Management Swiss Army Knife</h2>
<p><a href="https://pipenv.pypa.io/en/latest/">pipenv</a> is a tool that tries to solve many of the shortcomings of pip above:</p>
<ul>
<li>pipenv generates a <code>Pipfile</code> in your project which is conceptually similar to a <a href="https://nodejs.dev/learn/the-package-json-guide">Package.json</a> file in Node.js land. That is, a manifest at the top level of your project that describes which dependencies and Python version it requires. This file is maintained as you add/remove packages (no more manual <code>pip freeze</code> steps)</li>
<li>Pipenv also maintains a <code>Pipfile.lock</code> file - this is a machine readable list of all of your dependencies and subdependencies allowing Pipenv to handle deterministic builds and avoid confusing dependency issues.</li>
<li>Pipenv will transparently take care of your virtualenv management for you. You can run your commands as normal but prefixed with <code>pipenv run</code> and the library will make sure you&rsquo;re using the environment associated with whatever project you&rsquo;re trying to use.</li>
</ul>
<p>Many people stopped using pipenv when they believed the project to have been abandoned in 2019. However, it turns out pipenv is still under active development. As of writing the most recent release was <a href="https://github.com/pypa/pipenv/releases/tag/v2020.11.15">v2020.11.15</a>.</p>
<h2 id="pypoetry-a-challenger-environment--package-management-option">pypoetry: A Challenger Environment + Package Management Option</h2>
<p><a href="https://python-poetry.org/">Poetry</a> is yet another all-in-one virtualenv and package manager which offers similar functionality to <code>pipenv</code>. It gained a lot of users during the pipenv project hiatus mentioned above and has <a href="https://dev.to/frostming/a-review-pipenv-vs-poetry-vs-pdm-39b4">similar performance and functionality</a>.</p>
<p>The main reason I prefer poetry over pipenv today is its ability to generate &ldquo;standard&rdquo; Python packages (wheels, source distributions) that are fully Pypa compliant natively (you can do this with <code>pipenv</code> but it requires manual maintainence of <a href="https://greut.medium.com/building-a-python-package-a-docker-image-using-pipenv-233d8793b6cc">setup.py and requirements.txt</a> files which is another moving part that could go wrong in a big project).</p>
<p>Pypoetry also stores its project information in a <a href="https://www.python.org/dev/peps/pep-0621/#abstract">PEP-621</a> compatible <code>pyproject.toml</code> format, providing core metadata compatibility with other dependency management tools and indeed PyPA&rsquo;s own <a href="https://github.com/pypa/setuptools/issues/1688">setuptools</a> toolkit.</p>
<h2 id="where-do-minianaconda-fit-into-all-of-this">Where do [Mini/Ana]conda Fit Into All of This?</h2>
<p><a href="https://docs.continuum.io/anaconda/install/">Anaconda</a> and its slimmed down cousin <a href="https://docs.conda.io/projects/continuumio-conda/en/latest/user-guide/install/download.html">Miniconda</a> are alternatives to the standard CPython/PyPA python distribution distributed by Continuum Analytics. Both environments use the <a href="https://docs.conda.io/projects/continuumio-conda/en/latest/index.html#">conda</a> package + venv management tool.</p>
<p>Conda is open source but not directly compatible with PyPa packages. However, almost every package you can think of is available on <a href="https://conda-forge.org/">conda-forge</a> - a community driven conda-compatible package repository. Furthermore, if something is missing from conda you can run <code>pip</code> inside your conda virtual environment and get it the normal way from PyPa.</p>
<h3 id="what-about-deterministic-builds-and-distributing-software-using-conda">What About Deterministic Builds and Distributing Software Using Conda?</h3>
<p>Well, conda environments and requirements can be stored in an <a href="https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html">environment.yml</a> and the file format allows you to specify both packages installed via conda and pip. Furthermore, using the <code>conda env export</code> command to generate an <code>environment.yml</code> file dumps all of the packages installed in your current environment including their version information for deterministic recreation. Happy days!</p>
<h3 id="but-wait-theres-more">But Wait, There&rsquo;s More!</h3>
<p>One feature of conda that is both controversial and convenient - the latter especially if you&rsquo;re a data scientist - is it&rsquo;s management of system libraries and dependencies beyond Python. Conda can install C libraries that your Python packages depend on for you - including Nvidia&rsquo;s CUDA runtime libraries needed for tensorflow and torch.</p>
<p>If you&rsquo;ve ever had the pleasure of trying to manually configure Nvidia drivers and CUDA runtime libraries on Linux you&rsquo;ll know how much of a pain this is. Even with pip/virtualenv environments, torch and tensorflow will try to link against and load whichever version of CUDA is installed system wide and that means that switching between versions of these libraries for different projects could mean messing with which system libraries are installed. Assuming you even have permission to do that (you might be on a shared GPU cluster), we&rsquo;re back at square one with tightly coupled inter-project dependencies - the very problem that virtualenv is supposed to fix for us but can&rsquo;t because of the dependency on cuda. As usual there are of course <a href="https://towardsdatascience.com/installing-multiple-cuda-cudnn-versions-in-ubuntu-fcb6aa5194e2">manual workarounds</a> but to me this is another moving part that could fail or go wrong - especially in a team environment.</p>
<p>As for the controversy? Well purists don&rsquo;t tend to like the fact that <code>conda</code> also messes with system libraries - even if those libraries, like with pip/virtualenv based environments are copies isolated in folders.</p>
<h3 id="if-conda-is-so-good-why-dont-you-marry-it">If Conda is So Good Why Don&rsquo;t You Marry It?</h3>
<p>Conda is great but it has its downsides too:</p>
<ul>
<li>It&rsquo;s incompatibility with the pip/pypa universe requires extra faff when building pip-compatible software (or you can accept that your software is doomed to only be run by conda-users)</li>
<li><code>environment.yml</code> files can be <strong>too</strong> deterministic and this is exacerbated by the system libraries issue. If I generate an <code>environment.yml</code> file on my Linux desktop and create a conda environment from it on my Mac it will usually fail because the linux libraries are not compatible with the mac libraries.</li>
<li>Running conda inside docker environments is a bit weird and again controversial some might argue since you always have permission to install whichever libraries you need inside a container and there shouldn&rsquo;t be any use cases where you&rsquo;d need two conflicting environments/libraries inside a container. <a href="https://pythonspeed.com/articles/activate-conda-dockerfile/">Again, it&rsquo;s perfectly possible</a> but in my opinion, another weak link.</li>
</ul>
<h2 id="best-of-both-worlds-conda--pip-based-package-managers">Best of Both Worlds: Conda + Pip-based Package Managers</h2>
<p>Both <a href="https://python-poetry.org/docs/managing-environments/">poetry</a> and <a href="https://pipenv.pypa.io/en/latest/advanced/#pipenv-and-other-python-distributions">pipenv</a> can be used in combination with other virtual environment managers.</p>
<p>This, in my opinion, offers the best of both worlds: we can take the speed and ease-of-use of conda and team it up with the flexibility and compatibility offered by these pip-based package management offerings.</p>
<p>To use pipenv or poetry inside a conda-based environment you can simply activate the environment you want to use and then run <code>pip install poetry</code> or <code>pip install pipenv</code> - the tool of your choice will then be available for use whenever you have that environment active in the future.</p>
<h1 id="recommended-setups-for-various-use-cases">Recommended Setups for Various Use Cases</h1>
<h2 id="some-principles-for-use-with-the-recommendations-below">Some Principles for Use With The Recommendations Below</h2>
<ul>
<li><strong>K.I.S.S</strong> Keep it simple stupid - these suggestions get more complicated for more nuanced use cases. My general philosophy, as mentioned earlier in the post is to minimise moving parts so I definitely don&rsquo;t think everyone should be maintaining a <code>pyproject.toml</code>, a <code>requirements.txt</code> file, an <code>environment.yml</code> file for Windows usesrs and an <code>environment.yml</code> file for Linux users. You know your use case and you can judge for yourself what is appropriate.</li>
<li><strong>If I say <em>or</em> then it&rsquo;s up to you</strong>. Pick one and be consistent. Quite a lot of the time <code>poetry</code> and <code>pipenv</code> offer very similar feature sets and which one you want to use is just a personal preference. They&rsquo;re not directly compatible though so if you pick <code>poetry</code> and your colleague picks <code>pipenv</code> you&rsquo;re going to have a bad time.</li>
</ul>
<h2 id="im-new-to-python-mac-windows-or-linux">I&rsquo;m new to Python (Mac, Windows or Linux)</h2>
<p>Firstly, if you&rsquo;re <em><strong>really really</strong></em> new to Python you might want to consider just getting familiar with the language without having to deal with virtual environments - most modern Linux distributions have Python 3.x pre-installed and if you&rsquo;re on mac you can get it trivially if you use <a href="https://brew.sh/">brew</a>. That said, virtualenvs are likely to be something that you&rsquo;ll need sooner rather than later once you get into intermediate Python development so it might be better to dive in sooner rather than later.</p>
<ul>
<li><strong>If you&rsquo;re new to Python and you&rsquo;re running Mac, Windows or Linux</strong> you might find <a href="https://docs.anaconda.com/anaconda/install/index.html">Anaconda</a> to be the most intuitive, lowest barrier to entry option for getting started.</li>
<li><strong>If you&rsquo;re on Windows</strong>, Conda-based distributions definitely represent the lowest barrier to entry since you don&rsquo;t have to worry about setting up compilers and libraries. That said, if you are running <a href="https://docs.microsoft.com/en-us/windows/wsl/install-win10">WSL</a> you probably already have Python 3 installed and can make use of some <a href="https://towardsdatascience.com/python-and-the-wsl-597fbe05659f">excellent existing resources</a>.</li>
<li><strong>If you&rsquo;re new to deep learning</strong>, again conda-based distributions are probably the lowest barrier to entry since conda can handle installing CUDA and dependencies for you.</li>
</ul>
<h2 id="im-an-experienced-python-developer-and-noone-else-needs-to-run-my-code">I&rsquo;m an experienced Python developer and noone else needs to run my code</h2>
<p>My suggestions assume that even though you&rsquo;re not planning to share your code with others, you&rsquo;re still interested in version controlling it and your dependencies in case your laptop breaks/gets stolen/spontaneously combusts and you need to re-create your project.</p>
<ul>
<li><strong>If you&rsquo;re on Linux or Mac and you don&rsquo;t need CUDA</strong> then, assuming you have root permissions you&rsquo;ll probably find that <code>pipenv</code> or <code>poetry</code> work well for you. I&rsquo;m not suggesting conda as a first stop since most of the time Python 3.X is already available in modern *Nix environments so you might not need to install anything (except your chosen package manager via <code>pip</code>).</li>
<li><strong>If you&rsquo;re on Linux or Mac and you need CUDA</strong> then <code>conda</code> is likely the lowest barrier to entry. If you&rsquo;ve never done it, try installing and using Tensorflow/PyTorch without conda once - for academic/edification. Then you&rsquo;ll be able to feel the benefit.</li>
<li><strong>If you&rsquo;re working on Windows outside of WSL</strong> my default suggestion would still be conda due to its management of compiler toolchains and external libraries. If you&rsquo;re on windows inside WSL then see above for Linux/Mac.</li>
</ul>
<h2 id="im-writing-privateproprietary-python-code-that-friendscolleagues-need-to-use">I&rsquo;m writing private/proprietary Python code that friends/colleagues need to use</h2>
<ul>
<li><strong>If you all run the same OS</strong> (for example you&rsquo;re all on the same analytics team in an organisation that uses Windows 10 company-wide) then K.I.S.S and use conda. If everyone is using the same OS you can probably safely mix <code>conda install</code> and <code>pip install</code> commands and version control your <code>environment.yml</code> file without worrying about cross-platform compatibility issues.</li>
<li><strong>If you are writing code that needs to work cross-platform but you don&rsquo;t need CUDA</strong> (e.g. you run MacOS, your colleage runs Linux) then use <code>pipenv</code> or <code>poetry</code>. This will allow you to provide cross-platform deterministic builds/dependency resolution. Keep the <code>pyproject.toml</code> or <code>Pipfile</code> and respective lock files version controlled. If you or one of your colleagues runs Windows, they might find that the easiest way to interact with you is to install anaconda and then run <code>pipenv</code> or <code>poetry</code> inside a conda-managed environment.</li>
<li><strong>If you are writing code that needs to work cross-platform and uses CUDA</strong> (e.g. you&rsquo;re building a PyTorch model on Linux and your friend wants to run it on Windows) then you&rsquo;re probably going to want to use <code>conda</code> to manage the environment (i.e. pull in specific versions of cuda runtime libraries) and <code>poetry</code> or <code>pipenv</code> to manage pythonic dependencies. You could version control a hand written <code>environment.yml</code> with the specific versions of the cuda runtime that your model is expecting (but without OS-specific build tags) and you will definitely want to version control your <code>pyproject.toml</code> and <code>Pipfile</code> as above. Alternatively, document the <code>conda install</code> commands the user should run in the project readme.</li>
<li><strong>If you are writing code that you need to package as a wheel or egg for others to use</strong> (e.g. it&rsquo;s a proprietary Python package you ship to customers) then I refer you to the section below but leveraging <a href="https://python-poetry.org/docs/cli/#publish">poetry publish</a> <code>--repository</code> option to specify a private PIP repository.</li>
</ul>
<h2 id="im-writing-python-code-that-i-want-to-share-with-the-community">I&rsquo;m writing Python code that I want to share with the community</h2>
<ul>
<li><strong>If you don&rsquo;t need CUDA</strong> then my suggestion would be standalone <code>poetry</code> since it has build/distribution tools built in and you can produce wheels and source distributions from the commandline and submit them to <a href="https://pypi.org/">pypi</a>. Version control your <code>pyproject.toml</code> and <code>poetry.lock</code> files.</li>
<li><strong>If you need CUDA</strong> then my suggestion is to use conda to create and manage your virtual environment and install cuda and then use <code>poetry</code> to manage packages and PyPi build (or use standalone <code>poetry</code> and manually manage your cuda libraries - you masochist you!). You might want to version control your <code>environment.yml</code> but this file won&rsquo;t be needed for building or uploading your package to PyPi - it&rsquo;s just for you (and other developers) to use to quickly spin up your project locally in a development context.</li>
<li><strong>If you want your package to be available in conda</strong> then you&rsquo;ll need to use <a href="https://conda.io/projects/conda-build/en/latest/user-guide/tutorials/build-pkgs-skeleton.html">conda-build</a> to generate conda-specific package files and metadata for your project.</li>
</ul>
<h1 id="pep-582-pdm-and-the-future-of-python-dependencies">PEP-582, PDM and the Future of Python Dependencies?</h1>
<p>Without wishing to confuse matters further, I wanted to give <a href="https://www.python.org/dev/peps/pep-0582/">PEP-582</a> an honourable mention.</p>
<p>This is a Python Enhancement Proposal that will allow the python runtime to support <code>npm</code>-esque loading of dependencies from a file in the project directory (like <code>node_modules</code>). There is already a package manager <a href="https://pdm.fming.dev/">PDM</a> in development for working with local directories</p>
<p>This is an interesting and exciting paradigm shift that should simplify python packaging and remove the need completely for virtual environments. However, there are many issues to solve and the proposal is only for Python 3.8 with no plans to backport the functionality to earlier versions of the language runtime.</p>
<p>Given how long it&rsquo;s taken some users to make the jump from Python 2.X to Python 3.X, it is likely that virtual environments are going to be around for a few more years to come.</p>
</div>
<div class="tags">
<ul class="flat">
<li><a href="/tags/python">python</a></li>
<li><a href="/tags/devops">devops</a></li>
</ul>
</div><div id="disqus_thread"></div>
<script type="text/javascript">
(function () {
if (window.location.hostname == "localhost")
return;
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
var disqus_shortname = 'brainsteam';
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
<noscript>Please enable JavaScript to view the </a></noscript>
<a href="http://disqus.com/" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
</div>
</div>
<div class="footer wrapper">
<nav class="nav">
<div>2021 © James Ravenscroft 2020 | <a href="https://github.com/knadh/hugo-ink">Ink</a> theme on <a href="https://gohugo.io">Hugo</a></div>
</nav>
</div>
<script type="application/javascript">
var doNotTrack = false;
if (!doNotTrack) {
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-186263385-1', 'auto');
ga('send', 'pageview');
}
</script>
<script async src='https://www.google-analytics.com/analytics.js'></script>
<script>feather.replace()</script>
</body>
</html>