<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: read-the-docs</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/read-the-docs.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2024-09-23T21:45:15+00:00</updated><author><name>Simon Willison</name></author><entry><title>simonw/docs cookiecutter template</title><link href="https://simonwillison.net/2024/Sep/23/docs-cookiecutter/#atom-tag" rel="alternate"/><published>2024-09-23T21:45:15+00:00</published><updated>2024-09-23T21:45:15+00:00</updated><id>https://simonwillison.net/2024/Sep/23/docs-cookiecutter/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/docs"&gt;simonw/docs cookiecutter template&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Over the last few years I’ve settled on the combination of &lt;a href="https://www.sphinx-doc.org/"&gt;Sphinx&lt;/a&gt;, the &lt;a href="https://github.com/pradyunsg/furo"&gt;Furo&lt;/a&gt; theme and the &lt;a href="https://myst-parser.readthedocs.io/en/latest/"&gt;myst-parser&lt;/a&gt; extension (enabling Markdown in place of reStructuredText) as my documentation toolkit of choice, maintained in GitHub and hosted using &lt;a href="https://about.readthedocs.com/"&gt;ReadTheDocs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; and &lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper&lt;/a&gt; projects are two examples of that stack in action.&lt;/p&gt;
&lt;p&gt;Today I wanted to spin up a new documentation site so I finally took the time to construct a &lt;a href="https://cookiecutter.readthedocs.io/"&gt;cookiecutter&lt;/a&gt; template for my preferred configuration. You can use it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipx install cookiecutter
cookiecutter gh:simonw/docs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or with &lt;a href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv tool run cookiecutter gh:simonw/docs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Answer a few questions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[1/3] project (): shot-scraper
[2/3] author (): Simon Willison
[3/3] docs_directory (docs):
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it creates a &lt;code&gt;docs/&lt;/code&gt; directory ready for you to start editing docs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd docs
pip install -r requirements.txt
make livehtml
&lt;/code&gt;&lt;/pre&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&lt;/a&gt;, &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/markdown"&gt;markdown&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sphinx-docs"&gt;sphinx-docs&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/uv"&gt;uv&lt;/a&gt;&lt;/p&gt;



</summary><category term="documentation"/><category term="projects"/><category term="python"/><category term="markdown"/><category term="cookiecutter"/><category term="sphinx-docs"/><category term="read-the-docs"/><category term="uv"/></entry><entry><title>AI crawlers need to be more respectful</title><link href="https://simonwillison.net/2024/Jul/25/ai-crawlers-need-to-be-more-respectful/#atom-tag" rel="alternate"/><published>2024-07-25T20:02:25+00:00</published><updated>2024-07-25T20:02:25+00:00</updated><id>https://simonwillison.net/2024/Jul/25/ai-crawlers-need-to-be-more-respectful/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://about.readthedocs.com/blog/2024/07/ai-crawlers-abuse/"&gt;AI crawlers need to be more respectful&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Eric Holscher:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;At Read the Docs, we host documentation for many projects and are generally bot friendly, but the behavior of AI crawlers is currently causing us problems. We have noticed AI crawlers aggressively pulling content, seemingly without basic checks against abuse.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One crawler downloaded 73 TB of zipped HTML files just in Month, racking up $5,000 in bandwidth charges!

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/crawling"&gt;crawling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/eric-holscher"&gt;eric-holscher&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ethics"&gt;ethics&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&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/ai-ethics"&gt;ai-ethics&lt;/a&gt;&lt;/p&gt;



