<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: pytest</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/pytest.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-01-26T23:55:29+00:00</updated><author><name>Simon Willison</name></author><entry><title>Tips for getting coding agents to write good Python tests</title><link href="https://simonwillison.net/2026/Jan/26/tests/#atom-tag" rel="alternate"/><published>2026-01-26T23:55:29+00:00</published><updated>2026-01-26T23:55:29+00:00</updated><id>https://simonwillison.net/2026/Jan/26/tests/#atom-tag</id><summary type="html">
    &lt;p&gt;Someone &lt;a href="https://news.ycombinator.com/item?id=46765460#46765823"&gt;asked&lt;/a&gt; on Hacker News if I had any tips for getting coding agents to write decent quality tests. Here's what I said:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I work in Python which helps a lot because there are a TON of good examples of pytest tests floating around in the training data, including things like usage of fixture libraries for mocking external HTTP APIs and snapshot testing and other neat patterns.&lt;/p&gt;
&lt;p&gt;Or I can say "use pytest-httpx to mock the endpoints" and Claude knows what I mean.&lt;/p&gt;
&lt;p&gt;Keeping an eye on the tests is important. The most common anti-pattern I see is large amounts of duplicated test setup code - which isn't a huge deal, I'm much more more tolerant of duplicated logic in tests than I am in implementation, but it's still worth pushing back on.&lt;/p&gt;
&lt;p&gt;"Refactor those tests to use pytest.mark.parametrize" and "extract the common setup into a pytest fixture" work really well there.&lt;/p&gt;
&lt;p&gt;Generally though the best way to get good tests out of a coding agent is to make sure it's working in a project with an existing test suite that uses good patterns. Coding agents pick the existing patterns up without needing any extra prompting at all.&lt;/p&gt;
&lt;p&gt;I find that once a project has clean basic tests the new tests added by the agents tend to match them in quality. It's similar to how working on large projects with a team of other developers work - keeping the code clean means when people look for examples of how to write a test they'll be pointed in the right direction.&lt;/p&gt;
&lt;p&gt;One last tip I use a lot is this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Clone datasette/datasette-enrichments
from GitHub to /tmp and imitate the
testing patterns it uses
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I do this all the time with different existing projects I've written - the quickest way to show an agent how you like something to be done is to have it look at an example.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="python"/><category term="testing"/><category term="ai"/><category term="pytest"/><category term="generative-ai"/><category term="llms"/><category term="coding-agents"/></entry><entry><title>TIL: Subtests in pytest 9.0.0+</title><link href="https://simonwillison.net/2025/Dec/5/til-pytest-subtests/#atom-tag" rel="alternate"/><published>2025-12-05T06:03:29+00:00</published><updated>2025-12-05T06:03:29+00:00</updated><id>https://simonwillison.net/2025/Dec/5/til-pytest-subtests/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/pytest/subtests"&gt;TIL: Subtests in pytest 9.0.0+&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I spotted an interesting new feature &lt;a href="https://docs.pytest.org/en/stable/changelog.html#pytest-9-0-0-2025-11-05"&gt;in the release notes for pytest 9.0.0&lt;/a&gt;: &lt;a href="https://docs.pytest.org/en/stable/how-to/subtests.html#subtests"&gt;subtests&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I'm a &lt;em&gt;big&lt;/em&gt; user of the &lt;a href="https://docs.pytest.org/en/stable/example/parametrize.html"&gt;pytest.mark.parametrize&lt;/a&gt; decorator - see &lt;a href="https://simonwillison.net/2018/Jul/28/documentation-unit-tests/"&gt;Documentation unit tests&lt;/a&gt; from 2018 - so I thought it would be interesting to try out subtests and see if they're a useful alternative.&lt;/p&gt;
&lt;p&gt;Short version: this parameterized test:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;pytest&lt;/span&gt;.&lt;span class="pl-c1"&gt;mark&lt;/span&gt;.&lt;span class="pl-c1"&gt;parametrize&lt;/span&gt;(&lt;span class="pl-s"&gt;"setting"&lt;/span&gt;, &lt;span class="pl-s1"&gt;app&lt;/span&gt;.&lt;span class="pl-c1"&gt;SETTINGS&lt;/span&gt;)&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;test_settings_are_documented&lt;/span&gt;(&lt;span class="pl-s1"&gt;settings_headings&lt;/span&gt;, &lt;span class="pl-s1"&gt;setting&lt;/span&gt;):
    &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s1"&gt;setting&lt;/span&gt;.&lt;span class="pl-c1"&gt;name&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;settings_headings&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;Becomes this using subtests instead:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;test_settings_are_documented&lt;/span&gt;(&lt;span class="pl-s1"&gt;settings_headings&lt;/span&gt;, &lt;span class="pl-s1"&gt;subtests&lt;/span&gt;):
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;setting&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;app&lt;/span&gt;.&lt;span class="pl-c1"&gt;SETTINGS&lt;/span&gt;:
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;subtests&lt;/span&gt;.&lt;span class="pl-c1"&gt;test&lt;/span&gt;(&lt;span class="pl-s1"&gt;setting&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;setting&lt;/span&gt;.&lt;span class="pl-c1"&gt;name&lt;/span&gt;):
            &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s1"&gt;setting&lt;/span&gt;.&lt;span class="pl-c1"&gt;name&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;settings_headings&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;Why is this better? Two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It appears to run a bit faster&lt;/li&gt;
&lt;li&gt;Subtests can be created programatically after running some setup code first&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I &lt;a href="https://gistpreview.github.io/?0487e5bb12bcbed850790a6324788e1b"&gt;had Claude Code&lt;/a&gt; port &lt;a href="https://github.com/simonw/datasette/pull/2609/files"&gt;several tests&lt;/a&gt; to the new pattern. I like it.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/til"&gt;til&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="testing"/><category term="ai"/><category term="pytest"/><category term="til"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="coding-agents"/><category term="claude-code"/></entry><entry><title>Setting up a codebase for working with coding agents</title><link href="https://simonwillison.net/2025/Oct/25/coding-agent-tips/#atom-tag" rel="alternate"/><published>2025-10-25T18:42:24+00:00</published><updated>2025-10-25T18:42:24+00:00</updated><id>https://simonwillison.net/2025/Oct/25/coding-agent-tips/#atom-tag</id><summary type="html">
    &lt;p&gt;Someone on Hacker News &lt;a href="https://news.ycombinator.com/item?id=45695621#45704966"&gt;asked for tips&lt;/a&gt; on setting up a codebase to be more productive with AI coding tools. Here's my reply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Good automated tests which the coding agent can run. I love pytest for this - one of my projects has 1500 tests and Claude Code is really good at selectively executing just tests relevant to the change it is making, and then running the whole suite at the end.&lt;/li&gt;
&lt;li&gt;Give them the ability to interactively test the code they are writing too. Notes on how to start a development server (for web projects) are useful, then you can have them use Playwright or curl to try things out.&lt;/li&gt;
&lt;li&gt;I'm having great results from maintaining a GitHub issues collection for projects and pasting URLs to issues directly into Claude Code.&lt;/li&gt;
&lt;li&gt;I actually don't think documentation is too important: LLMs can read the code a lot faster than you to figure out how to use it. I have comprehensive documentation across all of my projects but I don't think it's that helpful for the coding agents, though they are good at helping me spot if it needs updating.&lt;/li&gt;
&lt;li&gt;Linters, type checkers, auto-formatters - give coding agents helpful tools to run and they'll use them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the most part anything that makes a codebase easier for humans to maintain turns out to help agents as well.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Thought of another one: detailed error messages! If a manual or automated test fails the more information you can return back to the model the better, and stuffing extra data in the error message or assertion is a very inexpensive way to do that.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="ai"/><category term="pytest"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="coding-agents"/></entry><entry><title>TIL: Testing different Python versions with uv with-editable and uv-test</title><link href="https://simonwillison.net/2025/Oct/9/uv-test/#atom-tag" rel="alternate"/><published>2025-10-09T03:37:06+00:00</published><updated>2025-10-09T03:37:06+00:00</updated><id>https://simonwillison.net/2025/Oct/9/uv-test/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/python/uv-tests"&gt;TIL: Testing different Python versions with uv with-editable and uv-test&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
While tinkering with upgrading various projects to handle Python 3.14 I finally figured out a universal &lt;code&gt;uv&lt;/code&gt; recipe for running the tests for the current project in any specified version of Python:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run --python 3.14 --isolated --with-editable '.[test]' pytest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should work in any directory with a &lt;code&gt;pyproject.toml&lt;/code&gt; (or even a &lt;code&gt;setup.py&lt;/code&gt;) that defines a &lt;code&gt;test&lt;/code&gt; set of extra dependencies and uses &lt;code&gt;pytest&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--with-editable '.[test]'&lt;/code&gt; bit ensures that changes you make to that directory will be picked up by future test runs. The &lt;code&gt;--isolated&lt;/code&gt; flag ensures no other environments will affect your test run.&lt;/p&gt;
&lt;p&gt;I like this pattern so much I built a little shell script that uses it, &lt;a href="https://til.simonwillison.net/python/uv-tests#user-content-uv-test"&gt;shown here&lt;/a&gt;. Now I can change to any Python project directory and run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv-test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or for a different Python version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv-test -p 3.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I can pass additional &lt;code&gt;pytest&lt;/code&gt; options too:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv-test -p 3.11 -k permissions
&lt;/code&gt;&lt;/pre&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/til"&gt;til&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="testing"/><category term="pytest"/><category term="til"/><category term="uv"/></entry><entry><title>Making PyPI's test suite 81% faster</title><link href="https://simonwillison.net/2025/May/1/making-pypis-test-suite-81-faster/#atom-tag" rel="alternate"/><published>2025-05-01T21:32:18+00:00</published><updated>2025-05-01T21:32:18+00:00</updated><id>https://simonwillison.net/2025/May/1/making-pypis-test-suite-81-faster/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.trailofbits.com/2025/05/01/making-pypis-test-suite-81-faster/"&gt;Making PyPI&amp;#x27;s test suite 81% faster&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Fantastic collection of tips from Alexis Challande on speeding up a Python CI workflow.&lt;/p&gt;
&lt;p&gt;I've used &lt;a href="https://github.com/pytest-dev/pytest-xdist"&gt;pytest-xdist&lt;/a&gt; to run tests in parallel (across multiple cores) before, but the following tips were new to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;COVERAGE_CORE=sysmon pytest --cov=myproject&lt;/code&gt; tells &lt;a href="https://coverage.readthedocs.io/en/7.8.0/"&gt;coverage.py&lt;/a&gt; on Python 3.12 and higher to use the new &lt;a href="https://docs.python.org/3/library/sys.monitoring.html#module-sys.monitoring"&gt;sys.monitoring&lt;/a&gt; mechanism, which knocked their test execution time down from 58s to 27s.&lt;/li&gt;
&lt;li&gt;Setting &lt;code&gt;testpaths = ["tests/"]&lt;/code&gt; in &lt;code&gt;pytest.ini&lt;/code&gt; lets &lt;code&gt;pytest&lt;/code&gt; skip scanning other folders when trying to find tests.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python -X importtime ...&lt;/code&gt; shows a trace of exactly how long every package took to import. I could have done with this last week when I was trying to &lt;a href="https://github.com/simonw/llm/issues/949"&gt;debug slow LLM startup time&lt;/a&gt; which turned out to be caused be heavy imports.&lt;/li&gt;
&lt;/ul&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/1jb4l7/making_pypi_s_test_suite_81_faster"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="performance"/><category term="pypi"/><category term="python"/><category term="pytest"/></entry><entry><title>Smoke test your Django admin site</title><link href="https://simonwillison.net/2025/Mar/13/smoke-test-your-django-admin/#atom-tag" rel="alternate"/><published>2025-03-13T15:02:09+00:00</published><updated>2025-03-13T15:02:09+00:00</updated><id>https://simonwillison.net/2025/Mar/13/smoke-test-your-django-admin/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://jmduke.com/posts/post/django-admin-changelist-test/"&gt;Smoke test your Django admin site&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Justin Duke demonstrates a neat pattern for running simple tests against your internal Django admin site: introspect every admin route via &lt;code&gt;django.urls.get_resolver()&lt;/code&gt; and loop through them with &lt;code&gt;@pytest.mark.parametrize&lt;/code&gt; to check they all return a 200 HTTP status code.&lt;/p&gt;
&lt;p&gt;This catches simple mistakes with the admin configuration that trigger exceptions that might otherwise go undetected.&lt;/p&gt;
&lt;p&gt;I rarely write automated tests against my own admin sites and often feel guilty about it. I wrote &lt;a href="https://til.simonwillison.net/django/testing-django-admin-with-pytest"&gt;some notes&lt;/a&gt; on testing it with &lt;a href="https://pytest-django.readthedocs.io/en/latest/helpers.html#fixtures"&gt;pytest-django fixtures&lt;/a&gt; a few years ago.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-admin"&gt;django-admin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="django-admin"/><category term="python"/><category term="testing"/><category term="pytest"/></entry><entry><title>[red-knot] type inference/checking test framework</title><link href="https://simonwillison.net/2024/Oct/16/markdown-test-framework/#atom-tag" rel="alternate"/><published>2024-10-16T20:43:55+00:00</published><updated>2024-10-16T20:43:55+00:00</updated><id>https://simonwillison.net/2024/Oct/16/markdown-test-framework/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/astral-sh/ruff/pull/13636"&gt;[red-knot] type inference/checking test framework&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Ruff maintainer Carl Meyer recently landed an interesting new design for a testing framework. It's based on Markdown, and could be described as a form of "literate testing" - the testing equivalent of Donald Knuth's &lt;a href="https://en.wikipedia.org/wiki/Literate_programming"&gt;literate programming&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A markdown test file is a suite of tests, each test can contain one or more Python files, with optionally specified path/name. The test writes all files to an in-memory file system, runs red-knot, and matches the resulting diagnostics against &lt;code&gt;Type:&lt;/code&gt; and &lt;code&gt;Error:&lt;/code&gt; assertions embedded in the Python source as comments.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Test suites are Markdown documents with embedded fenced blocks that look &lt;a href="https://github.com/astral-sh/ruff/blob/2095ea83728d32959a435ab749acce48dfb76256/crates/red_knot_python_semantic/resources/mdtest/literal/float.md?plain=1#L5-L7"&gt;like this&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```py
reveal_type(1.0) # revealed: float
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tests can optionally include a &lt;code&gt;path=&lt;/code&gt; specifier, which can provide neater messages when reporting test failures:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```py path=branches_unify_to_non_union_type.py
def could_raise_returns_str() -&amp;gt; str:
    return 'foo'
...
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A larger example test suite can be browsed in the &lt;a href="https://github.com/astral-sh/ruff/tree/6282402a8cb44ac6362c6007fc911c3d75729648/crates/red_knot_python_semantic/resources/mdtest"&gt;red_knot_python_semantic/resources/mdtest&lt;/a&gt; directory.&lt;/p&gt;
&lt;p&gt;This document &lt;a href="https://github.com/astral-sh/ruff/blob/main/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md"&gt;on control flow for exception handlers&lt;/a&gt; (from &lt;a href="https://github.com/astral-sh/ruff/pull/13729"&gt;this PR&lt;/a&gt;) is the best example I've found of detailed prose documentation to accompany the tests.&lt;/p&gt;
&lt;p&gt;The system is implemented in Rust, but it's easy to imagine an alternative version of this idea written in Python as a &lt;code&gt;pytest&lt;/code&gt; plugin. This feels like an evolution of the old Python &lt;a href="https://docs.python.org/3/library/doctest.html"&gt;doctest&lt;/a&gt; idea, except that tests are embedded directly in Markdown rather than being embedded in Python code docstrings.&lt;/p&gt;
&lt;p&gt;... and it looks like such plugins exist already. Here are two that I've found so far:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/modal-labs/pytest-markdown-docs"&gt;pytest-markdown-docs&lt;/a&gt; by Elias Freider and Modal Labs.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sphinx-doc.org/en/master/usage/extensions/doctest.html"&gt;sphinx.ext.doctest&lt;/a&gt; is a core Sphinx extension for running test snippets in documentation.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/scientific-python/pytest-doctestplus"&gt;pytest-doctestplus&lt;/a&gt; from the Scientific Python community, first released in 2011.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I tried &lt;code&gt;pytest-markdown-docs&lt;/code&gt; by creating a &lt;code&gt;doc.md&lt;/code&gt; file like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Hello test doc

```py
assert 1 + 2 == 3
```

But this fails:

```py
assert 1 + 2 == 4
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then running it with &lt;a href="https://docs.astral.sh/uv/guides/tools/"&gt;uvx&lt;/a&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx --with pytest-markdown-docs pytest --markdown-docs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I got one pass and one fail:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_______ docstring for /private/tmp/doc.md __________
Error in code block:
```
10   assert 1 + 2 == 4
11   
```
Traceback (most recent call last):
  File "/private/tmp/tt/doc.md", line 10, in &amp;lt;module&amp;gt;
    assert 1 + 2 == 4
