<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: djp</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/djp.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2024-11-13T01:14:22+00:00</updated><author><name>Simon Willison</name></author><entry><title>django-plugin-django-debug-toolbar</title><link href="https://simonwillison.net/2024/Nov/13/django-plugin-django-debug-toolbar/#atom-tag" rel="alternate"/><published>2024-11-13T01:14:22+00:00</published><updated>2024-11-13T01:14:22+00:00</updated><id>https://simonwillison.net/2024/Nov/13/django-plugin-django-debug-toolbar/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tomviner/django-plugin-django-debug-toolbar"&gt;django-plugin-django-debug-toolbar&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Tom Viner built a plugin for my &lt;a href="https://djp.readthedocs.io/"&gt;DJP Django plugin system&lt;/a&gt; that configures the excellent &lt;a href="https://django-debug-toolbar.readthedocs.io/"&gt;django-debug-toolbar&lt;/a&gt; debugging tool.&lt;/p&gt;
&lt;p&gt;You can see everything it sets up for you &lt;a href="https://github.com/tomviner/django-plugin-django-debug-toolbar/blob/0.3.2/django_plugin_django_debug_toolbar/__init__.py"&gt;in this Python code&lt;/a&gt;: it configures installed apps, URL patterns and middleware and sets the &lt;code&gt;INTERNAL_IPS&lt;/code&gt; and &lt;code&gt;DEBUG&lt;/code&gt; settings.&lt;/p&gt;
&lt;p&gt;Here are Tom's &lt;a href="https://github.com/tomviner/django-plugin-django-debug-toolbar/issues/1"&gt;running notes&lt;/a&gt; as he created the plugin.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/tomviner/status/1856498919359828152"&gt;@tomviner&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/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djp"&gt;djp&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="plugins"/><category term="djp"/></entry><entry><title>Weeknotes: Three podcasts, two trips and a new plugin system</title><link href="https://simonwillison.net/2024/Sep/30/weeknotes/#atom-tag" rel="alternate"/><published>2024-09-30T17:43:22+00:00</published><updated>2024-09-30T17:43:22+00:00</updated><id>https://simonwillison.net/2024/Sep/30/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;I fell behind a bit on my weeknotes. Here's most of what I've been doing in September.&lt;/p&gt;
&lt;h4 id="lisbon-portugal-and-durham-north-carolina"&gt;Lisbon, Portugal and Durham, North Carolina&lt;/h4&gt;
&lt;p&gt;I had two trips this month. The first was a short visit to Lisbon, Portugal for the Python Software Foundation's annual board retreat. This inspired me to write about &lt;a href="https://simonwillison.net/2024/Sep/18/board-of-the-python-software-foundation/"&gt;Things I've learned serving on the board of the Python Software Foundation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second was to Durham, North Carolina for DjangoCon US 2024. I wrote about that one in &lt;a href="https://simonwillison.net/2024/Sep/27/themes-from-djangocon-us-2024/"&gt;Themes from DjangoCon US 2024&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My talk at DjangoCon was about plugin systems, and in a classic example of conference-driven development I ended up writing and releasing a new plugin system for Django in preparation for that talk. I introduced that in &lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/"&gt;DJP: A plugin system for Django&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="podcasts"&gt;Podcasts&lt;/h4&gt;
&lt;p&gt;I haven't been a podcast guest &lt;a href="https://simonwillison.net/search/?year=2024&amp;amp;month=1&amp;amp;tag=podcasts"&gt;since January&lt;/a&gt;, and then three came along at once! All three appearences involved LLMs in some way but I don't think there was a huge amount of overlap in terms of what I actually said.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I went on &lt;a href="https://simonwillison.net/2024/Sep/10/software-misadventures/"&gt;The Software Misadventures Podcast&lt;/a&gt; to talk about my career to-date.&lt;/li&gt;
&lt;li&gt;My appearance &lt;a href="https://simonwillison.net/2024/Sep/20/using-llms-for-code/"&gt;on TWIML&lt;/a&gt; dug into ways in which I use Claude and ChatGPT to help me write code.&lt;/li&gt;
&lt;li&gt;I was the guest for the inaugral episode of Gergely Orosz's &lt;a href="https://newsletter.pragmaticengineer.com/p/ai-tools-for-software-engineers-simon-willison"&gt;Pragmatic Engineer Podcast&lt;/a&gt;, which ended up touching on a whole array of different topics relevant to modern software engineering, from the importance of open source to the impact AI tools are likely to have on our industry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gergely has been sharing neat edited snippets from our conversation on Twitter. Here's &lt;a href="https://twitter.com/GergelyOrosz/status/1839682428471779596"&gt;one on RAG&lt;/a&gt; and another about &lt;a href="https://twitter.com/GergelyOrosz/status/1840779737297260646"&gt;how open source has been the the biggest productivity boost&lt;/a&gt; of my career.&lt;/p&gt;
&lt;h4 id="on-the-blog"&gt;On the blog&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/29/notebooklm-audio-overview/"&gt;NotebookLM's automatically generated podcasts are surprisingly effective&lt;/a&gt; - Sept. 29, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/27/themes-from-djangocon-us-2024/"&gt;Themes from DjangoCon US 2024&lt;/a&gt; - Sept. 27, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/"&gt;DJP: A plugin system for Django&lt;/a&gt; - Sept. 25, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/20/using-llms-for-code/"&gt;Notes on using LLMs for code&lt;/a&gt; - Sept. 20, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/18/board-of-the-python-software-foundation/"&gt;Things I've learned serving on the board of the Python Software Foundation&lt;/a&gt; - Sept. 18, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/12/openai-o1/"&gt;Notes on OpenAI's new o1 chain-of-thought models&lt;/a&gt; - Sept. 12, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/10/software-misadventures/"&gt;Notes from my appearance on the Software Misadventures Podcast&lt;/a&gt; - Sept. 10, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/8/teresa-t-whale-pillar-point/"&gt;Teresa T is name of the whale in Pillar Point Harbor near Half Moon Bay&lt;/a&gt; - Sept. 8, 2024&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="museums"&gt;Museums&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.niche-museums.com/112"&gt;The Vincent and Ethel Simonetti Historic Tuba Collection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="releases"&gt;Releases&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/shot-scraper/releases/tag/1.5"&gt;shot-scraper 1.5&lt;/a&gt;&lt;/strong&gt; - 2024-09-27&lt;br /&gt;A command-line utility for taking automated screenshots of websites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-datasette/releases/tag/0.2"&gt;django-plugin-datasette 0.2&lt;/a&gt;&lt;/strong&gt; - 2024-09-26&lt;br /&gt;Django plugin to run Datasette inside of Django&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/djp/releases/tag/0.3.1"&gt;djp 0.3.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-26&lt;br /&gt;A plugin system for Django&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-gemini/releases/tag/0.1a5"&gt;llm-gemini 0.1a5&lt;/a&gt;&lt;/strong&gt; - 2024-09-24&lt;br /&gt;LLM plugin to access Google's Gemini family of models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-blog/releases/tag/0.1.1"&gt;django-plugin-blog 0.1.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-24&lt;br /&gt;A blog for Django as a DJP plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-database-url/releases/tag/0.1"&gt;django-plugin-database-url 0.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-24&lt;br /&gt;Django plugin for reading the DATABASE_URL environment variable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-django-header/releases/tag/0.1.1"&gt;django-plugin-django-header 0.1.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-23&lt;br /&gt;Add a Django-Compositions HTTP header to a Django app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-jina-api/releases/tag/0.1a0"&gt;llm-jina-api 0.1a0&lt;/a&gt;&lt;/strong&gt; - 2024-09-20&lt;br /&gt;Access Jina AI embeddings via their API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm/releases/tag/0.16"&gt;llm 0.16&lt;/a&gt;&lt;/strong&gt; - 2024-09-12&lt;br /&gt;Access large language models from the command-line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-acl/releases/tag/0.4a4"&gt;datasette-acl 0.4a4&lt;/a&gt;&lt;/strong&gt; - 2024-09-10&lt;br /&gt;Advanced permission management for Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-cmd/releases/tag/0.2a0"&gt;llm-cmd 0.2a0&lt;/a&gt;&lt;/strong&gt; - 2024-09-09&lt;br /&gt;Use LLM to generate and execute commands in your shell&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/files-to-prompt/releases/tag/0.3"&gt;files-to-prompt 0.3&lt;/a&gt;&lt;/strong&gt; - 2024-09-09&lt;br /&gt;Concatenate a directory full of files into a single prompt for use with LLMs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/json-flatten/releases/tag/0.3.1"&gt;json-flatten 0.3.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-07&lt;br /&gt;Python functions for flattening a JSON object to a single dictionary of pairs, and unflattening that dictionary back to a JSON object&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/csv-diff/releases/tag/1.2"&gt;csv-diff 1.2&lt;/a&gt;&lt;/strong&gt; - 2024-09-06&lt;br /&gt;Python CLI tool and library for diffing CSV and JSON files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette/releases/tag/1.0a16"&gt;datasette 1.0a16&lt;/a&gt;&lt;/strong&gt; - 2024-09-06&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-search-all/releases/tag/1.1.4"&gt;datasette-search-all 1.1.4&lt;/a&gt;&lt;/strong&gt; - 2024-09-06&lt;br /&gt;Datasette plugin for searching all searchable tables at once&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="tils"&gt;TILs&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/llms/streaming-llm-apis"&gt;How streaming LLM APIs work&lt;/a&gt; - 2024-09-21&lt;/li&gt;
&lt;/ul&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/podcasts"&gt;podcasts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/psf"&gt;psf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djp"&gt;djp&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="podcasts"/><category term="weeknotes"/><category term="psf"/><category term="llms"/><category term="djp"/></entry><entry><title>django-plugin-datasette</title><link href="https://simonwillison.net/2024/Sep/26/django-plugin-datasette/#atom-tag" rel="alternate"/><published>2024-09-26T21:57:52+00:00</published><updated>2024-09-26T21:57:52+00:00</updated><id>https://simonwillison.net/2024/Sep/26/django-plugin-datasette/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-datasette"&gt;django-plugin-datasette&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I did some more work on my &lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/"&gt;DJP plugin mechanism&lt;/a&gt; for Django at the DjangoCon US sprints today. I added a new plugin hook, &lt;a href="https://djp.readthedocs.io/en/latest/plugin_hooks.html#asgi-wrapper"&gt;asgi_wrapper()&lt;/a&gt;, released in &lt;a href="https://github.com/simonw/djp/releases/tag/0.3"&gt;DJP 0.3&lt;/a&gt; and inspired by the similar hook &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#asgi-wrapper-datasette"&gt;in Datasette&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The hook only works for Django apps that are &lt;a href="https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/"&gt;served using ASGI&lt;/a&gt;. It allows plugins to add their own wrapping ASGI middleware around the Django app itself, which means they can do things like attach entirely separate ASGI-compatible applications outside of the regular Django request/response cycle.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; is one of those ASGI-compatible applications!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;django-plugin-datasette&lt;/code&gt; uses that new hook to configure a new URL, &lt;code&gt;/-/datasette/&lt;/code&gt;, which serves a full Datasette instance that scans through Django’s &lt;code&gt;settings.DATABASES&lt;/code&gt; dictionary and serves an explore interface on top of any SQLite databases it finds there.&lt;/p&gt;
&lt;p&gt;It doesn’t support authentication yet, so this will expose your entire database contents - probably best used as a local debugging tool only.&lt;/p&gt;
&lt;p&gt;I did borrow some code from the &lt;a href="https://github.com/simonw/datasette-mask-columns"&gt;datasette-mask-columns&lt;/a&gt; plugin to ensure that the &lt;code&gt;password&lt;/code&gt; column in the &lt;code&gt;auth_user&lt;/code&gt; column is reliably redacted. That column contains a heavily salted hashed password so exposing it isn’t necessarily a disaster, but I like to default to keeping hashes safe.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djp"&gt;djp&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="plugins"/><category term="projects"/><category term="sqlite"/><category term="datasette"/><category term="djp"/></entry><entry><title>Solving a bug with o1-preview, files-to-prompt and LLM</title><link href="https://simonwillison.net/2024/Sep/25/o1-preview-llm/#atom-tag" rel="alternate"/><published>2024-09-25T18:41:13+00:00</published><updated>2024-09-25T18:41:13+00:00</updated><id>https://simonwillison.net/2024/Sep/25/o1-preview-llm/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://gist.github.com/simonw/03776d9f80534aa8e5348580dc6a800b"&gt;Solving a bug with o1-preview, files-to-prompt and LLM&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I added &lt;a href="https://github.com/simonw/djp/issues/10"&gt;a new feature&lt;/a&gt; to DJP this morning: you can now have plugins specify their middleware in terms of how it should be positioned relative to other middleware - inserted directly before or directly after &lt;code&gt;django.middleware.common.CommonMiddleware&lt;/code&gt; for example.&lt;/p&gt;
&lt;p&gt;At one point I got stuck with a weird test failure, and after ten minutes of head scratching I decided to pipe the entire thing into OpenAI's &lt;code&gt;o1-preview&lt;/code&gt; to see if it could spot the problem. I used &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; to gather the code and &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; to run the prompt:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt &lt;span class="pl-k"&gt;**&lt;/span&gt;/&lt;span class="pl-k"&gt;*&lt;/span&gt;.py -c &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m o1-preview &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;The middleware test is failing showing all of these - why is MiddlewareAfter repeated so many times?&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;['MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware2', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware4', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware2', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware', 'MiddlewareBefore']&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The model whirled away for a few seconds and spat out &lt;a href="https://gist.github.com/simonw/03776d9f80534aa8e5348580dc6a800b#response"&gt;an explanation&lt;/a&gt; of the problem - one of my middleware classes was accidentally calling &lt;code&gt;self.get_response(request)&lt;/code&gt; in two different places.&lt;/p&gt;
&lt;p&gt;I did enjoy how o1 attempted to reference the &lt;a href="https://docs.djangoproject.com/en/5.1/topics/http/middleware/#writing-your-own-middleware"&gt;relevant Django documentation&lt;/a&gt; and then half-repeated, half-hallucinated a quote from it:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Reference: From the Django documentation on writing middleware: Each middleware component is responsible for doing some specific function. They accept the request, do something, and pass the request to the next middleware component (if needed). They can also modify the response before sending it back to the client." src="https://static.simonwillison.net/static/2024/o1-hallucination.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;This took 2,538 input tokens and 4,354 output tokens - &lt;a href="https://gist.github.com/simonw/03776d9f80534aa8e5348580dc6a800b?permalink_comment_id=5207703#gistcomment-5207703"&gt;by my calculations&lt;/a&gt; at $15/million input and $60/million output that prompt cost just under 30 cents.


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