</summary><category term="crawling"/><category term="eric-holscher"/><category term="ethics"/><category term="ai"/><category term="read-the-docs"/><category term="ai-ethics"/></entry><entry><title>Weeknotes: python_requires, documentation SEO</title><link href="https://simonwillison.net/2022/Jan/25/weeknotes/#atom-tag" rel="alternate"/><published>2022-01-25T23:54:52+00:00</published><updated>2022-01-25T23:54:52+00:00</updated><id>https://simonwillison.net/2022/Jan/25/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;Fixed Datasette on Python 3.6 for the last time. Worked on documentation infrastructure improvements. Spent some time with Fly Volumes.&lt;/p&gt;
&lt;h4&gt;Datasette 0.60.1 for Python 3.6&lt;/h4&gt;
&lt;p&gt;I got &lt;a href="https://github.com/simonw/datasette/issues/1609"&gt;a report&lt;/a&gt; that users of Python 3.6 were seeing errors when they tried to install Datasette.&lt;/p&gt;
&lt;p&gt;I actually &lt;a href="https://github.com/simonw/datasette/issues/1577"&gt;dropped support&lt;/a&gt; for 3.6 a few weeks ago, but that shouldn't have affected the already released Datasette 0.60 - so something was clearly wrong.&lt;/p&gt;
&lt;p&gt;This lead me to finally get my head around how &lt;code&gt;pip install&lt;/code&gt; handles Python version support. It's actually a very neat system which I hadn't previously taken the time to understand.&lt;/p&gt;
&lt;p&gt;Python packages can (and should!) provide a &lt;code&gt;python_requires=&lt;/code&gt; line in their &lt;code&gt;setup.py&lt;/code&gt;. That line for Datasette currently looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python_requires="&amp;gt;=3.7"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But in the 0.60 release it was still this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python_requires="&amp;gt;=3.6"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run &lt;code&gt;pip install package&lt;/code&gt; this becomes part of the &lt;code&gt;pip&lt;/code&gt; resolution mechanism - it will default to attempting to install the highest available version of the package that supports your version of Python.&lt;/p&gt;
&lt;p&gt;So why did &lt;code&gt;pip install datasette&lt;/code&gt; break? It turned out that one of Datasette's dependencies, &lt;a href="https://www.uvicorn.org/"&gt;Uvicorn&lt;/a&gt;, had dropped support for Python 3.6 but did not have a &lt;code&gt;python_requires&lt;/code&gt; indicator that pip could use to resolve the correct version.&lt;/p&gt;
&lt;p&gt;Coincidentally, Uvicorn actually added &lt;code&gt;python_requires&lt;/code&gt; just &lt;a href="https://github.com/encode/uvicorn/pull/1328"&gt;a few weeks ago&lt;/a&gt; - but it wasn't out in a release yet, so &lt;code&gt;pip install&lt;/code&gt; couldn't take it into account.&lt;/p&gt;
&lt;p&gt;I raised this issue with the Uvicorn development team and  they turned around a fix really promptly - &lt;a href="https://github.com/encode/uvicorn/releases/tag/0.17.0.post1"&gt;0.17.0.post1&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But before I had seen how fast the Uvicorn team could move I figured out how to fix the issue myself, thanks to &lt;a href="https://twitter.com/samuel_hames/status/1484327636860293121"&gt;a tip from Sam Hames&lt;/a&gt; on Twitter.&lt;/p&gt;
&lt;p&gt;The key to fixing it was &lt;a href="https://www.python.org/dev/peps/pep-0508/#environment-markers"&gt;environment markers&lt;/a&gt;, a feature of Python's dependency resolution system that allows you to provide extra rules for when a dependency should be used.&lt;/p&gt;
&lt;p&gt;Here's an &lt;code&gt;install_requires=&lt;/code&gt; example showing these in action:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;install_requires&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[
    &lt;span class="pl-s"&gt;"uvicorn~=0.11"&lt;/span&gt;,
    &lt;span class="pl-s"&gt;'uvicorn&amp;lt;=0.16.0;python_version&amp;lt;="3.6"'&lt;/span&gt;
]&lt;/pre&gt;
&lt;p&gt;This will install a Uvicorn version that loosely matches 0.11, but over-rides that rule to specify that it must be &lt;code&gt;&amp;lt;=0.16.0&lt;/code&gt; if the user's Python version is 3.6 or less.&lt;/p&gt;
&lt;p&gt;Since Datasette 0.60.1 will be the last version of Datasette to support Python 3.6, I decided to play it safe and pin the dependencies of every library to the most recent version that I have tested in Python 3.6 myself. Here's &lt;a href="https://github.com/simonw/datasette/blob/0.60.1/setup.py#L44-L78"&gt;the setup.py file&lt;/a&gt; I constructed for that.&lt;/p&gt;
&lt;p&gt;This ties into a larger open question for me about Datasette's approach to pinning dependencies.&lt;/p&gt;
&lt;p&gt;The rule of thumb I've heard is that you should pin dependencies for standalone released tools but leave dependencies loose for libraries that people will use as dependencies in their own projects - ensuring those users can run with different dependency versions if their projects require them.&lt;/p&gt;
&lt;p&gt;Datasette is &lt;em&gt;mostly&lt;/em&gt; a standalone tool - but it can also be used as a library. I'm actually planning to make its use as a library more obvious through &lt;a href="https://github.com/simonw/datasette/issues/1398"&gt;improvements to the documentation&lt;/a&gt; in the future.&lt;/p&gt;
&lt;p&gt;As such, pinning exact versions doesn't feel quite right to me.&lt;/p&gt;
&lt;p&gt;Maybe the solution here is to split the reusable library parts of Datasette out into a separate package - maybe &lt;code&gt;datasette-core&lt;/code&gt; - and have the &lt;code&gt;datasette&lt;/code&gt; CLI package depend on exact pinned dependencies while the &lt;code&gt;datasette-core&lt;/code&gt; library uses loose dependencies instead.&lt;/p&gt;
&lt;p&gt;Still thinking about this.&lt;/p&gt;
&lt;h4&gt;Datasette documentation tweaks&lt;/h4&gt;
&lt;p&gt;Datasette uses &lt;a href="https://readthedocs.org/"&gt;Read The Docs&lt;/a&gt; to host the documentation. Among other benefits, this makes it easy to host multiple documentation versions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.datasette.io/en/latest/"&gt;docs.datasette.io/en/latest/&lt;/a&gt; is the latest version of the documentation, continuously deployed from the &lt;code&gt;main&lt;/code&gt; branch on GitHub&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.datasette.io/en/stable/"&gt;docs.datasette.io/en/stable/&lt;/a&gt; is the documentation for the most recent stable (non alpha or beta) release - currently 0.60.1. This is the version you get when you run &lt;code&gt;pip install datasette&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.datasette.io/en/0.59/"&gt;docs.datasette.io/en/0.59/&lt;/a&gt; is the documentation for version 0.59 - and every version back to 0.22.1 is hosted under similar URLs, currently covering 73 different releases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those previous versions all automatically show a note at the top of the page warning that this is out-dated documentation and linking back to stable - a feature which Read The Docs provides automatically.&lt;/p&gt;
&lt;p&gt;But... I noticed that &lt;code&gt;/en/latest/&lt;/code&gt; didn't do this. I wanted a warning banner to let people know that they were looking at the in-development version of the documentation.&lt;/p&gt;
&lt;p&gt;After some digging around, I fixed it using &lt;a href="https://til.simonwillison.net/readthedocs/link-from-latest-to-stable"&gt;a little bit of extra JavaScript&lt;/a&gt; added to my documentation template. Here's the key implementation detail:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;jQuery&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;$&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Show banner linking to /stable/ if this is a /latest/ page&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;!&lt;/span&gt;&lt;span class="pl-pds"&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;span class="pl-cce"&gt;\/&lt;/span&gt;latest&lt;span class="pl-cce"&gt;\/&lt;/span&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;test&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;location&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;pathname&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-k"&gt;var&lt;/span&gt; &lt;span class="pl-s1"&gt;stableUrl&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;location&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;pathname&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;replace&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"/latest/"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"/stable/"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Check it's not a 404&lt;/span&gt;
  &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;stableUrl&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;method&lt;/span&gt;: &lt;span class="pl-s"&gt;"HEAD"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;status&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-c1"&gt;200&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Page exists, insert a warning banner linking to it&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This uses &lt;code&gt;fetch()&lt;/code&gt; to make an HTTP HEAD request for the stable documentation page, and inserts a warning banner only if that page isn't a 404. This avoids linking to a non-existant documentation page if it has been created in development but not yet released as part of a stable release. &lt;a href="https://docs.datasette.io/en/latest/csv_export.html"&gt;Example here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/latest-docs-warning.png" alt="Screenshot of the documentation page with a banner that says: This documentation covers the development version of Datasette. See this page for the current stable release." style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Thinking about this problem got me thinking about SEO.&lt;/p&gt;