AssertionError

============= short test summary info ==============
FAILED doc.md::/private/tmp/doc.md
=========== 1 failed, 1 passed in 0.02s ============
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also &lt;a href="https://twitter.com/exhaze/status/1846675911225364742"&gt;just learned&lt;/a&gt; that the venerable Python &lt;code&gt;doctest&lt;/code&gt; standard library module has the ability to &lt;a href="https://docs.python.org/3/library/doctest.html#simple-usage-checking-examples-in-a-text-file"&gt;run tests in documentation files&lt;/a&gt; too, with &lt;code&gt;doctest.testfile("example.txt")&lt;/code&gt;: "The file content is treated as if it were a single giant docstring; the file doesn’t need to contain a Python program!"

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/charliermarsh/status/1846544708480168229"&gt;Charlie Marsh&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markdown"&gt;markdown&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruff"&gt;ruff&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/astral"&gt;astral&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/donald-knuth"&gt;donald-knuth&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="testing"/><category term="markdown"/><category term="rust"/><category term="pytest"/><category term="ruff"/><category term="uv"/><category term="astral"/><category term="donald-knuth"/></entry><entry><title>An LLM TDD loop</title><link href="https://simonwillison.net/2024/Oct/13/an-llm-tdd-loop/#atom-tag" rel="alternate"/><published>2024-10-13T19:37:47+00:00</published><updated>2024-10-13T19:37:47+00:00</updated><id>https://simonwillison.net/2024/Oct/13/an-llm-tdd-loop/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://codeinthehole.com/tips/llm-tdd-loop-script/"&gt;An LLM TDD loop&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Super neat demo by David Winterbottom, who wrapped my &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; and &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; tools in &lt;a href="https://gist.github.com/codeinthehole/d12af317a76b43423b111fd6d508c4fc"&gt;a short Bash script&lt;/a&gt; that can be fed a file full of Python unit tests and an empty implementation file and will then iterate on that file in a loop until the tests pass.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/codeinthehole/status/1845541873651274144"&gt;@codeinthehole&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="testing"/><category term="ai"/><category term="pytest"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="files-to-prompt"/></entry><entry><title>Python Developers Survey 2023 Results</title><link href="https://simonwillison.net/2024/Sep/3/python-developers-survey-2023/#atom-tag" rel="alternate"/><published>2024-09-03T02:47:45+00:00</published><updated>2024-09-03T02:47:45+00:00</updated><id>https://simonwillison.net/2024/Sep/3/python-developers-survey-2023/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://lp.jetbrains.com/python-developers-survey-2023/"&gt;Python Developers Survey 2023 Results&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The seventh annual Python survey is out. Here are the things that caught my eye or that I found surprising:&lt;/p&gt;
&lt;p&gt;25% of survey respondents had been programming in Python for less than a year, and 33% had less than a year of professional experience.&lt;/p&gt;
&lt;p&gt;37% of Python developers reported contributing to open-source projects last year - a new question for the survey. This is delightfully high!&lt;/p&gt;
&lt;p&gt;6% of users are still using Python 2. The survey notes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Almost half of Python 2 holdouts are under 21 years old and a third are students. Perhaps courses are still using Python 2?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In web frameworks, Flask and Django neck and neck at 33% each, but &lt;a href="https://fastapi.tiangolo.com/"&gt;FastAPI&lt;/a&gt; is a close third at 29%! &lt;a href="https://www.starlette.io/"&gt;Starlette&lt;/a&gt; is at 6%, but that's an under-count because it's the basis for FastAPI.&lt;/p&gt;
&lt;p&gt;The most popular library in "other framework and libraries" was BeautifulSoup with 31%, then Pillow 28%, then &lt;a href="https://github.com/opencv/opencv-python"&gt;OpenCV-Python&lt;/a&gt; at 22% (wow!) and Pydantic at 22%. Tkinter had 17%. These numbers are all a surprise to me.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.pytest.org/en/stable/"&gt;pytest&lt;/a&gt; scores 52% for unit testing, &lt;code&gt;unittest&lt;/code&gt; from the standard library just 25%. I'm glad to see &lt;code&gt;pytest&lt;/code&gt; so widely used, it's my favourite testing tool across any programming language.&lt;/p&gt;
&lt;p&gt;The top cloud providers are AWS, then Google Cloud Platform, then Azure... but &lt;a href="https://www.pythonanywhere.com/"&gt;PythonAnywhere&lt;/a&gt; (11%) took fourth place just ahead of DigitalOcean (10%). And &lt;a href="https://www.alibabacloud.com/"&gt;Alibaba Cloud&lt;/a&gt; is a new entrant in sixth place (after Heroku) with 4%. Heroku's ending of its free plan dropped them from 14% in 2021 to 7% now.&lt;/p&gt;
&lt;p&gt;Linux and Windows equal at 55%, macOS is at 29%. This was one of many multiple-choice questions that could add up to more than 100%.&lt;/p&gt;
&lt;p&gt;In databases, SQLite usage was trending down - 38% in 2021 to 34% for 2023, but still in second place behind PostgreSQL, stable at 43%.&lt;/p&gt;
&lt;p&gt;The survey incorporates quotes from different Python experts responding to the numbers, it's worth &lt;a href="https://lp.jetbrains.com/python-developers-survey-2023/"&gt;reading through the whole thing&lt;/a&gt;.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://pyfound.blogspot.com/2024/08/python-developers-survey-2023-results.html"&gt;PSF news&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/surveys"&gt;surveys&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/psf"&gt;psf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pydantic"&gt;pydantic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/starlette"&gt;starlette&lt;/a&gt;&lt;/p&gt;



</summary><category term="open-source"/><category term="postgresql"/><category term="python"/><category term="sqlite"/><category term="surveys"/><category term="pytest"/><category term="psf"/><category term="pydantic"/><category term="starlette"/></entry><entry><title>Upgrading my cookiecutter templates to use python -m pytest</title><link href="https://simonwillison.net/2024/Aug/17/python-m-pytest/#atom-tag" rel="alternate"/><published>2024-08-17T05:12:47+00:00</published><updated>2024-08-17T05:12:47+00:00</updated><id>https://simonwillison.net/2024/Aug/17/python-m-pytest/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/python-lib/issues/9"&gt;Upgrading my cookiecutter templates to use python -m pytest&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Every now and then I get caught out by weird test failures when I run &lt;code&gt;pytest&lt;/code&gt; and it turns out I'm running the wrong installation of that tool, so my tests fail because that &lt;code&gt;pytest&lt;/code&gt; is executing in a different virtual environment from the one needed by the tests.&lt;/p&gt;
&lt;p&gt;The fix for this is easy: run &lt;code&gt;python -m pytest&lt;/code&gt; instead, which guarantees that you will run &lt;code&gt;pytest&lt;/code&gt; in the same environment as your currently active Python.&lt;/p&gt;
&lt;p&gt;Yesterday I went through and updated every one of my &lt;code&gt;cookiecutter&lt;/code&gt; templates (&lt;a href="https://github.com/simonw/python-lib"&gt;python-lib&lt;/a&gt;, &lt;a href="https://github.com/simonw/click-app"&gt;click-app&lt;/a&gt;, &lt;a href="https://github.com/simonw/datasette-plugin"&gt;datasette-plugin&lt;/a&gt;, &lt;a href="https://github.com/simonw/sqlite-utils-plugin"&gt;sqlite-utils-plugin&lt;/a&gt;, &lt;a href="https://github.com/simonw/llm-plugin"&gt;llm-plugin&lt;/a&gt;) to use this pattern in their READMEs and generated repositories instead, to help spread that better recipe a little bit further.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="python"/><category term="pytest"/><category term="cookiecutter"/></entry><entry><title>inline-snapshot</title><link href="https://simonwillison.net/2024/Apr/16/inline-snapshot/#atom-tag" rel="alternate"/><published>2024-04-16T16:04:25+00:00</published><updated>2024-04-16T16:04:25+00:00</updated><id>https://simonwillison.net/2024/Apr/16/inline-snapshot/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://15r10nk.github.io/inline-snapshot/"&gt;inline-snapshot&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I'm a big fan of snapshot testing, where expected values are captured the first time a test suite runs and then asserted against in future runs. It's a very productive way to build a robust test suite.&lt;/p&gt;
&lt;p&gt;inline-snapshot by Frank Hoffmann is a particularly neat implementation of the pattern. It defines a &lt;code&gt;snapshot()&lt;/code&gt; function which you can use in your tests:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;assert 1548 * 18489 == snapshot()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;When you run that test using &lt;code&gt;pytest --inline-snapshot=create&lt;/code&gt; the &lt;code&gt;snapshot()&lt;/code&gt; function will be replaced in your code (using AST manipulation) with itself wrapping the &lt;code&gt;repr()&lt;/code&gt; of the expected result:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;assert 1548 * 18489 == snapshot(28620972)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If you modify the code and need to update the tests you can run &lt;code&gt;pytest --inline-snapshot=fix&lt;/code&gt; to regenerate the recorded snapshot values.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="testing"/><category term="pytest"/></entry><entry><title>pytest-icdiff</title><link href="https://simonwillison.net/2023/Jun/3/pytest-icdiff/#atom-tag" rel="alternate"/><published>2023-06-03T16:59:24+00:00</published><updated>2023-06-03T16:59:24+00:00</updated><id>https://simonwillison.net/2023/Jun/3/pytest-icdiff/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/hjwp/pytest-icdiff"&gt;pytest-icdiff&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is neat: “pip install pytest-icdiff” provides an instant usability upgrade to the output of failed tests in pytest, especially if the assertions involve comparing larger strings or nested JSON objects.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://mastodon.social/@hynek/110479665200902390"&gt;@hynek&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="testing"/><category term="pytest"/></entry><entry><title>pyfakefs usage</title><link href="https://simonwillison.net/2023/Feb/1/pyfakefs-usage/#atom-tag" rel="alternate"/><published>2023-02-01T22:37:42+00:00</published><updated>2023-02-01T22:37:42+00:00</updated><id>https://simonwillison.net/2023/Feb/1/pyfakefs-usage/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://pytest-pyfakefs.readthedocs.io/en/latest/usage.html"&gt;pyfakefs usage&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New to me pytest fixture library that provides a really easy way to mock Python’s filesystem functions—open(), os.path.listdir() and so on—so a test can run against a fake set of files. This looks incredibly useful.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/"&gt;Luke Plant&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/luke-plant"&gt;luke-plant&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="luke-plant"/><category term="python"/><category term="testing"/><category term="pytest"/></entry><entry><title>mitsuhiko/insta</title><link href="https://simonwillison.net/2022/Oct/31/insta/#atom-tag" rel="alternate"/><published>2022-10-31T01:06:44+00:00</published><updated>2022-10-31T01:06:44+00:00</updated><id>https://simonwillison.net/2022/Oct/31/insta/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/mitsuhiko/insta"&gt;mitsuhiko/insta&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I asked for recommendations on Twitter for testing libraries in other languages that would give me the same level of delight that I get from pytest. Two people pointed me to insta by  Armin Ronacher, a Rust testing framework for “snapshot testing” which automatically records reference values to your repository, so future tests can spot if they change.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/david_raznick/status/1586885078457753602"&gt;@david_raznick&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/armin-ronacher"&gt;armin-ronacher&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="armin-ronacher"/><category term="testing"/><category term="rust"/><category term="pytest"/></entry><entry><title>Running C unit tests with pytest</title><link href="https://simonwillison.net/2022/Feb/12/running-c-unit-tests-with-pytest/#atom-tag" rel="alternate"/><published>2022-02-12T17:14:35+00:00</published><updated>2022-02-12T17:14:35+00:00</updated><id>https://simonwillison.net/2022/Feb/12/running-c-unit-tests-with-pytest/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://p403n1x87.github.io/running-c-unit-tests-with-pytest.html"&gt;Running C unit tests with pytest&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Brilliant, detailed tutorial by Gabriele Tornetta on testing C code using pytest, which also doubles up as a ctypes tutorial. There’s a lot of depth here—in addition to exercising C code through ctypes, Gabriele shows how to run each test in a separate process so that segmentation faults don’t fail the entire suite,  then adds code to run the compiler as part of the pytest run, and then shows how to use gdb trickery to generate more useful stack traces.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=30301880"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/c"&gt;c&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ctypes"&gt;ctypes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="c"/><category term="ctypes"/><category term="testing"/><category term="pytest"/></entry><entry><title>How I build a feature</title><link href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#atom-tag" rel="alternate"/><published>2022-01-12T18:10:17+00:00</published><updated>2022-01-12T18:10:17+00:00</updated><id>https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#atom-tag</id><summary type="html">
    &lt;p&gt;I'm maintaining &lt;a href="https://github.com/simonw/simonw/blob/main/releases.md"&gt;a lot of different projects&lt;/a&gt; at the moment. I thought it would be useful to describe the process I use for adding a new feature to one of them, using the new &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-create-database"&gt;sqlite-utils create-database&lt;/a&gt; command as an example.&lt;/p&gt;