</summary><category term="ai"/><category term="openai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="o1"/><category term="djp"/><category term="llm-reasoning"/><category term="files-to-prompt"/></entry><entry><title>DJP: A plugin system for Django</title><link href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#atom-tag" rel="alternate"/><published>2024-09-25T14:00:42+00:00</published><updated>2024-09-25T14:00:42+00:00</updated><id>https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;strong&gt;&lt;a href="https://djp.readthedocs.io"&gt;DJP&lt;/a&gt;&lt;/strong&gt; is a new plugin mechanism for Django, built on top of &lt;a href="https://pluggy.readthedocs.io"&gt;Pluggy&lt;/a&gt;. I announced the first version of DJP during my talk yesterday at DjangoCon US 2024, &lt;a href="https://2024.djangocon.us/talks/how-to-design-and-implement-extensible-software-with-plugins/"&gt;How to design and implement extensible software with plugins&lt;/a&gt;. I'll post a full write-up of that talk once the video becomes available - this post describes DJP and how to use what I've built so far.&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#why-plugins-"&gt;Why plugins?&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#setting-up-djp"&gt;Setting up DJP&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#django-plugin-django-header"&gt;django-plugin-django-header&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#django-plugin-blog"&gt;django-plugin-blog&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#django-plugin-database-url"&gt;django-plugin-database-url&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#writing-a-plugin"&gt;Writing a plugin&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#writing-tests-for-plugins"&gt;Writing tests for plugins&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#why-call-it-djp-"&gt;Why call it DJP?&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/#what-s-next-for-djp-"&gt;What's next for DJP?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;  