&lt;p&gt;A problem I've had with other projects that host multiple versions of their documentation is that sometimes I'll search on Google and end up landing on a page covering a much older version of their project. I think I've had this happen for both PostgreSQL and Python in the past.&lt;/p&gt;
&lt;p&gt;What's best practice for avoiding this? I &lt;a href="https://twitter.com/simonw/status/1484287724773203971"&gt;asked on Twitter&lt;/a&gt; and also started digging around for answers. "If in doubt, imitate Django" is a good general rule of thumb, so I had a look at how Django did this and spotted the following in the HTML of one of their &lt;a href="https://docs.djangoproject.com/en/2.2/topics/db/"&gt;prior version pages&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-text-html-basic"&gt;&lt;pre&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;link&lt;/span&gt; &lt;span class="pl-c1"&gt;rel&lt;/span&gt;="&lt;span class="pl-s"&gt;canonical&lt;/span&gt;" &lt;span class="pl-c1"&gt;href&lt;/span&gt;="&lt;span class="pl-s"&gt;https://docs.djangoproject.com/en/4.0/topics/db/&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So Django are using the &lt;a href="https://developers.google.com/search/docs/advanced/crawling/consolidate-duplicate-urls"&gt;rel=canonical&lt;/a&gt; tag to point crawlers towards their most recent release.&lt;/p&gt;
&lt;p&gt;I decided to implement that myself... and then discovered that the Datasette documentation was doing it already! Read The Docs &lt;a href="https://docs.readthedocs.io/en/latest/custom_domains.html#canonical-urls"&gt;implement this piece&lt;/a&gt; of SEO best practice out of the box.&lt;/p&gt;
&lt;h4&gt;Datasette on Fly volumes&lt;/h4&gt;
&lt;p&gt;This one isn't released yet, but I made some good progress on it this week.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://fly.io/"&gt;Fly.io&lt;/a&gt; announced this week that they would be providing 3GB of volume storage to accounts on their free tier. They called this announcement &lt;a href="https://fly.io/blog/free-postgres/"&gt;Free Postgres Databases&lt;/a&gt;, but tucked away in the blog post was this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The lede is "free Postgres" because that's what matters to full stack apps. You don't have to use these for Postgres. If SQLite is more your jam, mount up to 3GB of volumes and use "free SQLite." Yeah, we're probably underselling that.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(There is &lt;a href="https://twitter.com/mrkurt/status/1484609372114272261"&gt;evidence&lt;/a&gt; that they may have been &lt;a href="https://xkcd.com/356/"&gt;nerd sniping&lt;/a&gt; me with that paragraph.)&lt;/p&gt;
&lt;p&gt;I have a plugin called &lt;a href="https://datasette.io/plugins/datasette-publish-fly"&gt;datasette-publish-fly&lt;/a&gt; which publishes Datasette instances to Fly. Obviously that needs to grow support for configuring volumes!&lt;/p&gt;
&lt;p&gt;I've so far &lt;a href="https://github.com/simonw/datasette-publish-fly/issues/11"&gt;completed the research&lt;/a&gt; on how that feature should work. The next step is to finish &lt;a href="https://github.com/simonw/datasette-publish-fly/issues/10"&gt;implementing the feature&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;sqlite-utils --help&lt;/h4&gt;
&lt;p&gt;I pushed out minor release &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-22-1"&gt;sqlite-utils 3.22.1&lt;/a&gt; today with one notable improvement: every single one of the 39 commands in the CLI tool now includes an example of usage as part of the &lt;code&gt;--help&lt;/code&gt; text.&lt;/p&gt;

