More from Applied Cartography
Here is a confession: I am a very strong proponent of a robust test suite being perhaps the single most important asset of a codebase, but when it comes to auxiliary services like admin sites or CLIs when it comes to testing I tend to ask for forgiveness more than I ask for permission. Django's admin site is no different: and, because Django's admin DSL is very magic-string-y, there's a lot of stuff that never gets caught by CI or mypy until a lovely CS agent informs me that something is blowing up in their face. Take this example, which bites me more often than I care to admit: from django.contrib import admin from stripe.models import StripeCustomer class StripeCustomer(models.Model): id = models.CharField(max_length=100, unique=True) username = models.CharField(max_length=100, unique=True) email_address = models.EmailField() creation_date = models.DateTimeField(auto_now=True) @admin.register(StripeCustomer) class StripeCustomerAdmin(admin.ModelAdmin): list_display = ( "id", "username", "email", "creation_date", ) search_fields = ( "username", "email", ) One thing that has made my life slightly easier in this respect is a parametric test that just makes sure we can render the empty state for every single admin view. Code snippet first, explanation after: from django.urls import get_resolver, reverse def extract_routes(resolver: URLResolver) -> iter[str]: keys = [key for key in resolver.reverse_dict.keys() if isinstance(key, str)] key_to_route = {key: resolver.reverse_dict.getlist(key)[0][0][0] for key in keys} for key in keys: yield key for key, (prefix, subresolver) in resolver.namespace_dict.items(): for route in extract_routes(subresolver): yield f"{key}:{route.name}" def is_django_admin_route(route_name: str): # Matches, e.g., `admin:emails_event_changelist`. return route_name.split(":").endswith("changelist") ADMIN_URL_ROUTE = "buttondown.urls.admin" DJANGO_ADMIN_CHANGELIST_ROUTES = [ route.name for route in extract_routes(get_resolver(ADMIN_URL_ROUTE)) if is_django_admin_route(route.name) ] # The fixture is overkill for this example, but I'm copying this from the actual codebase. @pytest.fixture def superuser_client(superuser: User, client: Client) -> Client: client.force_login(superuser) return client @pytest.mark.parametrize( "url", DJANGO_ADMIN_CHANGELIST_ROUTES ) def test_can_render_route(superuser_client: Any, url: str) -> None: url = reverse(url, args=[]) response = superuser_client.get(url) assert response.status_code == 200 Okay, a bit of a mouthful, but the final test itself is very clean and tidy and catches a lot of stuff. That extract_routes implementation looks scary and magical, and it is — I use a more robust implementation in django-typescript-routes, which itself we gratefully purloined from django-js-reverse. Lots of scary indexing, but its held up well for a while. The fixture and parametrize assumes usage of pytest (you should use pytest!) but it's trivially rewritable to use subTest instead.
When we added support for complex filtering in Buttondown, I spent a long time trying to come up with a schema for filters that felt sufficiently ergonomic and future-proof. I had a few constraints, all of which were reasonable: It needed to be JSON-serializable, and trivially parsable by both the front-end and back-end. It needed to be arbitrarily extendible across a number of domains (you could filter subscribers, but also you might want to filter emails or other models.) It needed to be able to handle both and and or logic (folks tagged foo and bar as well as folded tagged foo or bar). It needed to handle nested logic (folks tagged foo and folks tagged bar or baz.) The solution I landed upon is not, I’m sure, a novel one, but googling “recursive filter schema” was unsuccessful and I am really happy with the result so here it is in case you need something like this: @dataclass class FilterGroup: filters: list[Filter] groups: list[FilterGroup] predicate: "and" | "or" @dataclass class Filter: field: str operator: "less_than" | "greater_than" | "equals" | "not_equals" | "contains" | "not_contains" value: str And there you have it. Simple, easily serializable/type-safe, can handle everything you throw at it. For example, a filter for all folks younger than 18 or older than 60 and retired: FilterGroup( predicate="or", filters=[ Field( field="age", operator="less_than", value="18" ) ], groups=[ FilterGroup( predicate="and", filters=[ Field( field="age", operator="greater_than", value="60" ), Field( field="status", operator="equals", value="retired" ) ] groups=[], ) ] )
If there's been one through line in changes to Buttondown's architecture over the past six months or so, it's been the removal and consolidation of dependencies: on the front-end, back-end, and in paid services. I built our own very spartan version of Metabase, Notion, and Storybook; we vended a half-dozen or so Django packages that were not worth the overhead of pulling from PyPI (and rewrote another half-dozen or so, which we will open-source in due time); we ripped out c3, our visualization library, and built our own; we ripped out vuedraggable and a headlessui and a slew more of otherwise-underwhelming frontend packages in favor of purpose-built (faster, smaller, less-flexible) versions. [1] There are a few reasons for this: Both Buttondown as an application and I as a developer have now been around long enough to be scarred by big ecosystem changes. Python has gone through both the 2.x to 3.x transition and, more recently, the untyped to typed transition; Vue has gone from 2.x to 3.x. The academic problem of "what happens if this language completely changes?" is no longer academic, and packages that we installed back in 2018 slowly succumbed to bitrot. It's more obvious to me now than a few years ago that pulling in dependencies incurs a non-trivial learning cost for folks paratrooping into the codebase. A wrapper library around fetch might be marginally easier to invoke once you get used to it, but it's a meaningful bump in the learning curve to adapt to it for the first time. It is easier than ever to build 60% of a tool, which is problematic in many respects but useful if you know exactly which 60% you care about. (Internal tools like Storybook or Metabase are great examples of this. It was a fun and trivial exercise to get Claude to build a tool that did everything I wanted Metabase to do, and save me $120/mo in the process.) We still use a lot of very heavy, very complex stuff that we're very happy with. Our editor sits on top of tiptap (and therefore ProseMirror); we use marked and turndown liberally, because they're fast and robust. On the Python side, our number of non-infrastructural packages is smaller but still meaningful (beautifulsoup, for instance, and django-allauth / django-anymail which are both worth their weight in gold). But the bar for pulling in a small dependency is much higher than it was, say, twelve months ago. My current white whale is to finally get rid of axios. 39 call sites to go! ↩︎
After many wonderful years of working out of my home office (see Workspaces), I've now "expanded" [1] into an office of my own. 406 W Franklin St #201 is now the Richmond-area headquarters of Buttondown. Send me gifts! The move is a bittersweet one; it was a great joy to be so close to Haley and Lucy (and, of course, Telly), and the flexibility of being able to hop off a call and then take the dog for a walk or hold Lucy for a while was very, very nice. At the same time, for the first time in my life that flexibility has become a little bit of a burden! It turns out it is very hard to concentrate on responding to emails when your alternative is to play with your daughter giggling in the adjoining room; similarly, as Buttondown grows and as more and more of my time is spent on calls, it turns out long-winded demos and onboarding calls are logistically trickier when it is Nap Time a scant six feet away. And, beyond that, it's felt harder and harder to turn my brain off for the day: when there is always more work to be done, it's hard not to poke away at a stubborn pull request or jot down some strategy notes instead of being more present for my family (or even for myself, in a non-work capacity.) So, I leased an office. The space is pretty cool: it's downtown in the sweet spot of a little more than a mile away from the house: trivially walkable (or bikeable, as the above photo suggests) but far enough away to give me a good bit of mental space. The building is an old manor (turned dormitory, turned office building). I've got a bay window with plenty of light but no views; I've got a nice ethernet connection and a Mac Mini with very few things installed; I've got a big Ikea desk and a printer; I've got an alarm on my phone for 4:50pm, informing me that it's time to go home, where my world becomes once again lively and lovely, full of noise and joy and laughter. Air quote because I'm fairly confident this office is actually smaller than the home office. ↩︎
More in technology
Being chronically late to meetings sucks. Not only is it very rude, but you’re signalling that you don’t value your coworkers’ time. However, I’ve picked up a technique that works unreasonably well within a team.1 If you are late to the first meeting of the day three times within a quarter, then you will have to make pancakes for the whole team. Let’s say that you have a daily stand-up taking place at 10:00. Arriving at 10:00:59: completely OK. Arriving at 10:01:00: You’re one step closer to making pancakes! Keep in mind that you may hit some obstacles when implementing this rule, so feel free to adjust it. When proposing this idea in my current team, I learned that the office does not offer pancake-making facilities. The pancakes can be substituted for other types of cake or bringing in something else, as long as the team gives prior approval of that modification. The pancake strikes can also be pooled together and spent with your teammates if they wish to do so. If you’re struggling with your team being late to your daily meeting(s), then go ahead and add this rule to the working agreement. You do have a working agreement set up, right? Right? And a free security tech tip to close out: if you see an unlocked work laptop at the office, open your internal chat application of choice on it and try posting to a public channel that you’ll be bringing cake/beers/candy to the office. Works wonders for enforcing the habit of locking your laptop up when leaving the desk! to be fair, the sample size is two, but it has worked out really well in both! ↩︎
In the worlds of programming and robotics, turtles are entities — either virtual or physical robots— that follow commands to move around a 2D plane. Those are usually very simple commands, such as “move forward 10 units” or “rotate 90 degrees clockwise,” and they help people learn some programming fundamentals (like Logo in the ’80s!) […] The post Turtle bots, Gestalt principles, and emergent art appeared first on Arduino Blog.
It was probably going to happen sooner or later, but Microsoft has officially released the source code for 6502 BASIC. The specific revision is very Commodore-centric: it's the 1977 "8K" BASIC variant "1.1," which Commodore users know better as BASIC V2.0, the same BASIC used in the early PET and with later spot changes from Commodore (including removing Bill Gates' famous Easter egg) in the VIC-20 and Commodore 64. I put "8K" in quotes because the 40-bit Microsoft Binary Format version, which is most familiar as the native floating point format for most 8-bit BASICs derived from Microsoft's and all Commodore BASICs from the PET on up, actually starts at 9K in size. In the C64, because there is RAM and I/O between the BASIC ROM and the Kernal ROM, there is an extra JMP at the end of the BASIC ROM to continue to the routine in the lowest portions of the Kernal ROM. The jump doesn't exist in the VIC-20 where the ROM is contiguous and as a result everything past that point is shifted by three bytes on the C64, the length of the instruction. This is, of course, the same BASIC that Gates wanted a percentage of but Jack Tramiel famously refused to budge on the $25,000 one-time fee, claiming "I'm already married." Gates yielded to Tramiel, as most people did then, but I suspect the slight was never forgotten. Not until the 128 did Microsoft officially appear in the credits for Commodore BASIC, and then likely only as a way to push its bona fides as a low-end business computer. Microsoft's source release also includes changes from Commodore's own John Feagans, who rewrote the garbage collection routine, and was the original developer of the Commodore Kernal and later Magic Desk. The source code is all in one big file (typical for the time) and supports six machine models, the first most likely a vapourware 6502 system never finished by Canadian company Semi-Tech Microelectronics (STM) better known for the CP/M-based Pied Piper, then the Apple II, the Commodore (in this case PET 2001), the Ohio Scientific (OSI) Challenger, the Commodore/MOS KIM-1, and most intriguingly a PDP-10-based simulator written by Paul Allen. The source code, in fact, was cross-assembled on a PDP-10 using MACRO-10, and when assembled for the PDP-10 emulator it actually emits a PDP-10 executable that traps on every instruction into the simulator linked with it — an interesting way of effectively accomplishing threaded code. A similar setup was used for their 8080 emulator. Unfortunately, I don't believe Allen's code has been released anywhere, though I'd love to be proven wrong if people know otherwise. Note that they presently don't even mention the STM port in the Github README, possibly because no one was sure what it did. While MACRO-10 source for 6502 BASIC has circulated before and been analysed in detail, most notably by Michael Steil, this is nevertheless the first official release where it is truly open-source under the MIT license and Microsoft should be commended for doing so. This also makes it much easier to pull a BASIC up for your own 6502 homebrew system — there's nothing like the original.