&lt;h4 id="why-plugins-"&gt;Why plugins?&lt;/h4&gt;
&lt;p&gt;Django already has a thriving ecosystem of third-party apps and extensions. What can a plugin system add here?&lt;/p&gt;
&lt;p&gt;If you've ever installed a Django extension - such as &lt;a href="https://django-debug-toolbar.readthedocs.io"&gt;django-debug-toolbar&lt;/a&gt; or &lt;a href="https://django-extensions.readthedocs.io"&gt;django-extensions&lt;/a&gt; - you'll be familiar with the process. You &lt;code&gt;pip install&lt;/code&gt; the package, then add it to your list of &lt;code&gt;INSTALLED_APPS&lt;/code&gt; in &lt;code&gt;settings.py&lt;/code&gt; - and often configure other picees, like adding something to &lt;code&gt;MIDDLEWARE&lt;/code&gt; or updating your &lt;code&gt;urls.py&lt;/code&gt; with new URL patterns.&lt;/p&gt;
&lt;p&gt;This isn't exactly a huge burden, but it's added friction. It's also the exact kind of thing plugin systems are designed to solve.&lt;/p&gt;
&lt;p&gt;DJP addresses this. You configure DJP just once, and then any additional DJP-enabled plugins you &lt;code&gt;pip install&lt;/code&gt; can automatically register configure themselves within your Django project.&lt;/p&gt;
&lt;h4 id="setting-up-djp"&gt;Setting up DJP&lt;/h4&gt;
&lt;p&gt;There are three steps to adding DJP to an existing Django project:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pip install djp&lt;/code&gt; - or add it to your &lt;code&gt;requirements.txt&lt;/code&gt; or similar.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Modify your &lt;code&gt;settings.py&lt;/code&gt; to add these two lines:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;# Can be at the start of the file:&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;djp&lt;/span&gt;