&lt;p&gt;This feature was inspired by the new &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli-reference.html#cli-reference"&gt;CLI reference page&lt;/a&gt; in the documentation, which shows the help output for every command on one page - making it much easier to spot potential improvements.&lt;/p&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.22.1"&gt;3.22.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/sqlite-utils/releases"&gt;94 releases total&lt;/a&gt;) - 2022-01-26
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/s3-credentials"&gt;s3-credentials&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/s3-credentials/releases/tag/0.10"&gt;0.10&lt;/a&gt; - (&lt;a href="https://github.com/simonw/s3-credentials/releases"&gt;10 releases total&lt;/a&gt;) - 2022-01-25
&lt;br /&gt;A tool for creating credentials for accessing S3 buckets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette/releases/tag/0.60.1"&gt;0.60.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette/releases"&gt;106 releases total&lt;/a&gt;) - 2022-01-21
&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/sqlite/json-extract-path"&gt;json_extract() path syntax in SQLite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/aws/helper-for-boto-aws-pagination"&gt;Helper function for pagination using AWS boto3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/pixelmator/pixel-editing-favicon"&gt;Pixel editing a favicon with Pixelmator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/readthedocs/documentation-seo-canonical"&gt;Promoting the stable version of the documentation using rel=canonical&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/readthedocs/link-from-latest-to-stable"&gt;Linking from /latest/ to /stable/ on Read The Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/fly/undocumented-graphql-api"&gt;Using the undocumented Fly GraphQL API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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/seo"&gt;seo&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/read-the-docs"&gt;read-the-docs&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="python"/><category term="seo"/><category term="datasette"/><category term="weeknotes"/><category term="fly"/><category term="sqlite-utils"/><category term="read-the-docs"/></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>Adding Sphinx autodoc to a project, and configuring Read The Docs to build it</title><link href="https://simonwillison.net/2021/Aug/11/sphinx-autodoc/#atom-tag" rel="alternate"/><published>2021-08-11T01:21:28+00:00</published><updated>2021-08-11T01:21:28+00:00</updated><id>https://simonwillison.net/2021/Aug/11/sphinx-autodoc/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/sphinx/sphinx-autodoc"&gt;Adding Sphinx autodoc to a project, and configuring Read The Docs to build it&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
My TIL notes from figuring out how to use sphinx-autodoc for the sqlite-utils reference documentation today.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sphinx-docs"&gt;sphinx-docs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/read-the-docs"&gt;read-the-docs&lt;/a&gt;&lt;/p&gt;