&lt;p&gt;I like each feature to be represented by what I consider to be the &lt;strong&gt;perfect commit&lt;/strong&gt; - one that bundles together the implementation, the tests, the documentation and a link to an external issue thread.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 29th October 2022:&lt;/strong&gt; I wrote &lt;a href="https://simonwillison.net/2022/Oct/29/the-perfect-commit/"&gt;more about the perfect commit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sqlite-utils create-database&lt;/code&gt; command is very simple: it creates a new, empty SQLite database file. You use it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% sqlite-utils create-database empty.db
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#everything-starts-with-an-issue"&gt;Everything starts with an issue&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#development-environment"&gt;Development environment&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#automated-tests"&gt;Automated tests&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#implementing-the-feature"&gt;Implementing the feature&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#code-formatting-with-black"&gt;Code formatting with Black&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#linting"&gt;Linting&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#documentation"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#committing-the-change"&gt;Committing the change&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#branches-and-pull-requests"&gt;Branches and pull requests&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#release-notes-and-a-release"&gt;Release notes, and a release&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#a-live-demo"&gt;A live demo&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#tell-the-world-about-it"&gt;Tell the world about it&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/#more-examples-of-this-pattern"&gt;More examples of this pattern&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="everything-starts-with-an-issue"&gt;Everything starts with an issue&lt;/h4&gt;
&lt;p&gt;Every piece of work I do has an associated issue. This acts as ongoing work-in-progress notes and lets me record decisions, reference any research, drop in code snippets and sometimes even add screenshots and video - stuff that is really helpful but doesn't necessarily fit in code comments or commit messages.&lt;/p&gt;
&lt;p&gt;Even if it's a tiny improvement that's only a few lines of code, I'll still open an issue for it - sometimes just a few minutes before closing it again as complete.&lt;/p&gt;
&lt;p&gt;Any commits that I create that relate to an issue reference the issue number in their commit message. GitHub does a great job of automatically linking these together, bidirectionally so I can navigate from the commit to the issue or from the issue to the commit.&lt;/p&gt;
&lt;p&gt;Having an issue also gives me something I can link to from my release notes.&lt;/p&gt;
&lt;p&gt;In the case of the &lt;code&gt;create-database&lt;/code&gt; command, I opened &lt;a href="https://github.com/simonw/sqlite-utils/issues/348"&gt;this issue&lt;/a&gt; in November when I had the idea for the feature.&lt;/p&gt;
&lt;p&gt;I didn't do the work until over a month later - but because I had designed the feature in the issue comments I could get started on the implementation really quickly.&lt;/p&gt;
&lt;h4 id="development-environment"&gt;Development environment&lt;/h4&gt;
&lt;p&gt;Being able to quickly spin up a development environment for a project is crucial. All of my projects have a section in the README or the documentation describing how to do this - here's &lt;a href="https://sqlite-utils.datasette.io/en/stable/contributing.html"&gt;that section for sqlite-utils&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;On my own laptop each project gets a directory, and I use &lt;code&gt;pipenv shell&lt;/code&gt; in that directory to activate a directory-specific virtual environment, then &lt;code&gt;pip install -e '.[test]'&lt;/code&gt; to install the dependencies and test dependencies.&lt;/p&gt;
&lt;h4 id="automated-tests"&gt;Automated tests&lt;/h4&gt;
&lt;p&gt;All of my features are accompanied by automated tests. This gives me the confidence to boldly make changes to the software in the future without fear of breaking any existing features.&lt;/p&gt;
&lt;p&gt;This means that writing tests needs to be as quick and easy as possible - the less friction here the better.&lt;/p&gt;
&lt;p&gt;The best way to make writing tests easy is to have a great testing framework in place from the very beginning of the project. My cookiecutter templates (&lt;a href="https://github.com/simonw/python-lib"&gt;python-lib&lt;/a&gt;, &lt;a href="https://github.com/simonw/datasette-plugin"&gt;datasette-plugin&lt;/a&gt; and &lt;a href="https://github.com/simonw/click-app"&gt;click-app&lt;/a&gt;) all configure &lt;a href="https://docs.pytest.org/"&gt;pytest&lt;/a&gt; and add a &lt;code&gt;tests/&lt;/code&gt; folder with a single passing test, to give me something to start adding tests to.&lt;/p&gt;
&lt;p&gt;I can't say enough good things about pytest. Before I adopted it, writing tests was a chore. Now it's an activity I genuinely look forward to!&lt;/p&gt;
&lt;p&gt;I'm not a religious adherent to writing the tests first - see &lt;a href="https://simonwillison.net/2020/Feb/11/cheating-at-unit-tests-pytest-black/"&gt;How to cheat at unit tests with pytest and Black&lt;/a&gt; for more thoughts on that - but I'll write the test first if it's pragmatic to do so.&lt;/p&gt;
&lt;p&gt;In the case of &lt;code&gt;create-database&lt;/code&gt;, writing the test first felt like the right thing to do. Here's the test I started with:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;test_create_database&lt;/span&gt;(&lt;span class="pl-s1"&gt;tmpdir&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;db_path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;tmpdir&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-s"&gt;"test.db"&lt;/span&gt;
    &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;db_path&lt;/span&gt;.&lt;span class="pl-en"&gt;exists&lt;/span&gt;()
    &lt;span class="pl-s1"&gt;result&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;CliRunner&lt;/span&gt;().&lt;span class="pl-en"&gt;invoke&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;cli&lt;/span&gt;.&lt;span class="pl-s1"&gt;cli&lt;/span&gt;, [&lt;span class="pl-s"&gt;"create-database"&lt;/span&gt;, &lt;span class="pl-en"&gt;str&lt;/span&gt;(&lt;span class="pl-s1"&gt;db_path&lt;/span&gt;)]
    )
    &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;exit_code&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;
    &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s1"&gt;db_path&lt;/span&gt;.&lt;span class="pl-en"&gt;exists&lt;/span&gt;()&lt;/pre&gt;
&lt;p&gt;This test uses the &lt;a href="https://docs.pytest.org/en/6.2.x/tmpdir.html#the-tmpdir-fixture"&gt;tmpdir pytest fixture&lt;/a&gt; to provide a temporary directory that will be automatically cleaned up by pytest after the test run finishes.&lt;/p&gt;
&lt;p&gt;It checks that the &lt;code&gt;test.db&lt;/code&gt; file doesn't exist yet, then uses the Click framework's &lt;a href="https://click.palletsprojects.com/en/8.0.x/testing/"&gt;CliRunner utility&lt;/a&gt; to execute the create-database command. Then it checks that the command didn't throw an error and that the file has been created.&lt;/p&gt;
&lt;p&gt;The I run the test, and watch it fail - because I haven't built the feature yet!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% pytest -k test_create_database

============ test session starts ============
platform darwin -- Python 3.8.2, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /Users/simon/Dropbox/Development/sqlite-utils
plugins: cov-2.12.1, hypothesis-6.14.5
collected 808 items / 807 deselected / 1 selected                           

tests/test_cli.py F                                                   [100%]

================= FAILURES ==================
___________ test_create_database ____________

tmpdir = local('/private/var/folders/wr/hn3206rs1yzgq3r49bz8nvnh0000gn/T/pytest-of-simon/pytest-659/test_create_database0')

    def test_create_database(tmpdir):
        db_path = tmpdir / "test.db"
        assert not db_path.exists()
        result = CliRunner().invoke(
            cli.cli, ["create-database", str(db_path)]
        )
&amp;gt;       assert result.exit_code == 0
E       assert 1 == 0
E        +  where 1 = &amp;lt;Result SystemExit(1)&amp;gt;.exit_code

tests/test_cli.py:2097: AssertionError
========== short test summary info ==========
FAILED tests/test_cli.py::test_create_database - assert 1 == 0
===== 1 failed, 807 deselected in 0.99s ====
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-k&lt;/code&gt; option lets me run any test that match the search string, rather than running the full test suite. I use this all the time.&lt;/p&gt;
&lt;p&gt;Other pytest features I often use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pytest -x&lt;/code&gt;: runs the entire test suite but quits at the first test that fails&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pytest --lf&lt;/code&gt;: re-runs any tests that failed during the last test run&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pytest --pdb -x&lt;/code&gt;: open the Python debugger at the first failed test (omit the &lt;code&gt;-x&lt;/code&gt; to open it at every failed test). This is the main way I interact with the Python debugger. I often use this to help write the tests, since I can add &lt;code&gt;assert False&lt;/code&gt; and get a shell inside the test to interact with various objects and figure out how to best run assertions against them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="implementing-the-feature"&gt;Implementing the feature&lt;/h4&gt;
&lt;p&gt;Test in place, it's time to implement the command. I added this code to my existing &lt;a href="https://github.com/simonw/sqlite-utils/blob/3.20/sqlite_utils/cli.py"&gt;cli.py module&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;cli&lt;/span&gt;.&lt;span class="pl-en"&gt;command&lt;/span&gt;(&lt;span class="pl-s1"&gt;name&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"create-database"&lt;/span&gt;)&lt;/span&gt;
&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;argument&lt;/span&gt;(&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s"&gt;"path"&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;type&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-v"&gt;Path&lt;/span&gt;(&lt;span class="pl-s1"&gt;file_okay&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;, &lt;span class="pl-s1"&gt;dir_okay&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;, &lt;span class="pl-s1"&gt;allow_dash&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;),&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;required&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;)&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;create_database&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;):
    &lt;span class="pl-s"&gt;"Create a new empty database file."&lt;/span&gt;
    &lt;span class="pl-s1"&gt;db&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;sqlite_utils&lt;/span&gt;.&lt;span class="pl-v"&gt;Database&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;vacuum&lt;/span&gt;()&lt;/pre&gt;