&lt;span class="pl-c"&gt;# This MUST be the last line:&lt;/span&gt;
&lt;span class="pl-s1"&gt;djp&lt;/span&gt;.&lt;span class="pl-en"&gt;settings&lt;/span&gt;(&lt;span class="pl-en"&gt;globals&lt;/span&gt;())&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Modify your &lt;code&gt;urls.py&lt;/code&gt; to contain the following:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;djp&lt;/span&gt;

&lt;span class="pl-s1"&gt;urlpatterns&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [
    &lt;span class="pl-c"&gt;# Your existing URL patterns&lt;/span&gt;
] &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;djp&lt;/span&gt;.&lt;span class="pl-en"&gt;urlpatterns&lt;/span&gt;()&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That's everything. The &lt;code&gt;djp.settings(globals())&lt;/code&gt; line is a little bit of magic - it gives &lt;code&gt;djp&lt;/code&gt; an opportunity to make any changes it likes to your configured settings.&lt;/p&gt;
&lt;p&gt;You can see &lt;a href="https://github.com/simonw/djp/blob/5dd9ba1ac8b9c6d29fc88936451ad0e2eaa7248c/djp/__init__.py#L74-L84"&gt;what that does here&lt;/a&gt;. Short version: it adds &lt;code&gt;"djp"&lt;/code&gt; and any other apps from plugins to &lt;code&gt;INSTALLED_APPS&lt;/code&gt;, modifies &lt;code&gt;MIDDLEWARE&lt;/code&gt; for any plugins that need to do that and gives plugins a chance to modify any other settings they need to.&lt;/p&gt;
&lt;p&gt;One of my personal rules of plugin system design is that you should never ship a plugin hook (a customization point) without releasing at least one plugin that uses it. This validates the design and provides executable documentation in the form of working code.&lt;/p&gt;
&lt;p&gt;I've released three plugins for DJP so far.&lt;/p&gt;
&lt;h4 id="django-plugin-django-header"&gt;django-plugin-django-header&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-django-header"&gt;django-plugin-django-header&lt;/a&gt;&lt;/strong&gt; is a very simple initial example. It registers a &lt;a href="https://docs.djangoproject.com/en/5.1/topics/http/middleware/"&gt;Django middleware class&lt;/a&gt; that adds a &lt;code&gt;Django-Composition:&lt;/code&gt; HTTP header to every response with the name of a random &lt;a href="https://github.com/simonw/django-plugin-django-header/blob/6e6be545e756f43b35b737c120e3c5d85b27dfd3/django_plugin_django_header/middleware.py#L24-L151"&gt;Composition by Django Reinhardt&lt;/a&gt; (thanks,&lt;a href="https://en.wikipedia.org/wiki/List_of_compositions_by_Django_Reinhardt"&gt;Wikipedia&lt;/a&gt;).&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;pip install django-plugin-django-header&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then try it out with &lt;code&gt;curl&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;curl -I http://localhost:8000/&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You should get back something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
Django-Composition: Nuages
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm running this on my blog right now! Try this command to see it in action:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;curl -I https://simonwillison.net/&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The plugin is very simple. Its &lt;a href="https://github.com/simonw/django-plugin-django-header/blob/main/django_plugin_django_header/__init__.py"&gt;__init__.py&lt;/a&gt; registers middleware like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;djp&lt;/span&gt;