</summary><category term="documentation"/><category term="sqlite-utils"/><category term="sphinx-docs"/><category term="read-the-docs"/></entry><entry><title>The subset of reStructuredText worth committing to memory</title><link href="https://simonwillison.net/2018/Aug/25/restructuredtext/#atom-tag" rel="alternate"/><published>2018-08-25T18:44:29+00:00</published><updated>2018-08-25T18:44:29+00:00</updated><id>https://simonwillison.net/2018/Aug/25/restructuredtext/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/ReStructuredText"&gt;reStructuredText&lt;/a&gt; is the standard for documentation in the Python world.&lt;/p&gt;
&lt;p&gt;It’s a bit weird. It’s like &lt;a href="https://en.wikipedia.org/wiki/Markdown"&gt;Markdown&lt;/a&gt; but older, more feature-filled and in my experience significantly harder to remember.&lt;/p&gt;
&lt;p&gt;There are plenty of guides and cheatsheets out there, but when writing simple documentation for software projects I think there’s a subset that is worth committing to memory. I’ll describe that subset here.&lt;/p&gt;
&lt;p&gt;First though: when writing reStructuredText having a live preview render is extremely useful. I use &lt;a href="http://rst.ninjs.org/"&gt;rst.ninjs.org&lt;/a&gt; for this. If you don’t trust that hosted version (it round-trips your documentation through the server in order to render it) you can run a local copy instead using the &lt;a href="https://github.com/anru/rsted"&gt;underlying source code&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Paragraphs_10"&gt;&lt;/a&gt;Paragraphs&lt;/h3&gt;
&lt;p&gt;Paragraphs work the same way as Markdown and plain text. They are nice and easy.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This is the first paragraph. No need to wrap the text (though you can wrap at e.g. 80 characters without affecting rendering).

