<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: black</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/black.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-01-31T21:27:04+00:00</updated><author><name>Simon Willison</name></author><entry><title>Latest black (25.1.0) adds a newline after docstring and before pass in an exception class</title><link href="https://simonwillison.net/2025/Jan/31/black-newline-docstring/#atom-tag" rel="alternate"/><published>2025-01-31T21:27:04+00:00</published><updated>2025-01-31T21:27:04+00:00</updated><id>https://simonwillison.net/2025/Jan/31/black-newline-docstring/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/psf/black/issues/4571"&gt;Latest black (25.1.0) adds a newline after docstring and before pass in an exception class&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I filed a bug report against Black when the latest release - 25.1.0 - reformatted the following code to add an ugly (to me) newline between the docstring and the &lt;code&gt;pass&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;ModelError&lt;/span&gt;(&lt;span class="pl-v"&gt;Exception&lt;/span&gt;):
    &lt;span class="pl-s"&gt;"Models can raise this error, which will be displayed to the user"&lt;/span&gt;

    &lt;span class="pl-k"&gt;pass&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;Black maintainer Jelle Zijlstra confirmed that this is intended behavior with respect to &lt;a href="https://github.com/psf/black/issues/4522"&gt;Black's 2025 stable style&lt;/a&gt;, but also helped me understand that the &lt;code&gt;pass&lt;/code&gt; there is actually unnecessary so I can fix the aesthetics by &lt;a href="https://github.com/simonw/llm/commit/deb8bc3b4f5219583009eeb2c600d0b14c852c78"&gt;removing that entirely&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I'm linking to this issue because it's a neat example of how I like to include steps-to-reproduce using &lt;a href="https://docs.astral.sh/uv/guides/tools/"&gt;uvx&lt;/a&gt; to create one-liners you can paste into a terminal to see the bug that I'm reporting. In this case I shared the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Here's a way to see that happen using &lt;code&gt;uvx&lt;/code&gt;. With the previous Black version:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;echo&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;class ModelError(Exception):&lt;/span&gt;
&lt;span class="pl-s"&gt;    "Models can raise this error, which will be displayed to the user"&lt;/span&gt;
&lt;span class="pl-s"&gt;    pass&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;|&lt;/span&gt; uvx --with &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;black==24.10.0&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; black -&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This outputs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ModelError(Exception):
    "Models can raise this error, which will be displayed to the user"
    pass
All done! ✨ 🍰 ✨
1 file left unchanged.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But if you bump to &lt;code&gt;25.1.0&lt;/code&gt; this happens:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;echo&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;class ModelError(Exception):&lt;/span&gt;
&lt;span class="pl-s"&gt;    "Models can raise this error, which will be displayed to the user"&lt;/span&gt;
&lt;span class="pl-s"&gt;    pass&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;|&lt;/span&gt; uvx --with &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;black==25.1.0&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; black - &lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ModelError(Exception):
    "Models can raise this error, which will be displayed to the user"

    pass
reformatted -

All done! ✨ 🍰 ✨
1 file reformatted.
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;Via &lt;a href="https://fosstodon.org/@davidszotten/113928041285282786"&gt;David Szotten&lt;/a&gt; I learned that you can use &lt;code&gt;uvx black@25.1.0&lt;/code&gt; here instead.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/black"&gt;black&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="black"/><category term="uv"/></entry><entry><title>Compiling Black with mypyc</title><link href="https://simonwillison.net/2022/May/31/compiling-black-with-mypyc/#atom-tag" rel="alternate"/><published>2022-05-31T23:24:16+00:00</published><updated>2022-05-31T23:24:16+00:00</updated><id>https://simonwillison.net/2022/May/31/compiling-black-with-mypyc/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://ichard26.github.io/blog/2022/05/31/compiling-black-with-mypyc-part-1/"&gt;Compiling Black with mypyc&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Richard Si is a Black contributor who recently obtained a 2x performance boost by compiling Black using the mypyc tool from the mypy project, which uses Python type annotations to generate a compiled C version of the Python logic. He wrote up this fantastic three-part series describing in detail how he achieved this, including plenty of tips on Python profiling and clever optimization tricks.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/llanga/status/1531741163539005449"&gt;Łukasz Langa&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/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mypy"&gt;mypy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/black"&gt;black&lt;/a&gt;&lt;/p&gt;



</summary><category term="performance"/><category term="python"/><category term="mypy"/><category term="black"/></entry><entry><title>Black 22.1.0</title><link href="https://simonwillison.net/2022/Jan/30/black/#atom-tag" rel="alternate"/><published>2022-01-30T01:23:39+00:00</published><updated>2022-01-30T01:23:39+00:00</updated><id>https://simonwillison.net/2022/Jan/30/black/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://black.readthedocs.io/en/stable/change_log.html#id1"&gt;Black 22.1.0&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Black, the uncompromising code formatter for Python, has had its first stable non-beta release after almost four years of releases. I adopted Black a few years ago for all of my projects and I wouldn’t release Python code without it now—the productivity boost I get from not spending even a second thinking about code formatting and indentation is huge.&lt;/p&gt;

&lt;p&gt;I know Django has been holding off on adopting it until a stable release was announced, so hopefully that will happen soon.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/llanga/status/1487530230512295940"&gt;Łukasz Langa&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/lukasz-langa"&gt;lukasz-langa&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/black"&gt;black&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="python"/><category term="lukasz-langa"/><category term="black"/></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>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>Black Onlline Demo</title><link href="https://simonwillison.net/2018/Apr/25/black-onlline-demo/#atom-tag" rel="alternate"/><published>2018-04-25T05:17:44+00:00</published><updated>2018-04-25T05:17:44+00:00</updated><id>https://simonwillison.net/2018/Apr/25/black-onlline-demo/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://black.now.sh/"&gt;Black Onlline Demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Black is “the uncompromising Python code formatter” by Łukasz Langa—it reformats Python code to a very carefully thought out styleguide, and provides almost no options for how code should be formatted. It’s reminiscent of gofmt. José Padilla built a handy online tool for trying it out in your browser.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/llanga/status/986381413455806464"&gt;@llanga&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/lukasz-langa"&gt;lukasz-langa&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/black"&gt;black&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="lukasz-langa"/><category term="black"/></entry></feed>