&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;djp&lt;/span&gt;.&lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;middleware&lt;/span&gt;():
    &lt;span class="pl-k"&gt;return&lt;/span&gt; [
        &lt;span class="pl-s"&gt;"django_plugin_django_header.middleware.DjangoHeaderMiddleware"&lt;/span&gt;
    ]&lt;/pre&gt;
&lt;p&gt;That string references the middleware class &lt;a href="https://github.com/simonw/django-plugin-django-header/blob/main/django_plugin_django_header/middleware.py"&gt;in this file&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="django-plugin-blog"&gt;django-plugin-blog&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-blog"&gt;django-plugin-blog&lt;/a&gt;&lt;/strong&gt; is a much bigger example. It implements a full blog system for your Django application, with bundled models and templates and views and a URL configuration.&lt;/p&gt;
&lt;p&gt;You'll need to have configured auth and the Django admin already (those already there by default in the &lt;code&gt;django-admin startproject&lt;/code&gt; template). Now install the plugin:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;pip install django-plugin-blog&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And run migrations to create the new database tables:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;python manage.py migrate&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's all you need to do. Navigating to &lt;code&gt;/blog/&lt;/code&gt; will present the index page of the blog, including a link to a working Atom feed.&lt;/p&gt;
&lt;p&gt;You can add entries and tags through the Django admin (configured for you by the plugin) and those will show up on &lt;code&gt;/blog/&lt;/code&gt;, get their own URLs at &lt;code&gt;/blog/2024/&amp;lt;slug&amp;gt;/&lt;/code&gt; and be included in the Atom feed, the &lt;code&gt;/blog/archive/&lt;/code&gt; list and the &lt;code&gt;/blog/2024/&lt;/code&gt; year-based index too.&lt;/p&gt;
&lt;p&gt;The default design is very basic, but you can customize that by providing your own base template or providing custom templates for each of the pages. There are details on the templates &lt;a href="https://github.com/simonw/django-plugin-blog"&gt;in the README&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The blog implementation is directly adapted from my &lt;a href="https://til.simonwillison.net/django/building-a-blog-in-django"&gt;Building a blog in Django&lt;/a&gt; TIL.&lt;/p&gt;
&lt;p&gt;The primary goal of this plugin is to demonstrate what a plugin with views, templates, models and a URL configuration looks like. Here's the full &lt;a href="https://github.com/simonw/django-plugin-blog/blob/main/django_plugin_blog/__init__.py"&gt;__init__.py for the plugin&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;urls&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;path&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;conf&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;settings&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;djp&lt;/span&gt;