This is the second paragraph.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="Headings_20"&gt;&lt;/a&gt;Headings&lt;/h3&gt;
&lt;p&gt;reStructuredText section headings are a little surprising.&lt;/p&gt;
&lt;p&gt;Markdown has multiple levels of heading, each with a different number of prefix hashes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Markdown heading level 1
## Markdown heading level 2
..
###### Markdown heading fevel 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In reStructuredText there is no single format for these different levels. Instead, the format you use first will be treated as an H1, the next format as an H2 and so on. Here’s the &lt;a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#sections"&gt;description from the official documentation&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sections are identified through their titles, which are marked up with adornment: “underlines” below the title text, or underlines and matching “overlines” above the title. An underline/overline is a single repeated punctuation character that begins in column 1 and forms a line extending at least as far as the right edge of the title text. Specifically, an underline/overline character may be any non-alphanumeric printable 7-bit ASCII character. […] There may be any number of levels of section titles, although some output formats may have limits (HTML has 6 levels).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is deeply confusing. I suggest instead standardizing on the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=====================
 This is a heading 1
=====================
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This heading has = signs both above and below, and they extend past the text by a single character in each direction.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This is a heading 2
===================

This is a heading 3
-------------------

This is a heading 4
~~~~~~~~~~~~~~~~~~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need more levels, you can invent them using whatever character you like - but try to stay consistent within your project.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Bulleted_lists_54"&gt;&lt;/a&gt;Bulleted lists&lt;/h3&gt;
&lt;p&gt;As with headings, you can use a variety of characters for these. I suggest sticking with asterisks.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A blank line is required before starting a bulleted list.

* A bullet point
* Another bullet point
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you decide to wrap your text (I tend not to) you must maintain the indentation on the wrapped lines:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* A bulleted list item. Since the text is wrapped each subsequent
  line of text must be indented by two spaces.
* Second list item.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nested lists are supported, but you MUST leave a blank line above the first  inner list bullet point or they won't work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* This is the first bullet list item. Here comes a sub-list:

  * Hello sublist
  * Sublist two

* Back to the parent list.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="Inline_markup_78"&gt;&lt;/a&gt;Inline markup&lt;/h3&gt;
&lt;p&gt;I only use three inline markup features: bold, italic and code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;**Bold text** is surrounded by two asterisks.

*Italic text* is one asterisk.