&lt;p&gt;(I happen to know that the quickest way to create an empty SQLite database file is to run &lt;code&gt;VACUUM&lt;/code&gt; against it.)&lt;/p&gt;
&lt;p&gt;The test now passes!&lt;/p&gt;
&lt;p&gt;I iterated on this implementation a little bit more, to add the &lt;code&gt;--enable-wal&lt;/code&gt; option I had designed &lt;a href="https://github.com/simonw/sqlite-utils/issues/348#issuecomment-983120066"&gt;in the issue comments&lt;/a&gt; - and updated the test to match. You can see the final implementation in this commit: &lt;a href="https://github.com/simonw/sqlite-utils/commit/1d64cd2e5b402ff957f9be2d9bb490d313c73989"&gt;1d64cd2e5b402ff957f9be2d9bb490d313c73989&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If I add a new test and it passes the first time, I’m always suspicious of it. I’ll deliberately break the test (change a 1 to a 2 for example) and run it again to make sure it fails, then change it back again.&lt;/p&gt;
&lt;h4 id="code-formatting-with-black"&gt;Code formatting with Black&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/psf/black"&gt;Black&lt;/a&gt; has increased my productivity as a Python developer by a material amount. I used to spend a whole bunch of brain cycles agonizing over how to indent my code, where to break up long function calls and suchlike. Thanks to Black I never think about this at all - I instinctively run &lt;code&gt;black .&lt;/code&gt; in the root of my project and accept whatever style decisions it applies for me.&lt;/p&gt;
&lt;h4 id="linting"&gt;Linting&lt;/h4&gt;
&lt;p&gt;I have a few linters set up to run on every commit. I can run these locally too - how to do that is &lt;a href="https://sqlite-utils.datasette.io/en/stable/contributing.html#linting-and-formatting"&gt;documented here&lt;/a&gt; - but I'm often a bit lazy and leave them to &lt;a href="https://github.com/simonw/sqlite-utils/blob/main/.github/workflows/test.yml"&gt;run in CI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In this case one of my linters failed! I accidentally called the new command function &lt;code&gt;create_table()&lt;/code&gt; when it should have been called &lt;code&gt;create_database()&lt;/code&gt;. The code worked fine due to how the &lt;code&gt;cli.command(name=...)&lt;/code&gt; decorator works but &lt;code&gt;mypy&lt;/code&gt; &lt;a href="https://github.com/simonw/sqlite-utils/runs/4754944593?check_suite_focus=true"&gt;complained about&lt;/a&gt; the redefined function name. I fixed that in &lt;a href="https://github.com/simonw/sqlite-utils/commit/2f8879235afc6a06a8ae25ded1b2fe289ad8c3a6#diff-76294b3d4afeb27e74e738daa01c26dd4dc9ccb6f4477451483a2ece1095902e"&gt;a separate commit&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="documentation"&gt;Documentation&lt;/h4&gt;
&lt;p&gt;My policy these days is that if a feature isn't documented it doesn't exist. Updating existing documentation isn't much work at all if the documentation already exists, and over time these incremental improvements add up to something really comprehensive.&lt;/p&gt;
&lt;p&gt;For smaller projects I use a single &lt;code&gt;README.md&lt;/code&gt; which gets displayed on both GitHub and PyPI (and the Datasette website too, for example on &lt;a href="https://datasette.io/tools/git-history"&gt;datasette.io/tools/git-history&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;My larger projects, such as &lt;a href="https://docs.datasette.io/"&gt;Datasette&lt;/a&gt; and &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt;, use &lt;a href="https://readthedocs.org/"&gt;Read the Docs&lt;/a&gt; and &lt;a href="https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html"&gt;reStructuredText&lt;/a&gt; with &lt;a href="https://www.sphinx-doc.org/"&gt;Sphinx&lt;/a&gt; instead.&lt;/p&gt;
&lt;p&gt;I like reStructuredText mainly because it has really good support for internal reference links - something that is missing from Markdown, though it can be enabled using &lt;a href="https://myst-parser.readthedocs.io"&gt;MyST&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; uses Sphinx. I have the &lt;a href="https://github.com/executablebooks/sphinx-autobuild"&gt;sphinx-autobuild&lt;/a&gt; extension configured, which means I can run a live reloading server with the documentation like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd docs
make livehtml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any time I'm working on the documentation I have that server running, so I can hit "save" in VS Code and see a preview in my browser a few seconds later.&lt;/p&gt;
&lt;p&gt;For Markdown documentation I use the VS Code preview pane directly.&lt;/p&gt;
&lt;p&gt;The moment the documentation is live online, I like to add a link to it in a comment on the issue thread.&lt;/p&gt;
&lt;h4 id="committing-the-change"&gt;Committing the change&lt;/h4&gt;
&lt;p&gt;I run &lt;code&gt;git diff&lt;/code&gt; a LOT while hacking on code, to make sure I haven’t accidentally changed something unrelated. This also helps spot things like rogue &lt;code&gt;print()&lt;/code&gt; debug statements I may have added.&lt;/p&gt;
&lt;p&gt;Before my final commit, I sometimes even run &lt;code&gt;git diff | grep print&lt;/code&gt; to check for those.&lt;/p&gt;
&lt;p&gt;My goal with the commit is to bundle the test, documentation and implementation. If those are the only files I've changed I do this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git commit -a -m "sqlite-utils create-database command, closes #348"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this completes the work on the issue I use "&lt;code&gt;closes #N&lt;/code&gt;", which causes GitHub to close the issue for me. If it's not yet ready to close I use "&lt;code&gt;refs #N&lt;/code&gt;" instead.&lt;/p&gt;
&lt;p&gt;Sometimes there will be unrelated changes in my working directory. If so, I use &lt;code&gt;git add &amp;lt;files&amp;gt;&lt;/code&gt; and then commit just with &lt;code&gt;git commit -m message&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="branches-and-pull-requests"&gt;Branches and pull requests&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;create-database&lt;/code&gt; is a good example of a feature that can be implemented in a single commit, with no need to work in a branch.&lt;/p&gt;
&lt;p&gt;For larger features, I'll work in a feature branch:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b my-feature
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'll make a commit (often just labelled "WIP prototype, refs #N") and then push that to GitHub and open a pull request for it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push -u origin my-feature 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I ensure the new pull request links back to the issue in its description, then switch my ongoing commentary to comments on the pull request itself.&lt;/p&gt;
&lt;p&gt;I'll sometimes add a task checklist to the opening comment on the pull request, since tasks there get reflected in the GitHub UI anywhere that links to the PR. Then I'll check those off as I complete them.&lt;/p&gt;
&lt;p&gt;An example of a PR I used like this is &lt;a href="https://github.com/simonw/sqlite-utils/pull/361"&gt;#361: --lines and --text and --convert and --import&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I don't like merge commits - I much prefer to keep my &lt;code&gt;main&lt;/code&gt; branch history as linear as possible. I usually merge my PRs through the GitHub web interface using the squash feature, which results in a single, clean commit to main with the combined tests, documentation and implementation. Occasionally I will see value in keeping the individual commits, in which case I will rebase merge them.&lt;/p&gt;
&lt;p&gt;Another goal here is to keep the &lt;code&gt;main&lt;/code&gt; branch releasable at all times. Incomplete work should stay in a branch. This makes turning around and releasing quick bug fixes a lot less stressful!&lt;/p&gt;
&lt;h4 id="release-notes-and-a-release"&gt;Release notes, and a release&lt;/h4&gt;
&lt;p&gt;A feature isn't truly finished until it's been released to &lt;a href="https://pypi.org/"&gt;PyPI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All of my projects are configured the same way: they use GitHub releases to trigger a GitHub Actions workflow which publishes the new release to PyPI. The &lt;code&gt;sqlite-utils&lt;/code&gt; workflow for that &lt;a href="https://github.com/simonw/sqlite-utils/blob/main/.github/workflows/publish.yml"&gt;is here in publish.yml&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://cookiecutter.readthedocs.io/"&gt;cookiecutter&lt;/a&gt; templates for new projects set up this workflow for me. I just need to create a PyPI token for the project and assign it as a repository secret. See the &lt;a href="https://github.com/simonw/python-lib"&gt;python-lib cookiecutter README&lt;/a&gt; for details.&lt;/p&gt;
&lt;p&gt;To push out a new release, I need to increment the version number in &lt;a href="https://github.com/simonw/sqlite-utils/blob/main/setup.py"&gt;setup.py&lt;/a&gt; and write the release notes.&lt;/p&gt;
&lt;p&gt;I use &lt;a href="https://semver.org/"&gt;semantic versioning&lt;/a&gt; - a new feature is a minor version bump, a breaking change is a major version bump (I try very hard to avoid these) and a bug fix or documentation-only update is a patch increment.&lt;/p&gt;
&lt;p&gt;Since &lt;code&gt;create-database&lt;/code&gt; was a new feature, it went out in &lt;a href="https://github.com/simonw/sqlite-utils/releases/3.21"&gt;release 3.21&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My projects that use Sphinx for documentation have &lt;a href="https://github.com/simonw/sqlite-utils/blob/main/docs/changelog.rst"&gt;changelog.rst&lt;/a&gt; files in their repositories. I add the release notes there, linking to the relevant issues and cross-referencing the new documentation. Then I ship a commit that bundles the release notes with the bumped version number, with a commit message that looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git commit -m "Release 3.21

Refs #348, #364, #366, #368, #371, #372, #374, #375, #376, #379"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/sqlite-utils/commit/7c637b11805adc3d3970076a7ba6afe8e34b371e"&gt;the commit for release 3.21&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Referencing the issue numbers in the release automatically adds a note to their issue threads indicating the release that they went out in.&lt;/p&gt;
&lt;p&gt;I generate that list of issue numbers by pasting the release notes into an Observable notebook I built for the purpose: &lt;a href="https://observablehq.com/@simonw/extract-issue-numbers-from-pasted-text"&gt;Extract issue numbers from pasted text&lt;/a&gt;. Observable is really great for building this kind of tiny interactive utility.&lt;/p&gt;
&lt;p&gt;For projects that just have a README I write the release notes in Markdown and paste them directly into the GitHub "new release" form.&lt;/p&gt;
&lt;p&gt;I like to duplicate the release notes to GiHub releases for my Sphinx changelog projects too. This is mainly so the &lt;a href="https://datasette.io/"&gt;datasette.io&lt;/a&gt; website will display the release notes on its homepage, which is populated &lt;a href="https://simonwillison.net/2020/Dec/13/datasette-io/"&gt;at build time&lt;/a&gt; using the GitHub GraphQL API.&lt;/p&gt;
&lt;p&gt;To convert my reStructuredText to Markdown I copy and paste the rendered HTML into this brilliant &lt;a href="https://euangoddard.github.io/clipboard2markdown/"&gt;Paste to Markdown&lt;/a&gt; tool by &lt;a href="https://github.com/euangoddard"&gt;Euan Goddard&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="a-live-demo"&gt;A live demo&lt;/h4&gt;
&lt;p&gt;When possible, I like to have a live demo that I can link to.&lt;/p&gt;
&lt;p&gt;This is easiest for features in Datasette core. Datesette’s main branch gets &lt;a href="https://github.com/simonw/datasette/blob/0.60a1/.github/workflows/deploy-latest.yml#L51-L73"&gt;deployed automatically&lt;/a&gt; to &lt;a href="https://latest.datasette.io/"&gt;latest.datasette.io&lt;/a&gt; so I can often link to a demo there.&lt;/p&gt;
&lt;p&gt;For Datasette plugins, I’ll deploy a fresh instance with the plugin (e.g. &lt;a href="https://datasette-graphql-demo.datasette.io/"&gt;this one for datasette-graphql&lt;/a&gt;) or (more commonly) add it to my big &lt;a href="https://latest-with-plugins.datasette.io/"&gt;latest-with-plugins.datasette.io&lt;/a&gt; instance - which tries to demonstrate what happens to Datasette if you install dozens of plugins at once (so far it works OK).&lt;/p&gt;
&lt;p&gt;Here’s a demo of the &lt;a href="https://datasette.io/plugins/datasette-copyable"&gt;datasette-copyable plugin&lt;/a&gt; running there:  &lt;a href="https://latest-with-plugins.datasette.io/github/commits.copyable"&gt;https://latest-with-plugins.datasette.io/github/commits.copyable&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="tell-the-world-about-it"&gt;Tell the world about it&lt;/h4&gt;
&lt;p&gt;The last step is to tell the world (beyond the people who meticulously read the release notes) about the new feature.&lt;/p&gt;
&lt;p&gt;Depending on the size of the feature, I might do this with a tweet &lt;a href="https://twitter.com/simonw/status/1455266746701471746"&gt;like this one&lt;/a&gt; - usually with a screenshot and a link to the documentation. I often extend this into a short Twitter thread, which gives me a chance to link to related concepts and demos or add more screenshots.&lt;/p&gt;
&lt;p&gt;For larger or more interesting feature I'll blog about them. I may save this for my weekly &lt;a href="https://simonwillison.net/tags/weeknotes/"&gt;weeknotes&lt;/a&gt;, but sometimes for particularly exciting features I'll write up a dedicated blog entry. Some examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2020/Sep/23/sqlite-advanced-alter-table/"&gt;Executing advanced ALTER TABLE operations in SQLite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2020/Jul/30/fun-binary-data-and-sqlite/"&gt;Fun with binary data and SQLite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2020/Sep/23/sqlite-utils-extract/"&gt;Refactoring databases with sqlite-utils extract&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2021/Jun/19/sqlite-utils-memory/"&gt;Joining CSV and JSON data with an in-memory SQLite database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2021/Aug/6/sqlite-utils-convert/"&gt;Apply conversion functions to data in SQLite columns with the sqlite-utils CLI tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I may even assemble a full set of &lt;a href="https://simonwillison.net/tags/annotatedreleasenotes/"&gt;annotated release notes&lt;/a&gt; on my blog, where I quote each item from the release in turn and provide some fleshed out examples plus background information on why I built it.&lt;/p&gt;
&lt;p&gt;If it’s a new Datasette (or Datasette-adjacent) feature, I’ll try to remember to write about it in the next edition of the &lt;a href="https://datasette.substack.com/"&gt;Datasette Newsletter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, if I learned a new trick while building a feature I might extract that into &lt;a href="https://til.simonwillison.net/"&gt;a TIL&lt;/a&gt;. If I do that I'll link to the new TIL from the issue thread.&lt;/p&gt;
&lt;h4 id="more-examples-of-this-pattern"&gt;More examples of this pattern&lt;/h4&gt;
&lt;p&gt;Here are a bunch of examples of commits that implement this pattern, combining the tests, implementation and documentation into a single unit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sqlite-utils: &lt;a href="https://github.com/simonw/sqlite-utils/commit/324ebc31308752004fe5f7e4941fc83706c5539c"&gt;adding —limit and —offset to sqlite-utils rows&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;sqlite-utils: &lt;a href="https://github.com/simonw/sqlite-utils/commit/d83b2568131f2b1cc01228419bb08c96d843d65d"&gt;--where and -p options for sqlite-utils convert&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;s3-credentials: &lt;a href="https://github.com/simonw/s3-credentials/commit/905258379817e8b458528e4ccc5e6cc2c8cf4352"&gt;s3-credentials policy command&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;datasette: &lt;a href="https://github.com/simonw/datasette/commit/5cadc244895fc47e0534c6e90df976d34293921e"&gt;db.execute_write_script() and db.execute_write_many()&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;datasette: &lt;a href="https://github.com/simonw/datasette/commit/992496f2611a72bd51e94bfd0b17c1d84e732487"&gt;?_nosuggest=1 parameter for table views&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;datasette-graphql: &lt;a href="https://github.com/simonw/datasette-graphql/commit/2d8c042e93e3429c5b187121d26f8817997073dd"&gt;GraphQL execution limits: time_limit_ms and num_queries_limit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/git"&gt;git&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/software-engineering"&gt;software-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/black"&gt;black&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/read-the-docs"&gt;read-the-docs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-issues"&gt;github-issues&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="git"/><category term="github"/><category term="software-engineering"/><category term="testing"/><category term="pytest"/><category term="black"/><category term="read-the-docs"/><category term="github-issues"/></entry><entry><title>PAGNIs: Probably Are Gonna Need Its</title><link href="https://simonwillison.net/2021/Jul/1/pagnis/#atom-tag" rel="alternate"/><published>2021-07-01T19:13:58+00:00</published><updated>2021-07-01T19:13:58+00:00</updated><id>https://simonwillison.net/2021/Jul/1/pagnis/#atom-tag</id><summary type="html">
    &lt;p&gt;Luke Page has a great post up with &lt;a href="https://lukeplant.me.uk/blog/posts/yagni-exceptions/"&gt;his list of YAGNI exceptions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;YAGNI - You Ain't Gonna Need It - is a rule that says you shouldn't add a feature just because it might be useful in the future - only write code when it solves a direct problem.&lt;/p&gt;
&lt;p&gt;When should you over-ride YAGNI? When the cost of adding something later is so dramatically expensive compared with the cost of adding it early on that it's worth taking the risk. On when you know from experience that an initial investment will pay off many times over.&lt;/p&gt;
&lt;p&gt;Lukes's exceptions to YAGNI are well chosen: things like logging, API versioning, created_at timestamps and a bias towards "store multiple X for a user" (a many-to-many relationship) if there's any inkling that the system may need to support more than one.&lt;/p&gt;
&lt;p&gt;Because I like attempting to coin phrases, I propose we call these &lt;strong&gt;PAGNIs&lt;/strong&gt; - short for &lt;strong&gt;Probably Are Gonna Need Its&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here are some of mine.&lt;/p&gt;
&lt;h4&gt;A kill-switch for your mobile apps&lt;/h4&gt;
&lt;p&gt;If you're building a mobile app that talks to your API, make sure to ship a kill-switch: a mechanism by which you can cause older versions of the application to show a "you must upgrade to continue using this application" screen when the app starts up.&lt;/p&gt;
&lt;p&gt;In an ideal world, you'll never use this ability: you'll continue to build new features to the app and make backwards-compatible changes to the API forever, such that ancient app versions keep working and new app versions get to do new things.&lt;/p&gt;
&lt;p&gt;But... sometimes that simply isn't possible. You might discover a security hole in the design of the application or API that can only be fixed by breaking backwards-compatibility - or maybe you're still maintaining a v1 API from five years ago to support a mobile application version that's only still installed by 30 users, and you'd like to not have to maintain double the amount of API code.&lt;/p&gt;
&lt;p&gt;You can't add a kill-switch retroactively to apps that have already been deployed!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://twitter.com/myunderpants/status/1410655652867809281"&gt;Apparently Firebase offers this&lt;/a&gt; to many Android apps, but if you're writing for iOS you need to provide this yourself.&lt;/p&gt;
&lt;h4&gt;Automated deploys&lt;/h4&gt;
&lt;p&gt;Nothing kills a side project like coming back to it in six months time and having to figure out how to deploy it again. Thanks to &lt;a href="https://simonwillison.net/tags/githubactions/"&gt;GitHub Actions&lt;/a&gt; and hosting providers like Google Cloud Run, Vercel, Heroku and Netlify setting up automated deployments is way easier now than it used to be. I have enough examples now that getting automated deployments working for a new project usually only takes a few minutes, and it pays off instantly.&lt;/p&gt;
&lt;h4&gt;Continuous Integration (and a test framework)&lt;/h4&gt;
&lt;p&gt;Similar to automated deployment in that GitHub Actions (and Circle CI and Travis before it) make this much less painful to setup than it used to be.&lt;/p&gt;
&lt;p&gt;Introducing a test framework to an existing project can be extremely painful. Introducing it at the very start is easy - and it sets a precedent that code should be tested from day one.&lt;/p&gt;
&lt;p&gt;These days I'm all about &lt;a href="https://simonwillison.net/tags/pytest/"&gt;pytest&lt;/a&gt;, and I have various cookiecutter templates (&lt;a href="https://github.com/simonw/datasette-plugin"&gt;datasette-plugin&lt;/a&gt;, &lt;a href="https://github.com/simonw/click-app"&gt;click-app&lt;/a&gt;, &lt;a href="https://github.com/simonw/python-lib"&gt;python-lib&lt;/a&gt;) that configure it on my new projects (with a passing test) out of the box.&lt;/p&gt;
&lt;p&gt;(Honestly, at this point in my career I consider continuous integration a DAGNI - Definitely Are Gonna Need It.)&lt;/p&gt;
&lt;p&gt;One particularly worthwhile trick is making sure the tests can spin up their own isolated test databases - another thing which is pretty easy to setup early (Django does this for you) and harder to add later on. I extend that to other external data stores - I once put a significant amount of effort into setting up a mechanism for running tests against Elasticsearch and clearing out the data again afterwards, and it paid off multiple times over.&lt;/p&gt;
&lt;p&gt;Even better: &lt;strong&gt;continuous deployment&lt;/strong&gt;! When the tests pass, deploy. If you have automated deployment setup already adding this is pretty easy, and doing it from the very start of a project sets a strong cultural expectation that no-one will land code to the &lt;code&gt;main&lt;/code&gt; branch until it's in a production-ready state and covered by unit tests.&lt;/p&gt;
&lt;p&gt;(If continuous deployment to production is too scary for your project, a valuable middle-ground is continuous deployment to a staging environment. Having everyone on your team able to interact with a live demo of your current main branch is a huge group productivity boost.)&lt;/p&gt;
&lt;h4&gt;API pagination&lt;/h4&gt;
&lt;p&gt;Never build an API endpoint that isn't paginated. Any time you think "there will never be enough items in this list for it to be worth pagination" one of your users will prove you wrong.&lt;/p&gt;
&lt;p&gt;This can be as simple as shipping an API which, even though it only returns a single page, has hard-coded JSON that looks like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;results&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: [
    {&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;id&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;One&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;},
    {&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;id&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;2&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Two&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;},
    {&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;id&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;3&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Three&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;}
  ],
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;next_url&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;null&lt;/span&gt;
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But make sure you leave space for the pagination information! You'll regret it if you don't.&lt;/p&gt;
&lt;h4&gt;Detailed API logs&lt;/h4&gt;
&lt;p&gt;This is a trick I learned &lt;a href="https://simonwillison.net/2021/Apr/12/porting-vaccinateca-to-django/#value-of-api-logs"&gt;while porting VaccinateCA to Django&lt;/a&gt;. If you are building an API, having a mechanism that provides detailed logs - including the POST bodies passed to the API - is invaluable.&lt;/p&gt;
&lt;p&gt;It's an inexpensive way of maintaining a complete record of what happened with your application - invaluable for debugging, but also for tricks like replaying past API traffic against a new implementation under test.&lt;/p&gt;
&lt;p&gt;Logs like these may become infeasible at scale, but for a new project they'll probably add up to just a few MBs a day - and they're easy to prune or switch off later on if you need to.&lt;/p&gt;
&lt;p&gt;VIAL uses &lt;a href="https://github.com/CAVaccineInventory/vial/blob/a0780e27c39018b66f95278ce18eda5968c325f8/vaccinate/api/utils.py#L86"&gt;a Django view decorator&lt;/a&gt; to log these directly to a PostgreSQL table. We've been running this for a few months and it's now our largest table, but it's still only around 2GB - easily worth it for the productivity boost it gives us.&lt;/p&gt;
&lt;p&gt;(Don't log any sensitive data that you wouldn't want your development team having access to while debugging a problem. This may require clever redaction, or you can avoid logging specific endpoints entirely. Also: don't log authentication tokens that could be used to imitate users: decode them and log the user identifier instead.)&lt;/p&gt;
&lt;h4&gt;A bookmarkable interface for executing read-only SQL queries against your database&lt;/h4&gt;
&lt;p&gt;This one is very much exposing my biases (I just released &lt;a href="https://django-sql-dashboard.datasette.io/"&gt;Django SQL Dashboard 1.0&lt;/a&gt; which provides exactly this for Django+PosgreSQL projects) but having used this for the past few months I can't see myself going back. Using bookmarked SQL queries to inform the implementation of new features is an incredible productivity boost. Here's &lt;a href="https://github.com/CAVaccineInventory/vial/issues/528"&gt;an issue I worked on&lt;/a&gt; recently with 18 comments linking to illustrative SQL queries.&lt;/p&gt;
&lt;p&gt;(On further thought: this isn't actually a great example of a PAGNI because it's not particularly hard to add this to a project at a later date.)&lt;/p&gt;
&lt;h4&gt;Driving down the cost&lt;/h4&gt;
&lt;p&gt;One trick with all of these things is that while they may seem quite expensive to implement, they get dramatically cheaper as you gain experience and gather more tools for helping put them into practice.&lt;/p&gt;
&lt;p&gt;Any of the ideas I've shown here could take an engineering team weeks (if not months) to add to an existing project - but with the right tooling they can represent just an hour (or less) work at the start of a project. And they'll pay themselves off many, many times over in the future.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/continuous-deployment"&gt;continuous-deployment&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/continuous-integration"&gt;continuous-integration&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/definitions"&gt;definitions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/software-engineering"&gt;software-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/yagni"&gt;yagni&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pagni"&gt;pagni&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="continuous-deployment"/><category term="continuous-integration"/><category term="definitions"/><category term="software-engineering"/><category term="testing"/><category term="pytest"/><category term="github-actions"/><category term="django-sql-dashboard"/><category term="yagni"/><category term="pagni"/></entry><entry><title>Blazing fast CI with pytest-split and GitHub Actions</title><link href="https://simonwillison.net/2021/Feb/22/pytest-split/#atom-tag" rel="alternate"/><published>2021-02-22T19:06:40+00:00</published><updated>2021-02-22T19:06:40+00:00</updated><id>https://simonwillison.net/2021/Feb/22/pytest-split/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.jerrycodes.com/pytest-split-and-github-actions/"&gt;Blazing fast CI with pytest-split and GitHub Actions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
pytest-split is a neat looking variant on the pattern of splitting up a test suite to run different parts of it in parallel on different machines. It involves maintaining a periodically updated JSON file in the repo recording the average runtime of different tests, to enable them to be more fairly divided among test runners. Includes a recipe for running as a matrix in GitHub Actions.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/djm_/status/1363915616831344647"&gt;@djm_&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;&lt;/p&gt;



</summary><category term="testing"/><category term="pytest"/><category term="github-actions"/></entry><entry><title>A cookiecutter template for writing Datasette plugins</title><link href="https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/#atom-tag" rel="alternate"/><published>2020-06-20T16:15:42+00:00</published><updated>2020-06-20T16:15:42+00:00</updated><id>https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/#atom-tag</id><summary type="html">
    &lt;p&gt;Datasette’s &lt;a href="https://datasette.readthedocs.io/en/stable/plugins.html"&gt;plugin system&lt;/a&gt; is one of the most interesting parts of the entire project. As I explained to Matt Asay in &lt;a href="https://thenewstack.io/datasette-a-developer-a-shower-and-a-data-inspired-moment/"&gt;this interview&lt;/a&gt;, the great thing about plugins is that Datasette can gain new functionality overnight without me even having to review a pull request. I just need to get more people to write them!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/datasette-plugin"&gt;datasette-plugin&lt;/a&gt; is my most recent effort to help make that as easy as possible. It’s a &lt;a href="https://cookiecutter.readthedocs.io/"&gt;cookiecutter&lt;/a&gt; template that sets up the outline of a new plugin, combining various best patterns I’ve discovered over the past two years of writing my own plugins.&lt;/p&gt;
&lt;p&gt;Once you’ve &lt;a href="https://cookiecutter.readthedocs.io/en/1.7.2/installation.html"&gt;installed cookiecutter&lt;/a&gt; you can start building a new plugin by running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cookiecutter gh:simonw/datasette-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cookiecutter will run a quick interactive session asking for a few details. It will then use those details to generate a new directory structure ready for you to start work on the plugin.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/datasette-plugin/blob/main/README.md"&gt;datasette-plugin README&lt;/a&gt; describes the next steps. A couple of things are worth exploring in more detail.&lt;/p&gt;
&lt;h4 id="Writing_tests_for_plugins_14"&gt;Writing tests for plugins&lt;/h4&gt;
&lt;p&gt;I’m a big believer in automated testing: every single one of my plugins includes tests, and those test are run against every commit and must pass before new packages are shipped to &lt;a href="https://pypi.org"&gt;PyPI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In my experience the hardest part of writing tests is getting them started: setting up an initial test harness and ensuring that new tests can be easily written.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;datasette-plugin&lt;/code&gt; adds &lt;a href="https://docs.pytest.org/"&gt;pytest&lt;/a&gt; as a testing dependency and creates a &lt;code&gt;tests/&lt;/code&gt; folder with an initial, passing unit test in it.&lt;/p&gt;
&lt;p&gt;The test confirms that the new plugin has been correctly installed, by running a request through a configured Datasette instance and hitting the &lt;a href="https://datasette.readthedocs.io/en/stable/introspection.html#plugins"&gt;/-/plugins.json introspection endpoint&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In doing so, it demonstrates how to run tests that interact with Datasette’s HTTP API. This is a very productive way to write tests.&lt;/p&gt;
&lt;p&gt;The example test uses the &lt;a href="https://www.python-httpx.org"&gt;HTTPX&lt;/a&gt; Python library. HTTPX offers a requests-style API but with a couple of crucial improvements. Firstly, it’s been built with asyncio support as a top-level concern. Secondly, it &lt;a href="https://www.python-httpx.org/async/#calling-into-python-web-apps"&gt;understands the ASGI protocol&lt;/a&gt; and can be run directly against an ASGI Python interface without needing to spin up an actual HTTP server. Since Datasette &lt;a href="https://simonwillison.net/2019/Jun/23/datasette-asgi/"&gt;speaks ASGI&lt;/a&gt; this makes it the ideal tool for testing Datasette plugins.&lt;/p&gt;
&lt;p&gt;Here’s that first test that gets created by the cookiecutter template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from datasette.app import Datasette
import pytest
import httpx

@pytest.mark.asyncio
async def test_plugin_is_installed():
    app = Datasette([], memory=True).app()
    async with httpx.AsyncClient(app=app) as client:
        response = await client.get(
            &amp;quot;http://localhost/-/plugins.json&amp;quot;
        )
        assert 200 == response.status_code
        installed_plugins = {
            p[&amp;quot;name&amp;quot;] for p in response.json()
        }
        assert &amp;quot;datasette-plugin-template-demo&amp;quot; in installed_plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My hope is that including a passing test that demonstrates how to execute test requests will make it much easier for plugin authors to start building out their own custom test suite.&lt;/p&gt;
&lt;h4 id="Continuous_integration_with_GitHub_Actions_49"&gt;Continuous integration with GitHub Actions&lt;/h4&gt;
&lt;p&gt;My favourite thing about &lt;a href="https://simonwillison.net/tags/githubactions/"&gt;GitHub Actions&lt;/a&gt; is that they’re enabled on every GitHub repository for free, without any extra configuration necessary.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;datasette-plugin&lt;/code&gt; template takes advantage of this. Not only does every new project get a passing test - it also gets a GitHub Action - in &lt;code&gt;.github/workflows/test.yml&lt;/code&gt; - that executes the tests on every commit.&lt;/p&gt;
&lt;p&gt;It even &lt;a href="https://github.com/simonw/datasette-plugin/blob/8e4d5231bc276f19ccf630b18f075222e5afecb3/datasette-%7B%7Bcookiecutter.hyphenated%7D%7D/.github/workflows/test.yml#L8-L10"&gt;runs the test suite in parallel&lt;/a&gt; against Python 3.6, 3.7 and 3.8 - the versions currently supported by Datasette itself.&lt;/p&gt;
&lt;p&gt;A second action in &lt;code&gt;.github/workflows/publish.yml&lt;/code&gt; bakes in my opinions on the best way to manage plugin releases: it builds and ships a new package to PyPI every time a new tag (and corresponding GitHub release) is added to the repository.&lt;/p&gt;
&lt;p&gt;For this to work you’ll need to create a &lt;a href="https://pypi.org/help/#apitoken"&gt;PyPI API token&lt;/a&gt; and add it to your plugin’s GitHub repository as a &lt;code&gt;PYPI_TOKEN&lt;/code&gt; secret. This is &lt;a href="https://github.com/simonw/datasette-plugin/blob/main/README.md#publishing-your-plugin-as-a-package-to-pypi"&gt;explained in the README&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="Deploying_a_live_demo_of_the_template_with_GitHub_Actions_61"&gt;Deploying a live demo of the template with GitHub Actions&lt;/h4&gt;
&lt;p&gt;Whenever possible, I like to ship my projects with live demos. The Datasette repository &lt;a href="https://github.com/simonw/datasette/blob/master/.github/workflows/deploy-latest.yml"&gt;publishes a demo&lt;/a&gt; of the latest commit to &lt;a href="https://latest.datasette.io/"&gt;https://latest.datasette.io/&lt;/a&gt; on every commit. I try to do the same for my plugins, where it makes sense to do so.&lt;/p&gt;
&lt;p&gt;What could a live demo of a cookiecutter template look like?&lt;/p&gt;
&lt;p&gt;Ideally it would show a complete, generated project. I love GitHub’s code browsing interface, so a separate repository containing that generated project would be ideal.&lt;/p&gt;
&lt;p&gt;So that’s what &lt;a href="https://github.com/simonw/datasette-plugin-template-demo"&gt;https://github.com/simonw/datasette-plugin-template-demo&lt;/a&gt; is: it’s a repository showing the most recent output of the latest version of the cookiecutter template that lives in &lt;a href="https://github.com/simonw/datasette-plugin"&gt;https://github.com/simonw/datasette-plugin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It’s powered by &lt;a href="https://github.com/simonw/datasette-plugin/blob/main/.github/workflows/push.yml"&gt;this GitHub Action&lt;/a&gt;, which runs on every push to the &lt;code&gt;datasette-plugin&lt;/code&gt; repo, installs cookiecutter, uses cookiecutter against some &lt;a href="https://github.com/simonw/datasette-plugin/blob/main/input-for-demo.txt"&gt;fixed inputs&lt;/a&gt; to re-generate the project and then pushes the results up to &lt;code&gt;datasette-plugin-template-demo&lt;/code&gt; as a new commit.&lt;/p&gt;
&lt;p&gt;As a fun final touch, it uses the GitHub commit comments API to add a comment to the commit to &lt;code&gt;datasette-plugin&lt;/code&gt; linking to the “browse” view on the resulting code in the &lt;code&gt;datasette-plugin-template-demo&lt;/code&gt; repository. Here’s &lt;a href="https://github.com/simonw/datasette-plugin/commit/8e4d5231bc276f19ccf630b18f075222e5afecb3"&gt;one of those commit comments&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Figuring out how to build this took quite a bit of work. &lt;a href="https://github.com/simonw/datasette-plugin/issues/4"&gt;Issue #4&lt;/a&gt; has a blow-by-blow rundown of how I got it working.&lt;/p&gt;
&lt;p&gt;I couldn’t resist tweeting about it:&lt;/p&gt;
&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;Writing a GitHub Action for a repo that generates content for a second repo and pushes that content to the second repo and then posts a comment to the commit on the first repo that links to the newly committed code in the second repo&lt;/p&gt;- Simon Willison (@simonw) &lt;a href="https://twitter.com/simonw/status/1274118461896024064?ref_src=twsrc%5Etfw"&gt;June 19, 2020&lt;/a&gt;&lt;/blockquote&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="github"/><category term="plugins"/><category term="projects"/><category term="pypi"/><category term="datasette"/><category term="pytest"/><category term="github-actions"/><category term="cookiecutter"/></entry><entry><title>How to cheat at unit tests with pytest and Black</title><link href="https://simonwillison.net/2020/Feb/11/cheating-at-unit-tests-pytest-black/#atom-tag" rel="alternate"/><published>2020-02-11T06:56:55+00:00</published><updated>2020-02-11T06:56:55+00:00</updated><id>https://simonwillison.net/2020/Feb/11/cheating-at-unit-tests-pytest-black/#atom-tag</id><summary type="html">
    &lt;p&gt;I’ve been making a lot of progress on &lt;a href="https://simonwillison.net/tags/datasettecloud/"&gt;Datasette Cloud&lt;/a&gt; this week. As an application that provides private hosted Datasette instances (initially targeted at data journalists and newsrooms) the majority of the code I’ve written deals with permissions: allowing people to form teams, invite team members, promote and demote team administrators and suchlike.&lt;/p&gt;
&lt;p&gt;The one thing I’ve learned about permissions code over the years is that it absolutely warrants comprehensive unit tests. This is not code that can afford to have dumb bugs, or regressions caused by future development!&lt;/p&gt;
&lt;p&gt;I’ve become a big proponent of &lt;a href="https://docs.pytest.org/en"&gt;pytest&lt;/a&gt; over the past two years, but this is the first Django project that I’ve built using pytest from day one as opposed to relying on the Django test runner. It’s been a great opportunity to try out &lt;a href="https://pytest-django.readthedocs.io/"&gt;pytest-django&lt;/a&gt;, and I’m really impressed with it. It maintains my favourite things about Django’s test framework - smart usage of database transactions to reset the database and a handy &lt;a href="https://docs.djangoproject.com/en/3.0/topics/testing/tools/#the-test-client"&gt;test client object&lt;/a&gt; for sending fake HTTP requests - and adds all of that pytest magic that &lt;a href="https://simonwillison.net/2018/Jul/28/documentation-unit-tests/#Taking_advantage_of_pytest_78"&gt;I’ve grown to love&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It also means I get to use my favourite trick for productively writing unit tests: the combination of pytest and &lt;a href="https://github.com/psf/black"&gt;Black&lt;/a&gt;, the “uncompromising Python code formatter”.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Cheating_at_unit_tests_10"&gt;&lt;/a&gt;Cheating at unit tests&lt;/h3&gt;
&lt;p&gt;In pure test-driven development you write the tests first, and don’t start on the implementation until you’ve watched them fail.&lt;/p&gt;
&lt;p&gt;Most of the time I find that this is a net loss on productivity. I tend to prototype my way to solutions, so I often find myself with rough running code before I’ve developed enough of a concrete implementation plan to be able to write the tests.&lt;/p&gt;
&lt;p&gt;So… I cheat. Once I’m happy with the implementation I write the tests to match it. Then once I have the tests in place and I know what needs to change I can switch to using changes to the tests to drive the implementation.&lt;/p&gt;
&lt;p&gt;In particular, I like using a rough initial implementation to help generate the tests in the first place.&lt;/p&gt;
&lt;p&gt;Here’s how I do that with pytest. I’ll write a test that looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;test_some_api&lt;/span&gt;(&lt;span class="pl-s1"&gt;client&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;client&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"/some/api/"&lt;/span&gt;)
    &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-c1"&gt;False&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;.&lt;span class="pl-en"&gt;json&lt;/span&gt;()&lt;/pre&gt;
&lt;p&gt;Note that I’m using the pytest-django &lt;code&gt;client&lt;/code&gt; fixture here, which magically passes a fully configured Django test client object to my test function.&lt;/p&gt;
&lt;p&gt;I run this test, and it fails:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pytest -k test_some_api
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;pytest -k blah&lt;/code&gt; runs just tests that contain &lt;code&gt;blah&lt;/code&gt; in their name)&lt;/p&gt;
&lt;p&gt;Now… I run the test again, but with the &lt;code&gt;--pdb&lt;/code&gt; option to cause pytest to drop me into a debugger at the failure point:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pytest -k test_some_api --pdb
== test session starts ===
platform darwin -- Python 3.7.5, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
django: settings: config.test_settings (from ini)
...
client = &amp;lt;django.test.client.Client object at 0x10cfdb510&amp;gt;

    def test_some_api(client):
        response = client.get(&amp;quot;/some/api/&amp;quot;)
&amp;gt;       assert False == response.json()
E       assert False == {'this': ['is', 'an', 'example', 'api']}
core/test_docs.py:27: AssertionError
&amp;gt;&amp;gt; entering PDB &amp;gt;&amp;gt;
&amp;gt;&amp;gt; PDB post_mortem (IO-capturing turned off) &amp;gt;&amp;gt;
&amp;gt; core/test_docs.py(27)test_some_api()
-&amp;gt; assert False == response.json()
(Pdb) response.json()
{'this': ['is', 'an', 'example', 'api'], 'that_outputs': 'JSON'}
(Pdb) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;response.json()&lt;/code&gt; in the debugger dumps out the actual value to the console.&lt;/p&gt;
&lt;p&gt;Then I copy that output - in this case &lt;code&gt;{'this': ['is', 'an', 'example', 'api'], 'that_outputs': 'JSON'}&lt;/code&gt; - and paste it into the test:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;test_some_api&lt;/span&gt;(&lt;span class="pl-s1"&gt;client&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;client&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"/some/api/"&lt;/span&gt;)
    &lt;span class="pl-k"&gt;assert&lt;/span&gt; {&lt;span class="pl-s"&gt;'this'&lt;/span&gt;: [&lt;span class="pl-s"&gt;'is'&lt;/span&gt;, &lt;span class="pl-s"&gt;'an'&lt;/span&gt;, &lt;span class="pl-s"&gt;'example'&lt;/span&gt;, &lt;span class="pl-s"&gt;'api'&lt;/span&gt;], &lt;span class="pl-s"&gt;'that_outputs'&lt;/span&gt;: &lt;span class="pl-s"&gt;'JSON'&lt;/span&gt;} &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;.&lt;span class="pl-en"&gt;json&lt;/span&gt;()&lt;/pre&gt;
&lt;p&gt;Finally, I run &lt;code&gt;black .&lt;/code&gt; in my project root to reformat the test:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;test_some_api&lt;/span&gt;(&lt;span class="pl-s1"&gt;client&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;client&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"/some/api/"&lt;/span&gt;)
    &lt;span class="pl-k"&gt;assert&lt;/span&gt; {
        &lt;span class="pl-s"&gt;"this"&lt;/span&gt;: [&lt;span class="pl-s"&gt;"is"&lt;/span&gt;, &lt;span class="pl-s"&gt;"an"&lt;/span&gt;, &lt;span class="pl-s"&gt;"example"&lt;/span&gt;, &lt;span class="pl-s"&gt;"api"&lt;/span&gt;],
        &lt;span class="pl-s"&gt;"that_outputs"&lt;/span&gt;: &lt;span class="pl-s"&gt;"JSON"&lt;/span&gt;,
    } &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;.&lt;span class="pl-en"&gt;json&lt;/span&gt;()&lt;/pre&gt;
&lt;p&gt;This last step means that no matter how giant and ugly the test comparison has become I’ll always get a neatly formatted test out of it.&lt;/p&gt;
&lt;p&gt;I always eyeball the generated test to make sure that it’s what I would have written by hand if I wasn’t so lazy - then I commit it along with the implementation and move on to the next task.&lt;/p&gt;
&lt;p&gt;I’ve used this technique to write many of the tests in both &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; and &lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, and those are by far the best tested pieces of software I’ve ever released.&lt;/p&gt;
&lt;p&gt;I started doing this around two years ago, and I’ve held off writing about it until I was confident I understood the downsides. I haven’t found any yet: I end up with a robust, comprehensive test suite and it takes me less than half the time to write the tests than if I’d been hand-crafting all of those comparisons from scratch.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Also_this_week_86"&gt;&lt;/a&gt;Also this week&lt;/h3&gt;
&lt;p&gt;Working on Datasette Cloud has required a few minor releases to some of my open source projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shipped &lt;a href="https://github.com/simonw/datasette-auth-existing-cookies/releases"&gt;datasette-auth-existing-cookies&lt;/a&gt; 0.6 and 0.6.1&lt;/li&gt;
&lt;li&gt;Shipped &lt;a href="https://github.com/simonw/sqlite-utils/releases"&gt;sqlite-utils&lt;/a&gt; 2.2, 2.2.1, 2.3 and 2.3.1&lt;/li&gt;
&lt;li&gt;Shipped &lt;a href="https://datasette.readthedocs.io/en/latest/changelog.html#v0-35"&gt;Datasette 0.35&lt;/a&gt; with a new utility method for plugins to render their own templates, which I’m now using in…&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-upload-csvs/releases/tag/0.2a"&gt;datasette-upload-csvs 0.2a&lt;/a&gt; - still very alpha, but at least it looks slightly nicer now&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unrelated to Datasette Cloud, I also shipped &lt;a href="https://github.com/dogsheep/twitter-to-sqlite/releases/tag/0.16"&gt;twitter-to-sqlite 0.16&lt;/a&gt; with a new command for importing your Twitter friends (previously it only had a command for importing your followers).&lt;/p&gt;
&lt;p&gt;In bad personal motivation news… I missed my weekly update to &lt;a href="https://www.niche-museums.com/"&gt;Niche Museums&lt;/a&gt; and lost my streak!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/black"&gt;black&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="python"/><category term="testing"/><category term="datasette"/><category term="pytest"/><category term="weeknotes"/><category term="datasette-cloud"/><category term="black"/></entry><entry><title>Porting Datasette to ASGI, and Turtles all the way down</title><link href="https://simonwillison.net/2019/Jun/23/datasette-asgi/#atom-tag" rel="alternate"/><published>2019-06-23T21:39:00+00:00</published><updated>2019-06-23T21:39:00+00:00</updated><id>https://simonwillison.net/2019/Jun/23/datasette-asgi/#atom-tag</id><summary type="html">
    &lt;p&gt;This evening I finally closed a &lt;a href="https://simonwillison.net/tags/datasette/"&gt;Datasette&lt;/a&gt; issue that I opened more than 13 months ago: &lt;a href="https://github.com/simonw/datasette/issues/272"&gt;#272: Port Datasette to ASGI&lt;/a&gt;. A few notes on why this is such an important step for the project.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://asgi.readthedocs.io/"&gt;ASGI&lt;/a&gt; is the Asynchronous Server Gateway Interface standard. It’s been evolving steadily over the past few years under the guidance of Andrew Godwin. It’s intended as an asynchronous replacement for the venerable &lt;a href="https://wsgi.readthedocs.io/"&gt;WSGI&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Turtles_all_the_way_down_6"&gt;&lt;/a&gt;Turtles all the way down&lt;/h3&gt;
&lt;p&gt;Ten years ago at EuroDjangoCon 2009 in Prague I gave a talk entitled &lt;a href="https://www.slideshare.net/simon/django-heresies"&gt;Django Heresies&lt;/a&gt;. After discussing some of the design decisions in Django that I didn’t think had aged well, I spent the last part of the talk talking about &lt;em&gt;Turtles all the way down&lt;/em&gt;. I &lt;a href="https://simonwillison.net/2009/May/19/djng/?#turtles-all-the-way-down"&gt;wrote that idea up here&lt;/a&gt; on my blog (see also &lt;a href="https://www.slideshare.net/simon/django-heresies/65-The_Django_Contract_A_view"&gt;these slides&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The key idea was that Django would be more interesting if the core Django contract - a function that takes a request and returns a response - was extended to more places in the framework. The top level site, the reusable applications, middleware and URL routing could all share that same contract. Everything could be composed from the same raw building blocks.&lt;/p&gt;
&lt;p&gt;I’m excited about ASGI because it absolutely fits the &lt;em&gt;turtles all the way down&lt;/em&gt; model.&lt;/p&gt;
&lt;p&gt;The ASGI contract is an asynchronous function that takes three arguments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def application(scope, receive, send):
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scope&lt;/code&gt; is a serializable dictionary providing the context for the current connection. &lt;code&gt;receive&lt;/code&gt; is an awaitable which can be used to recieve incoming messages. &lt;code&gt;send&lt;/code&gt; is an awaitable that can be used to send replies.&lt;/p&gt;
&lt;p&gt;It’s a pretty low-level set of primitives (and less obvious than a simple request/response) - and that’s because ASGI is about more than just the standard HTTP request/response cycle. This contract works for HTTP, WebSockets and potentially any other protocol that needs to asynchronously send and receive data.&lt;/p&gt;
&lt;p&gt;It’s an extremely elegant piece of protocol design, informed by Andrew’s experience with Django Channels, SOA protocols (we are co-workers at Eventbrite where we’ve both been heavily involved in Eventbrite’s &lt;a href="https://github.com/eventbrite/pysoa"&gt;SOA mechanism&lt;/a&gt;) and Andrew’s extensive conversations with other maintainers in the Python web community.&lt;/p&gt;
&lt;p&gt;The ASGI protocol really is turtles all the way down - it’s a simple, well defined contract which can be composed together to implement all kinds of interesting web architectural patterns.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/simonw/asgi-cors/"&gt;asgi-cors library&lt;/a&gt; was my first attempt at building an ASGI turtle. &lt;a href="https://github.com/simonw/asgi-cors/blob/master/asgi_cors.py"&gt;The implementation&lt;/a&gt; is a simple Python decorator which, when applied to another ASGI callable, adds HTTP CORS headers based on the parameters you pass to the decorator. The library has zero installation dependencies (it has test dependencies on pytest and friends) and can be used on any HTTP ASGI project.&lt;/p&gt;
&lt;p&gt;Building &lt;code&gt;asgi-cors&lt;/code&gt; completely sold me on ASGI as the turtle pattern I had been desiring for over a decade!&lt;/p&gt;
&lt;h3&gt;&lt;a id="Datasette_plugins_and_ASGI_31"&gt;&lt;/a&gt;Datasette plugins and ASGI&lt;/h3&gt;
&lt;p&gt;Which brings me to Datasette.&lt;/p&gt;
&lt;p&gt;One of the most promising components of Datasette is its plugin mechanism. Based on &lt;a href="https://pluggy.readthedocs.io/en/latest/"&gt;pluggy&lt;/a&gt; (extracted from pytest), &lt;a href="https://datasette.readthedocs.io/en/stable/plugins.html"&gt;Datasette Plugins&lt;/a&gt; allow new features to be added to Datasette without needing to change the underlying code. This means new features can be built, packaged and shipped entirely independently of the core project. A list of currently available plugins &lt;a href="https://datasette.readthedocs.io/en/latest/ecosystem.html#datasette-plugins"&gt;can be found here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;WordPress is very solid blogging engine. Add in the plugin ecosystem around it and it can be used to build literally any CMS you can possibly imagine.&lt;/p&gt;
&lt;p&gt;My dream for Datasette is to apply the same model: I want a strong core for publishing and exploring data that’s enhanced by plugins to solve a huge array of data analysis, visualization and API-backed problems.&lt;/p&gt;
&lt;p&gt;Datasette has &lt;a href="https://datasette.readthedocs.io/en/latest/plugins.html#plugin-hooks"&gt;a range of plugin hooks already&lt;/a&gt;, but I’ve so far held back on implementing the most useful class of hooks: hooks that allow developers to add entirely new URL routes exposing completely custom functionality.&lt;/p&gt;
&lt;p&gt;The reason I held back is that I wanted to be confident that the contract I was offering was something I would continue to support moving forward. A plugin system isn’t much good if the core implementation keeps on changing in backwards-incompatible ways.&lt;/p&gt;
&lt;p&gt;ASGI is the exact contract I’ve been waiting for. It’s not quite ready yet, but you can follow &lt;a href="https://github.com/simonw/datasette/issues/520"&gt;#520: prepare_asgi plugin hook&lt;/a&gt; (thoughts and suggestions welcome!) to be the first to hear about this hook when it lands. I’m planning to use it to make my asgi-cors library available as a plugin, after which I’m excited to start exploring the idea of bringing authentication plugins to Datasette (and to the wider ASGI world in general).&lt;/p&gt;
&lt;p&gt;I’m hoping that many Datasette ASGI plugins will exist in a form that allows them to be used by other ASGI applications as well.&lt;/p&gt;
&lt;p&gt;I also plan to use ASGI to make components of Datasette itself available to other ASGI applications. If you just want a single instance of Datasette’s &lt;a href="https://datasette.readthedocs.io/en/stable/pages.html#table"&gt;table view&lt;/a&gt; to be embedded somewhere in your URL configuration you should be able to do that by routing traffic directly to the ASGI-compatible view class.&lt;/p&gt;
&lt;p&gt;I’m really excited about exploring the intersection of ASGI turtles-all-the-way-down and pluggy’s powerful mechanism for gluing components together. Both WSGI and Django’s reusable apps have attempted to create a reusable ecosystem in the past, to limited levels of success. Let’s see if ASGI can finally make the turtle dream come true.&lt;/p&gt;

&lt;h3&gt;&lt;a id="Further_reading_53"&gt;&lt;/a&gt;Further reading&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.encode.io/articles/hello-asgi/"&gt;Hello ASGI&lt;/a&gt; by Tom Christie is the best introduction to ASGI I’ve seen. Tom is the author of the &lt;a href="https://www.uvicorn.org/"&gt;Uvicorn&lt;/a&gt; ASGI server (used by Datasette as-of this evening) and &lt;a href="https://www.starlette.io/"&gt;Starlette&lt;/a&gt;, a delightfully well-designd ASGI web framework. I’ve learned an enormous amount about ASGI by reading Tom’s code. Tom also gave &lt;a href="https://www.youtube.com/watch?v=u8GSFEg5lnU"&gt;a talk about ASGI&lt;/a&gt; at DjangoCon Europe a few months ago.&lt;/p&gt;
&lt;p&gt;If you haven’t read &lt;a href="https://www.aeracode.org/2018/06/04/django-async-roadmap/"&gt;A Django Async Roadmap&lt;/a&gt; by Andrew Godwin last year you should absolutely catch up. More than just talking about ASGI, Andrew sketches out a detailed and actionable plan for bringing asyncio to Django core. Andrew landeded &lt;a href="https://github.com/django/django/pull/11209"&gt;the first Django core ASGI code&lt;/a&gt; based on the plan just a few days ago.&lt;/p&gt;
&lt;p&gt;If you're interested in the details of Datasette's ASGI implementation, I posted &lt;a href="https://github.com/simonw/datasette/issues/272"&gt;detailed commentary on issue #272&lt;/a&gt; over the past thirteen months as I researched and finalized my approach. I added further commentary to &lt;a href="https://github.com/simonw/datasette/pull/518"&gt;the associated pull request&lt;/a&gt;, which gathers together the 34 commits it took to ship the feature (squashed into a single commit to master).&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/andrew-godwin"&gt;andrew-godwin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/kim-christie"&gt;kim-christie&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/starlette"&gt;starlette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="andrew-godwin"/><category term="projects"/><category term="datasette"/><category term="asgi"/><category term="kim-christie"/><category term="pytest"/><category term="starlette"/></entry><entry><title>parameterized</title><link href="https://simonwillison.net/2019/Feb/19/parameterized/#atom-tag" rel="alternate"/><published>2019-02-19T21:05:05+00:00</published><updated>2019-02-19T21:05:05+00:00</updated><id>https://simonwillison.net/2019/Feb/19/parameterized/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/wolever/parameterized"&gt;parameterized&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I love the @parametrize decorator in pytest, which lets you run the same test multiple times against multiple parameters. The only catch is that the decorator in pytest doesn’t work for old-style unittest TestCase tests, which means you can’t easily add it to test suites that were built using the older model. I just found out about parameterized which works with unittest tests whether or not you are running them using the pytest test runner.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="testing"/><category term="pytest"/></entry><entry><title>Documentation unit tests</title><link href="https://simonwillison.net/2018/Jul/28/documentation-unit-tests/#atom-tag" rel="alternate"/><published>2018-07-28T15:59:55+00:00</published><updated>2018-07-28T15:59:55+00:00</updated><id>https://simonwillison.net/2018/Jul/28/documentation-unit-tests/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;Or: Test-driven documentation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Keeping documentation synchronized with an evolving codebase is difficult. Without extreme discipline, it’s easy for documentation to get out-of-date as new features are added.&lt;/p&gt;
&lt;p&gt;One thing that can help is keeping the documentation for a project in the same repository as the code itself. This allows you to construct the ideal commit: one that includes the code change, the updated unit tests AND the accompanying documentation all in the same unit of work.&lt;/p&gt;
&lt;p&gt;When combined with a code review system (like &lt;a href="https://www.phacility.com/phabricator/"&gt;Phabricator&lt;/a&gt; or &lt;a href="https://help.github.com/articles/about-pull-requests/"&gt;GitHub pull requests&lt;/a&gt;) this pattern lets you enforce documentation updates as part of the review process: if a change doesn’t update the relevant documentation, point that out in your review!&lt;/p&gt;
&lt;p&gt;Good code review systems also execute unit tests automatically and attach the results to the review. This provides an opportunity to have the tests enforce other aspects of the codebase: for example, running a linter so that no-one has to waste their time arguing over standardize coding style.&lt;/p&gt;
&lt;p&gt;I’ve been experimenting with using unit tests to ensure that aspects of a project are covered by the documentation. I think it’s a very promising technique.&lt;/p&gt;
&lt;h4 id="Introspect_the_code_introspect_the_docs_12"&gt;Introspect the code, introspect the docs&lt;/h4&gt;
&lt;p&gt;The key to this trick is introspection: interogating the code to figure out what needs to be documented, then parsing the documentation to see if each item has been covered.&lt;/p&gt;
&lt;p&gt;I’ll use my &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; project as an example. Datasette’s &lt;a href="https://github.com/simonw/datasette/blob/295d005ca48747faf046ed30c3c61e7563c61ed2/tests/test_docs.py"&gt;test_docs.py&lt;/a&gt; module contains three relevant tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;test_config_options_are_documented&lt;/code&gt; checks that every one of Datasette’s &lt;a href="http://datasette.readthedocs.io/en/latest/config.html"&gt;configuration options&lt;/a&gt; are documented.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test_plugin_hooks_are_documented&lt;/code&gt; ensures all of the plugin hooks (powered by &lt;a href="https://pluggy.readthedocs.io/en/latest/"&gt;pluggy&lt;/a&gt;) are covered in the &lt;a href="http://datasette.readthedocs.io/en/latest/plugins.html#plugin-hooks"&gt;plugin documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test_view_classes_are_documented&lt;/code&gt; iterates through all of the &lt;code&gt;*View&lt;/code&gt; classes (corresponding to pages in the Datasette user interface) and makes sure &lt;a href="http://datasette.readthedocs.io/en/latest/pages.html"&gt;they are covered&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In each case, the test uses introspection against the relevant code areas to figure out what needs to be documented, then runs a regular expression against the documentation to make sure it is mentioned in the correct place.&lt;/p&gt;
&lt;p&gt;Obviously the tests can’t confirm the quality of the documentation, so they are easy to cheat: but they do at least protect against adding a new option but forgetting to document it.&lt;/p&gt;
&lt;h4 id="Testing_that_Datasettes_view_classes_are_covered_26"&gt;Testing that Datasette’s view classes are covered&lt;/h4&gt;
&lt;p&gt;Datasette’s view classes use a naming convention: they all end in &lt;code&gt;View&lt;/code&gt;. The current list of view classes is &lt;code&gt;DatabaseView&lt;/code&gt;, &lt;code&gt;TableView&lt;/code&gt;, &lt;code&gt;RowView&lt;/code&gt;, &lt;code&gt;IndexView&lt;/code&gt; and &lt;code&gt;JsonDataView&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Since these classes are all imported into the &lt;a href="https://github.com/simonw/datasette/blob/295d005ca48747faf046ed30c3c61e7563c61ed2/datasette/app.py"&gt;datasette.app&lt;/a&gt; module (in order to be hooked up to URL routes) the easiest way to introspect them is to import that module, then run &lt;code&gt;dir(app)&lt;/code&gt; and grab any class names that end in &lt;code&gt;View&lt;/code&gt;. We can do that with a Python list comprehension:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from datasette import app
views = [v for v in dir(app) if v.endswith(&amp;quot;View&amp;quot;)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I’m using reStructuredText labels to mark the place in the documentation that addresses each of these classes. This also ensures that each documentation section can be linked to, for example:&lt;/p&gt;
&lt;p&gt;&lt;a href="http://datasette.readthedocs.io/en/latest/pages.html#tableview"&gt;http://datasette.readthedocs.io/en/latest/pages.html#tableview&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The reStructuredText syntax for that label looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.. _TableView:

Table
=====

The table page is the heart of Datasette...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can extract these labels using a regular expression:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pathlib import Path
import re

docs_path = Path(__file__).parent.parent / 'docs'
label_re = re.compile(r'\.\. _([^\s:]+):')

def get_labels(filename):
    contents = (docs_path / filename).open().read()
    return set(label_re.findall(contents))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since Datasette’s documentation is spread across multiple &lt;code&gt;*.rst&lt;/code&gt; files, and I want the freedom to document a view class in any one of them, I iterate through every file to find the labels and pull out the ones ending in &lt;code&gt;View&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def documented_views():
    view_labels = set()
    for filename in docs_path.glob(&amp;quot;*.rst&amp;quot;):
        for label in get_labels(filename):
            first_word = label.split(&amp;quot;_&amp;quot;)[0]
            if first_word.endswith(&amp;quot;View&amp;quot;):
                view_labels.add(first_word)
    return view_labels
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now have a list of class names and a list of labels across all of our documentation. Writing a basic unit test comparing the two lists is trivial:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def test_view_documentation():
    view_labels = documented_views()
    view_classes = set(v for v in dir(app) if v.endswith(&amp;quot;View&amp;quot;))
    assert view_labels == view_classes
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="Taking_advantage_of_pytest_78"&gt;Taking advantage of pytest&lt;/h4&gt;
&lt;p&gt;Datasette uses &lt;a href="https://pytest.org/"&gt;pytest&lt;/a&gt; for its unit tests, and documentation unit tests are a great opportunity to take advantage of some advanced pytest features.&lt;/p&gt;
&lt;h5 id="Parametrization_82"&gt;Parametrization&lt;/h5&gt;
&lt;p&gt;The first of these is &lt;a href="https://docs.pytest.org/en/6.2.x/parametrize.html"&gt;parametrization&lt;/a&gt;: pytest provides a decorator which can be used to execute a single test function multiple times, each time with different arguments.&lt;/p&gt;
&lt;p&gt;This example from the pytest documentation shows how parametrization works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pytest
@pytest.mark.parametrize(&amp;quot;test_input,expected&amp;quot;, [
    (&amp;quot;3+5&amp;quot;, 8),
    (&amp;quot;2+4&amp;quot;, 6),
    (&amp;quot;6*9&amp;quot;, 42),
])
def test_eval(test_input, expected):
    assert eval(test_input) == expected
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pytest treats this as three separate unit tests, even though they share a single function definition.&lt;/p&gt;
&lt;p&gt;We can combine this pattern with our introspection to execute an independent unit test for each of our view classes. Here’s what that looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@pytest.mark.parametrize(&amp;quot;view&amp;quot;, [v for v in dir(app) if v.endswith(&amp;quot;View&amp;quot;)])
def test_view_classes_are_documented(view):
    assert view in documented_views()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s the output from pytest if we execute just this unit test (and one of our classes is undocumented):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pytest -k test_view_classes_are_documented -v
=== test session starts ===
collected 249 items / 244 deselected

tests/test_docs.py::test_view_classes_are_documented[DatabaseView] PASSED [ 20%]
tests/test_docs.py::test_view_classes_are_documented[IndexView] PASSED [ 40%]
tests/test_docs.py::test_view_classes_are_documented[JsonDataView] PASSED [ 60%]
tests/test_docs.py::test_view_classes_are_documented[RowView] PASSED [ 80%]
tests/test_docs.py::test_view_classes_are_documented[TableView] FAILED [100%]

=== FAILURES ===

view = 'TableView'

    @pytest.mark.parametrize(&amp;quot;view&amp;quot;, [v for v in dir(app) if v.endswith(&amp;quot;View&amp;quot;)])
    def test_view_classes_are_documented(view):
&amp;gt;       assert view in documented_views()
E       AssertionError: assert 'TableView' in {'DatabaseView', 'IndexView', 'JsonDataView', 'RowView', 'Table2View'}
E        +  where {'DatabaseView', 'IndexView', 'JsonDataView', 'RowView', 'Table2View'} = documented_views()

tests/test_docs.py:77: AssertionError
=== 1 failed, 4 passed, 244 deselected in 1.13 seconds ===
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id="Fixtures_130"&gt;Fixtures&lt;/h5&gt;
&lt;p&gt;There’s a subtle inefficiency in the above test: for every view class, it calls the &lt;code&gt;documented_views()&lt;/code&gt; function - and that function then iterates through every &lt;code&gt;*.rst&lt;/code&gt; file in the &lt;code&gt;docs/&lt;/code&gt; directory and uses a regular expression to extract the labels. With 5 view classes and 17 documentation files that’s 85 executions of &lt;code&gt;get_labels()&lt;/code&gt;, and that number will only increase as Datasette’s code and documentation grow larger.&lt;/p&gt;
&lt;p&gt;We can use pytest’s neat &lt;a href="https://docs.pytest.org/en/6.2.x/fixture.html"&gt;fixtures&lt;/a&gt; to reduce this to a single call to &lt;code&gt;documented_views()&lt;/code&gt; that is shared across all of the tests. Here’s what that looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@pytest.fixture(scope=&amp;quot;session&amp;quot;)
def documented_views():
    view_labels = set()
    for filename in docs_path.glob(&amp;quot;*.rst&amp;quot;):
        for label in get_labels(filename):
            first_word = label.split(&amp;quot;_&amp;quot;)[0]
            if first_word.endswith(&amp;quot;View&amp;quot;):
                view_labels.add(first_word)
    return view_labels

@pytest.mark.parametrize(&amp;quot;view_class&amp;quot;, [
    v for v in dir(app) if v.endswith(&amp;quot;View&amp;quot;)
])
def test_view_classes_are_documented(documented_views, view_class):
    assert view_class in documented_views
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fixtures in pytest are an example of dependency injection: pytest introspects every &lt;code&gt;test_*&lt;/code&gt; function and checks if it has a function argument with a name matching something that has been annotated with the &lt;code&gt;@pytest.fixture&lt;/code&gt; decorator. If it finds any matching arguments, it executes the matching fixture function and passes its return value in to the test function.&lt;/p&gt;
&lt;p&gt;By default, pytest will execute the fixture function once for every test execution. In the above code we use the &lt;code&gt;scope=&amp;quot;session&amp;quot;&lt;/code&gt; argument to tell pytest that this particular fixture should be executed only once for every &lt;code&gt;pytest&lt;/code&gt; command-line execution of the tests, and that single return value should be passed to every matching test.&lt;/p&gt;
&lt;h4 id="What_if_you_havent_documented_everything_yet_157"&gt;What if you haven’t documented everything yet?&lt;/h4&gt;
&lt;p&gt;Adding unit tests to your documentation in this way faces an obvious problem: when you first add the tests, you may have to write a whole lot of documentation before they can all pass.&lt;/p&gt;
&lt;p&gt;Having tests that protect against future code being added without documentation is only useful once you’ve added them to the codebase - but blocking that on documenting your existing features could prevent that benefit from ever manifesting itself.&lt;/p&gt;
&lt;p&gt;Once again, pytest to the rescue. The &lt;code&gt;@pytest.mark.xfail&lt;/code&gt; decorator allows you to mark a test as “expected to fail” - if it fails, pytest will take note but will not fail the entire test suite.&lt;/p&gt;
&lt;p&gt;This means you can add deliberately failing tests to your codebase without breaking the build for everyone - perfect for tests that look for documentation that hasn’t yet been written!&lt;/p&gt;
&lt;p&gt;I used &lt;code&gt;xfail&lt;/code&gt; when I &lt;a href="https://github.com/simonw/datasette/commit/e8625695a3b7938f37b64dff09c14e47d9428fe5"&gt;first added view documentation tests&lt;/a&gt; to Datasette, then removed it once the documentation was all in place. Any future code in pull requests without documentation will cause a hard test failure.&lt;/p&gt;
&lt;p&gt;Here’s what the test output looks like when some of those tests are marked as “expected to fail”:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pytest tests/test_docs.py
collected 31 items

tests/test_docs.py ..........................XXXxx.                [100%]

============ 26 passed, 2 xfailed, 3 xpassed in 1.06 seconds ============
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since this reports both the xfailed &lt;em&gt;and&lt;/em&gt; the xpassed counts, it shows how much work is still left to be done before the &lt;code&gt;xfail&lt;/code&gt; decorator can be safely removed.&lt;/p&gt;
&lt;h4 id="Structuring_code_for_testable_documentation_180"&gt;Structuring code for testable documentation&lt;/h4&gt;
&lt;p&gt;A benefit of comprehensive unit testing is that it encourages you to design your code in a way that is easy to test. In my experience this leads to much higher code quality in general: it encourages separation of concerns and cleanly decoupled components.&lt;/p&gt;
&lt;p&gt;My hope is that documentation unit tests will have a similar effect. I’m already starting to think about ways of restructuring my code such that I can cleanly introspect it for the areas that need to be documented. I’m looking forward to discovering code design patterns that help support this goal.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/design-patterns"&gt;design-patterns&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/restructuredtext"&gt;restructuredtext&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="design-patterns"/><category term="documentation"/><category term="restructuredtext"/><category term="testing"/><category term="datasette"/><category term="pytest"/></entry><entry><title>Datasette plugins, and building a clustered map visualization</title><link href="https://simonwillison.net/2018/Apr/20/datasette-plugins/#atom-tag" rel="alternate"/><published>2018-04-20T15:41:11+00:00</published><updated>2018-04-20T15:41:11+00:00</updated><id>https://simonwillison.net/2018/Apr/20/datasette-plugins/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; now supports plugins!&lt;/p&gt;
&lt;p&gt;Last Saturday &lt;a href="https://twitter.com/simonw/status/985377670388105216"&gt;I asked Twitter&lt;/a&gt; for examples of Python projects with successful plugin ecosystems. &lt;a href="https://docs.pytest.org/"&gt;pytest&lt;/a&gt; was the clear winner: the &lt;a href="https://plugincompat.herokuapp.com/"&gt;pytest plugin compatibility table&lt;/a&gt; (an ingenious innovation that I would love to eventually copy for Datasette) lists 457 plugins, and even the core pytest system itself is built as a collection of default plugins that can be replaced or over-ridden.&lt;/p&gt;
&lt;p&gt;Best of all: pytest’s plugin mechanism is available as a separate package: &lt;a href="https://pluggy.readthedocs.io/"&gt;pluggy&lt;/a&gt;. And pluggy was exactly what I needed for Datasette.&lt;/p&gt;
&lt;p&gt;You can follow the ongoing development of the feature in issue &lt;a href="https://github.com/simonw/datasette/issues/14"&gt;#14&lt;/a&gt;. This morning I released &lt;a href="https://github.com/simonw/datasette/releases/tag/0.20"&gt;Datasette 0.20&lt;/a&gt; with support for a number of different plugin hooks: plugins can add custom template tags and SQL functions, and can also bundle their own static assets, JavaScript, CSS and templates. The hooks are described in some detail in the &lt;a href="https://datasette.readthedocs.io/en/latest/plugins.html"&gt;Datasette Plugins&lt;/a&gt; documentation.&lt;/p&gt;
&lt;h2&gt;&lt;a id="datasetteclustermap_10"&gt;&lt;/a&gt;datasette-cluster-map&lt;/h2&gt;
&lt;p&gt;I also released my first plugin: &lt;a href="https://pypi.org/project/datasette-cluster-map/"&gt;datasette-cluster-map&lt;/a&gt;. Once installed, it looks out for database tables that have a &lt;code&gt;latitude&lt;/code&gt; and &lt;code&gt;longitude&lt;/code&gt; column. When it finds them, it draws all of the points on an interactive map using &lt;a href="http://leafletjs.com/"&gt;Leaflet&lt;/a&gt; and &lt;a href="https://github.com/Leaflet/Leaflet.markercluster"&gt;Leaflet.markercluster&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let’s &lt;a href="https://datasette-cluster-map-demo.now.sh/polar-bears-455fe3a/USGS_WC_eartags_output_files_2009-2011-Status"&gt;try it out on some polar bears&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2018/polar-bears-cluster-map.gif" alt="Polar Bears on a cluster map"/&gt;&lt;/p&gt;
&lt;p&gt;The USGS Alaska Science Center have released a delightful set of data entitled &lt;a href="https://alaska.usgs.gov/products/data.php?dataid=130"&gt;Sensor and Location data from Ear Tag PTTs Deployed on Polar Bears in the Southern Beaufort Sea 2009 to 2011&lt;/a&gt;. It’s a collection of CSV files, which means it’s &lt;a href="https://gist.github.com/simonw/9f8bf23b37a42d7628c4dcc4bba10253"&gt;trivial to convert it to SQLite&lt;/a&gt; using my &lt;a href="https://github.com/simonw/csvs-to-sqlite"&gt;csvs-to-sqlite&lt;/a&gt; tool.&lt;/p&gt;
&lt;p&gt;Having created the SQLite database, we can deploy it to a hosting account on &lt;a href="https://zeit.co/now"&gt;Zeit Now&lt;/a&gt; alongside the new plugin like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Make sure we have the latest datasette
pip3 install datasette --upgrade
# Deploy polar-bears.db to now with an increased default page_size
datasette publish now \
    --install=datasette-cluster-map \
    --extra-options &amp;quot;--page_size=500&amp;quot; \
    polar-bears.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--install&lt;/code&gt; option is new in Datasette 0.20 (it works for &lt;code&gt;datasette publish heroku&lt;/code&gt; as well) - it tells the publishing provider to &lt;code&gt;pip install&lt;/code&gt; the specified package. You can use it more than once to install multiple plugins, and it accepts a path to a zip file in addition to the name of a PyPI package.&lt;/p&gt;
&lt;p&gt;Explore the full demo at &lt;a href="https://datasette-cluster-map-demo.now.sh/polar-bears"&gt;https://datasette-cluster-map-demo.now.sh/polar-bears&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a id="Visualize_any_query_on_a_map_35"&gt;&lt;/a&gt;Visualize any query on a map&lt;/h2&gt;
&lt;p&gt;Since the plugin inserts itself at the top of any Datasette table view with &lt;code&gt;latitude&lt;/code&gt; and &lt;code&gt;longitude&lt;/code&gt; columns, there are all sorts of neat tricks you can do with it.&lt;/p&gt;
&lt;p&gt;I also loaded the San Francisco tree list (thanks, &lt;a href="https://data.sfgov.org/City-Infrastructure/Street-Tree-List/tkzw-k3nq"&gt;Department of Public Works&lt;/a&gt;) into the demo. Impressively, you can click “load all” &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1/Street_Tree_List"&gt;on this page&lt;/a&gt; and &lt;code&gt;Leaflet.markercluster&lt;/code&gt; will load in all 189,144 points and display them on the same map… and it works fine on my laptop and my phone. Computers in 2018 are pretty good!&lt;/p&gt;
&lt;p&gt;But since it’s a Datasette table, we can filter it. Here’s a map of &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1/Street_Tree_List?qSpecies=2"&gt;every New Zealand Xmas Tree&lt;/a&gt; in San Francisco (8,683 points). Here’s &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1/Street_Tree_List?qCareAssistant=1"&gt;every tree where the Caretaker is Friends of the Urban Forest&lt;/a&gt;. Here’s &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1/Street_Tree_List?PlantDate__contains=1990&amp;amp;_search=palm&amp;amp;_sort=qLegalStatus"&gt;every palm tree planted in 1990&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2018/datasette-palm-trees-1990.png" alt="Palm trees planted in 1990" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; This is an incorrect example: there are 21 matches on "palm avenue" because the FTS search index covers the address field - they're not actually palm trees. Here's a corrected query for &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1?sql=select+PlantDate%2C+Street_Tree_List.rowid%2C+latitude%2C+longitude%2C+qSpecies.value%0D%0Afrom+Street_Tree_List+join+qSpecies+on+Street_Tree_List.qSpecies+%3D+qSpecies.id%0D%0Awhere+qSpecies.value+like+%22%25palm%25%22+and+PlantDate+like+%22%251990%25%22"&gt;palm trees planted in 1990&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The plugin currently only works against columns called &lt;code&gt;latitude&lt;/code&gt; and &lt;code&gt;longitude&lt;/code&gt;… but if your columns are called something else, don’t worry: you can craft a custom SQL query that aliases your columns and everything will work as intended. Here’s an example &lt;a href="https://datasette-cluster-map-demo.now.sh/polar-bears-455fe3a?sql=select+*%2C+%22Capture+Latitude%22+as+latitude%2C+%22Capture+Longitude%22+as+longitude%0D%0Afrom+%5BUSGS_WC_eartag_deployments_2009-2011%5D"&gt;against some more polar bear data&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select *, &amp;quot;Capture Latitude&amp;quot; as latitude, &amp;quot;Capture Longitude&amp;quot; as longitude
from [USGS_WC_eartag_deployments_2009-2011]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a id="Writing_your_own_plugins_50"&gt;&lt;/a&gt;Writing your own plugins&lt;/h2&gt;
&lt;p&gt;I’m really excited to see what people invent. If you want to have a go, your first stop should be the &lt;a href="https://datasette.readthedocs.io/en/latest/plugins.html"&gt;Plugins documentation&lt;/a&gt;. If you want an example of a simple plugin (including the all-important mechanism for packaging it up using &lt;code&gt;setup.py&lt;/code&gt;) take a look at &lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt; on GitHub.&lt;/p&gt;
&lt;p&gt;And if you have any thoughts, ideas or suggestions on how the plugin mechanism can be further employed please join the conversation on &lt;a href="https://github.com/simonw/datasette/issues/14"&gt;issue #14&lt;/a&gt;. I’ve literally just got started with Datasette’s plugin hooks, and I’m very keen to hear about things people want to build that aren’t yet supported.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/maps"&gt;maps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="maps"/><category term="plugins"/><category term="projects"/><category term="visualization"/><category term="datasette"/><category term="pytest"/><category term="leaflet"/></entry></feed>