&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;djp&lt;/span&gt;.&lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;installed_apps&lt;/span&gt;():
    &lt;span class="pl-k"&gt;return&lt;/span&gt; [&lt;span class="pl-s"&gt;"django_plugin_blog"&lt;/span&gt;]

&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;djp&lt;/span&gt;.&lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;urlpatterns&lt;/span&gt;():
    &lt;span class="pl-k"&gt;from&lt;/span&gt; .&lt;span class="pl-s1"&gt;views&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;index&lt;/span&gt;, &lt;span class="pl-s1"&gt;entry&lt;/span&gt;, &lt;span class="pl-s1"&gt;year&lt;/span&gt;, &lt;span class="pl-s1"&gt;archive&lt;/span&gt;, &lt;span class="pl-s1"&gt;tag&lt;/span&gt;, &lt;span class="pl-v"&gt;BlogFeed&lt;/span&gt;

    &lt;span class="pl-s1"&gt;blog&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;getattr&lt;/span&gt;(&lt;span class="pl-s1"&gt;settings&lt;/span&gt;, &lt;span class="pl-s"&gt;"DJANGO_PLUGIN_BLOG_URL_PREFIX"&lt;/span&gt;, &lt;span class="pl-c1"&gt;None&lt;/span&gt;) &lt;span class="pl-c1"&gt;or&lt;/span&gt; &lt;span class="pl-s"&gt;"blog"&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt; [
        &lt;span class="pl-en"&gt;path&lt;/span&gt;(&lt;span class="pl-s"&gt;f"&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;blog&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;/"&lt;/span&gt;, &lt;span class="pl-s1"&gt;index&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;"django_plugin_blog_index"&lt;/span&gt;),
        &lt;span class="pl-en"&gt;path&lt;/span&gt;(&lt;span class="pl-s"&gt;f"&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;blog&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;/&amp;lt;int:year&amp;gt;/&amp;lt;slug:slug&amp;gt;/"&lt;/span&gt;, &lt;span class="pl-s1"&gt;entry&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;"django_plugin_blog_entry"&lt;/span&gt;),
        &lt;span class="pl-en"&gt;path&lt;/span&gt;(&lt;span class="pl-s"&gt;f"&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;blog&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;/archive/"&lt;/span&gt;, &lt;span class="pl-s1"&gt;archive&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;"django_plugin_blog_archive"&lt;/span&gt;),
        &lt;span class="pl-en"&gt;path&lt;/span&gt;(&lt;span class="pl-s"&gt;f"&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;blog&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;/&amp;lt;int:year&amp;gt;/"&lt;/span&gt;, &lt;span class="pl-s1"&gt;year&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;"django_plugin_blog_year"&lt;/span&gt;),
        &lt;span class="pl-en"&gt;path&lt;/span&gt;(&lt;span class="pl-s"&gt;f"&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;blog&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;/tag/&amp;lt;slug:slug&amp;gt;/"&lt;/span&gt;, &lt;span class="pl-s1"&gt;tag&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;"django_plugin_blog_tag"&lt;/span&gt;),
        &lt;span class="pl-en"&gt;path&lt;/span&gt;(&lt;span class="pl-s"&gt;f"&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;blog&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;/feed/"&lt;/span&gt;, &lt;span class="pl-v"&gt;BlogFeed&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;"django_plugin_blog_feed"&lt;/span&gt;),
    ]&lt;/pre&gt;