``inline code`` uses two backticks at either side of the code.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="Links_90"&gt;&lt;/a&gt;Links&lt;/h3&gt;
&lt;p&gt;Links are my least favorite feature of reStructuredText. There are several different ways of including them, but the one I use most often (and hence have committed to memory) is this one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`a link, note the trailing underscores &amp;lt;http://example.com&amp;gt;`__
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So that’s a backtick at the start, then the link text, then the URL contained in greater than / less than symbols, then another backtick and then TWO underscores to finish it off.&lt;/p&gt;
&lt;p&gt;Why two underscores? Because if you only use one, the text part of the link is remembered and can be used to duplicate your link later on - see example below. In my experience this is more trouble than it’s worth.&lt;/p&gt;
&lt;p&gt;A more complex link syntax example (&lt;a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#embedded-uris-and-aliases"&gt;documented here&lt;/a&gt;) looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;See the `Python home page`_ for info.

This link_ is an alias to the link above.

.. _Python home page: http://www.python.org
.. _link: `Python home page`_
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I can’t remember this at all, so I stick with the anonymous hyperlink syntax instead.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Code_blocks_111"&gt;&lt;/a&gt;Code blocks&lt;/h3&gt;
&lt;p&gt;The easiest way to embed a block of code is like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::

    # This is a code example
    print(&amp;quot;It needs to be indented&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;::&lt;/code&gt; indicates that a code block is coming up. The blank line after the &lt;code&gt;::&lt;/code&gt; before the indentation starts is required.&lt;/p&gt;
&lt;p&gt;Most renderers have the ability to apply syntax highlighting. To specify that a block should have syntax highlighting for a specific language, replace the &lt;code&gt;::&lt;/code&gt; in the above example with one of the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.. code-block:: sql

.. code-block:: javascript

.. code-block:: python
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="Images_130"&gt;&lt;/a&gt;Images&lt;/h3&gt;
&lt;p&gt;There are &lt;a href="http://docutils.sourceforge.net/docs/ref/rst/directives.html#images"&gt;plenty of options&lt;/a&gt; for embedding images, but the most basic syntax (worth remembering) looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.. image:: full_text_search.png
   :alt: alternate text
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will embed an image of that filename that sits in the same directory as the document itself.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Internal_references_138"&gt;&lt;/a&gt;Internal references&lt;/h3&gt;
&lt;p&gt;In my opinion this is the key feature that makes reStructuredText more powerful than Markdown for larger documentation projects.&lt;/p&gt;
&lt;p&gt;Again, there is a vast and complex array of options around this, but the key thing to remember is how to add a reference name to a specific section and how to link to that section later on.&lt;/p&gt;
&lt;p&gt;Names are applied to section headings, by adding some magic text before the heading itself. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.. _full_text_search:

Full-text search
================
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the format: two periods, then a space, then an underscore, then the label, then a colon at the end.&lt;/p&gt;
&lt;p&gt;The label &lt;code&gt;full_text_search&lt;/code&gt; is now associated with that heading. I can link to it from any page in my documentation project like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:ref:`full_text_search`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the leading underscore isn’t included in this reference.&lt;/p&gt;
&lt;p&gt;The link text displayed will be the text of the heading, in this case “Full-text search”. If I want to replace that link text with something custom, I can do so like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Learn about the :ref:`search feature &amp;lt;full_text_search&amp;gt;`.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This syntax is similar to the inline hyperlink syntax described above.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Learning_more_165"&gt;&lt;/a&gt;Learning more&lt;/h3&gt;
&lt;p&gt;I extracted the patterns I describe in this post from the &lt;a href="https://datasette.readthedocs.io/"&gt;Datasette documentation&lt;/a&gt; - I encourage you to &lt;a href="https://github.com/simonw/datasette/tree/master/docs"&gt;dig around in the source code&lt;/a&gt; to see how it all works.&lt;/p&gt;
&lt;p&gt;The definitive guide to reStructuredText is &lt;a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html"&gt;the reStructuredText Markup Specification&lt;/a&gt;. My favourite of the various quick references is the &lt;a href="https://thomas-cokelaer.info/tutorials/sphinx/rest_syntax.html"&gt;Restructured Text (reST) and Sphinx CheatSheet&lt;/a&gt; by Thomas Cokelaer.&lt;/p&gt;

&lt;p&gt;I'm a huge fan of &lt;a href="https://readthedocs.org/"&gt;Read the Docs&lt;/a&gt; for hosting documentation - it's the key reason I use reStructuredText in my projects. Unsurprisingly, they offer &lt;a href="https://docs.readthedocs.io/en/latest/"&gt;extensive documentation&lt;/a&gt; to help you make the most of their platform.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/restructuredtext"&gt;restructuredtext&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sphinx-docs"&gt;sphinx-docs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/read-the-docs"&gt;read-the-docs&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="documentation"/><category term="python"/><category term="restructuredtext"/><category term="sphinx-docs"/><category term="read-the-docs"/></entry></feed>