DWiki is the pile of code that underlies Wandering Thoughts . It started out many years ago as a Python 2 program (partly because there was no Python 3 at the time), and it stayed that way for a long time , making it the most significant and by far the most substantial Python 2 program I still cared deeply about. Years ago I said I'd port it to Python 3 someday and somewhat to my surprise, that day has now come (well, it came yesterday).
The direct trigger was discovering that Python 3.13 had dropped 2to3 , which made me feel that I should run 2to3 over DWiki 's current Python 2 code base while I still could ( I had an old conversion from many years ago , but that converted code base was very out of date). One thing led to another, as it often does with me, and I wound up doing a full port and then putting it into production, which is to say serving this blog . I suspect that part of me just felt it was time.
( The 2to3 removal is in the Python 3.13 release notes , and it comes after 2to3 and its infrastructure were deprecated in 3.11 for reasonable reasons.)
As I expected years ago , the stuff that 2to3 could handle was the easy part. Much of the actual work of the port was sorting out the boundary between Unicode strings and byte strings in a Python 3 world. Some of this would have been easier if I'd found PEP 3333 earlier and followed it in my own discount WSGI implementation, but a bunch of it I had to find the hard way, by trying things and having them blow up, sometimes in production.
(I wound up in the same place as PEP 3333 just from the inherent requirements of the web. For example, the HTTP Content-Length is in octets, so if you're using it to read a POST body, the object you're reading from has to be providing bytes. And it turns out that you can't write HTTP headers to a text mode file object because that will turn \r\n sequences into \n, which will make things unhappy with you.)
Not all of the changes were at the IO boundaries of DWiki (and the IO boundaries themselves weren't always simple or obvious). Python 3's handling of cryptographic hashes requires bytes, which rippled through to several places where I use them in DWiki (and the hmac API changed a bit, which wasn't fixed up by 2to3). Python 3 also really wants your regular expressions to be in
r"..."
strings, because otherwise it will complain about you using regular expression backslash escapes like '\s' that aren't string backslash escapes.
I don't have a DWiki test suite, but long ago I built scripts that would crawl and collect all real pages from an old and a new version of DWiki . I originally used these to check for changes in how pages got rendered when I changed the wikitext processing code (often I wanted no changes), but this time around I was able to use them to verify that the Python 3 DWiki could at least render all existing pages into essentially the same thing (there were \r\n sequences that turned into \n instead of being passed through, but that's probably a good change). But that still left things like writing comments, and also the two sets of code involved in how DWiki runs in production instead of in testing.
I probably wouldn't have tried to do this if I hadn't had a relatively substantial block of free time. It took me more or less all day yesterday to get up to the current production state, with a lot of back and forth, experimentation, and tweaking. There was a lot of code and problem context that I might not have retained if I'd had to slice my work up into half hour or hour long chunks of work, and once I started running the Python 3 version as the live server I was relatively committed to fixing any problems that came up on the spot.
(I could have rolled back to the Python 2 version but it would have been at least a bit awkward for various reasons, including a pickle format change.)
The current Python 3 DWiki code still needs additional cleanups, partly to undo unnecessary 2to3 changes like changing '
for ... in
dct.keys():
' to '
for ... in list(dct.keys()):
'. But it's running stably now for, well, not quite 24 hours yet but for at least a bunch of all of the typical traffic that Wandering Thoughts gets. Probably there aren't any remaining Unicode conversion issues, although re-reading one of my old entries makes me feel I should audit every use of EnvironmentError when dealing with files.
(2to3 appears to always put list() around things that changed to return generators in Python 3. Sometimes this is important, but it's not necessary if the result is only being used in a '
for
'.)
I also want to think about what Unicode error handling to use in various circumstances , although these days I'm inclined to be draconian. For example, if someone tries to write a comment with invalid UTF-8, I probably don't want to backslash escape the invalid bits, so the default 'replace' handling is fine (in my case, this comes from using urllib to decode POST bodies ). And currently all of the existing content in Wandering Thoughts is UTF-8 clean, at least as far as I can tell.
(The whole Unicode and bytes issue is something where types would be handy ( or an option to turn off all of Python 3's implicit conversions ), but adding typing to DWiki 's 'originated in Python 2' codebase is both a lot of work and also extremely messy, because it uses things in ways that mypy is already unhappy about.)
PS: The Github version of DWiki is now significantly out of date and I'm probably not going to update it for reasons that don't fit in the margins of this entry.
Sidebar: The Python 3 WSGI rules in a nutshell
To summarize PEP 3333 in my own way, HTTP headers are Unicode strings, ie
str
, but must be limited to iso-8859-1 characters (at least when you write them). The wsgi.input file object produces bytes and your HTTP response body is also bytes. In a CGI environment, you read from
sys.stdin.buffer
and your WSGI CGI implementation writes to
sys.stdout.buffer
(including the headers, after encoding to iso-8859-1).
If your WSGI implementation is talking to a network socket, you can and must leave the network socket as a binary file object. In my case, this generally means wsgi.input is created with '
os.fdopen(fd,
"rb")
'.