&lt;p&gt;It still only needs to implement two hooks: one to add &lt;code&gt;django_plugin_blog&lt;/code&gt; to the &lt;code&gt;INSTALLED_APPS&lt;/code&gt; list and another to add the necessary URL patterns to the project.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;from .views import ...&lt;/code&gt; line is nested inside the &lt;code&gt;urlpatterns()&lt;/code&gt; hook because I was hitting circular import issues with those imports at the top of the module.&lt;/p&gt;
&lt;h4 id="django-plugin-database-url"&gt;django-plugin-database-url&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-database-url"&gt;django-plugin-database-url&lt;/a&gt;&lt;/strong&gt; is the smallest of my example plugins. It exists mainly to exercise the &lt;code&gt;settings()&lt;/code&gt; plugin hook, which allows plugins to further manipulate settings in any way they like.&lt;/p&gt;
&lt;p&gt;Quoting &lt;a href="https://github.com/simonw/django-plugin-database-url/blob/main/README.md"&gt;the README&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Once installed, any &lt;code&gt;DATABASE_URL&lt;/code&gt; environment variable will be automatically used to configure your Django database setting, using &lt;a href="https://github.com/jazzband/dj-database-url"&gt;dj-database-url&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's the &lt;a href="https://github.com/simonw/django-plugin-database-url/blob/main/django_plugin_database_url/__init__.py"&gt;full implementation&lt;/a&gt; of that plugin, most of which is copied straight from the &lt;a href="https://github.com/jazzband/dj-database-url/blob/master/README.rst#usage"&gt;dj-database-url documentation&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;djp&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;dj_database_url&lt;/span&gt;

&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;djp&lt;/span&gt;.&lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;settings&lt;/span&gt;(&lt;span class="pl-s1"&gt;current_settings&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;current_settings&lt;/span&gt;[&lt;span class="pl-s"&gt;"DATABASES"&lt;/span&gt;][&lt;span class="pl-s"&gt;"default"&lt;/span&gt;] &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;dj_database_url&lt;/span&gt;.&lt;span class="pl-en"&gt;config&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;conn_max_age&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;600&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;conn_health_checks&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;,
    )&lt;/pre&gt;
&lt;p&gt;If DJP gains traction, I expect that a lot of plugins will look like this - thin wrappers around existing libraries where the only added value is that they configure those libraries automatically once the plugin is installed.&lt;/p&gt;
&lt;h4 id="writing-a-plugin"&gt;Writing a plugin&lt;/h4&gt;
&lt;p&gt;A plugin is a Python package bundling a module that implements one or more of the &lt;a href="https://djp.readthedocs.io/en/latest/plugin_hooks.html"&gt;DJP plugin hooks&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As I've shown above, the Python code for plugins can be very short. The larger challenge is correctly packaging and distributing the plugin - plugins are discovered using &lt;a href="https://setuptools.pypa.io/en/latest/userguide/entry_point.html"&gt;Entry Points&lt;/a&gt; which are defined in a &lt;code&gt;pyproject.toml&lt;/code&gt; file, and you need to get those exactly right for your plugin to be discovered.&lt;/p&gt;
&lt;p&gt;DJP includes &lt;a href="https://djp.readthedocs.io/en/latest/creating_a_plugin.html"&gt;documentation on creating a plugin&lt;/a&gt;, but to make it as frictionless as possible I've released a new &lt;a href="https://github.com/simonw/django-plugin"&gt;django-plugin cookiecutter template&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This means you can start a new plugin like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;pip install cookiecutter
cookiecutter gh:simonw/django-plugin&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then answer the questions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  [1/6] plugin_name (): django-plugin-example
  [2/6] description (): A simple example plugin
  [3/6] hyphenated (django-plugin-example):
  [4/6] underscored (django_plugin_example):
  [5/6] github_username (): simonw
  [6/6] author_name (): Simon Willison
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you'l get a &lt;code&gt;django-plugin-example&lt;/code&gt; directory with a fully configured plugin ready to be published to PyPI.&lt;/p&gt;
&lt;p&gt;The template includes a &lt;code&gt;.github/workflows&lt;/code&gt; directory with actions that can run tests, and an action that publishes your plugin to PyPI any time you create a new release on GitHub.&lt;/p&gt;
&lt;p&gt;I've used that pattern myself for hundreds of plugin projects for &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; and &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt;, so I'm confident this is an effective way to release plugins.&lt;/p&gt;
&lt;p&gt;The workflows use PyPI's &lt;a href="https://docs.pypi.org/trusted-publishers/"&gt;Trusted Publishers&lt;/a&gt; mechanism (see &lt;a href="https://til.simonwillison.net/pypi/pypi-releases-from-github"&gt;my TIL&lt;/a&gt;), which means you don't need to worry about API keys or PyPI credentials - configure the GitHub repo once using the PyPI UI and everything should just work.&lt;/p&gt;
&lt;h4 id="writing-tests-for-plugins"&gt;Writing tests for plugins&lt;/h4&gt;
&lt;p&gt;Writing tests for plugins can be a little tricky, especially if they need to spin up a full Django environemnt in order to run the tests.&lt;/p&gt;
&lt;p&gt;I previously published &lt;a href="https://til.simonwillison.net/django/pytest-django"&gt;a TIL about that&lt;/a&gt;, showing how to have tests with their own &lt;code&gt;tests/test_project&lt;/code&gt; project that can be used by &lt;a href="https://pytest-django.readthedocs.io"&gt;pytest-django&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've baked that pattern into the &lt;code&gt;simon/django-plugin&lt;/code&gt; cookiecutter template as well, plus a single default test which checks that a hit to the &lt;code&gt;/&lt;/code&gt; index page returns a 200 status code - still a valuable default test since it confirms the plugin hasn't broken everything!&lt;/p&gt;
&lt;p&gt;The tests &lt;a href="https://github.com/simonw/django-plugin-django-header/blob/main/tests/test_django_plugin_django_header.py"&gt;for django-plugin-django-header&lt;/a&gt; and &lt;a href="https://github.com/simonw/django-plugin-blog/blob/main/tests/test_django_plugin_blog.py"&gt;for django-plugin-blog&lt;/a&gt; should provide a useful starting point for writing tests for your own plugins.&lt;/p&gt;
&lt;h4 id="why-call-it-djp-"&gt;Why call it DJP?&lt;/h4&gt;
&lt;p&gt;Because &lt;a href="https://pypi.org/project/django-plugins/"&gt;django-plugins&lt;/a&gt; already existed on PyPI, and I like &lt;a href="https://pypi.org/project/llm/"&gt;my three letter acronyms&lt;/a&gt; there!&lt;/p&gt;
&lt;h4 id="what-s-next-for-djp-"&gt;What's next for DJP?&lt;/h4&gt;
&lt;p&gt;I presented this at DjangoCon US 2024 yesterday afternoon. Initial response seemed positive, and I'm going to be attending the conference sprints on Thursday morning to see if anyone wants to write their own plugin or help extend the system further.&lt;/p&gt;
&lt;p&gt;Is this a good idea? I think so. Plugins have been transformative for both Datasette and LLM, and I think &lt;a href="https://pluggy.readthedocs.io/"&gt;Pluggy&lt;/a&gt; provides a mature, well-designed foundation for this kind of system.&lt;/p&gt;
&lt;p&gt;I'm optimistic about plugins as a natural extension of Django's existing ecosystem. Let's see where this goes.&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/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djp"&gt;djp&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="plugins"/><category term="projects"/><category term="djp"/></entry></feed>