Skip to content

TypedDict with keys that aren't identifiers, inheritance and WSGI #7654

Closed
@shabbyrobe

Description

@shabbyrobe

Hello! I'm not sure if this is a bug report (probably not), support request (definitely), a request for a missing feature (probably, though what, exactly, I'm not sure), or just a bit of an "experience report", but I'm struggling a bit with TypedDict while trying to lock down our WSGI environ vars.

It seems that this use case isn't covered particularly well by the current implementation, but I'm not sure if that's just because I'm just not "holding it right". Everything was going swimmingly until I bumped into the WSGI environ keys, then things started to unravel. It doesn't seem to be possible to declare wsgi.input as a key using the class-based syntax:

class Environ(TypedDict):
    wsgi.input: str

That makes sense, wsgi.input isn't syntactically legal... So my first instinct was to quote it, just in case, but nah, "illegal target for annotation":

class Environ(TypedDict):
    "wsgi.input": str

Oh well, it was worth a try, but I knew up front it was a long shot! So then I tried out the alternative syntax, which seems to do the trick nicely:

Environ = TypedDict("Environ", {"wsgi.input": Reader})

Except now I have to find a way to represent my application-specific headers, and I'd like to keep them separate from the core definitions because I have a few applications that need to make use of them. The PEP says that there's inheritance, which would solve the problem perfectly. My first thought was that TypedDict would mirror the syntax for type() and allow me to specify bases, but another quick glance at the PEP says it's only available if I'm using the class syntax. I thought maybe I could just re-use the dict, but I ran into #4128:

_environ_keys = {"wsgi.input": Reader}
Environ = TypedDict("Environ", _environ_keys)

# BORK! TypedDict() expects a dictionary literal as the second argument
MyEnviron = TypedDict("MyEnviron", {**_environ_keys, "HTTP_X_FLEEB": Optional[str]})

That was the end of that thread for now, I went back to just rolling it all into one big TypedDict declared using the alternative syntax; I'll worry about re-use later.

The last thing I ran into was the problem with accepting "extra keys", outlined in #4617, which prevents me from being able to have a fallback "catch-all" for the HTTP_ variables documented here

Variables corresponding to the client-supplied HTTP request headers (i.e., variables whose names begin with HTTP_). The presence or absence of these variables should correspond with the presence or absence of the appropriate HTTP header in the request.

There's a lot of these, of course! It's HTTP headers we're talking about, after all! I'm not really sure if "extra keys" would solve that problem, but I thought I should add it just to make the story complete.

I'm sort of a bit stuck - I've got a lot of WSGI stuff here that I can't really do much with type-wise unless I go through and unwrap environ into a dataclass, but some of this stuff is performance sensitive (we use Django for things that aren't and plain ol' WSGI for things that are, which works great!) and a whole dataclass conversion step is not going to keep us inside our performance budgets.

I'll keep pushing it around the page for a little while longer and see what other approaches I can find but it'd be great if there was a way to support WSGI a bit more thoroughly. environ can be a bit amorphous and TypedDict could be a huge help at making things a bit more resilient and safe to work with.

Thank you!

EDIT: I did keep pushing things around the page. Here's what I currently have (still needs a lot more work, of course): https://github.com/shabbyrobe/wsgitypes. This lets me type the bulk of our WSGI applications; I'll keep adding to it as I find more bits.

I found a way around the inheritance issue: I had been presuming (incorrectly, as it turns out) that trying to inherit a TypedDict declared using the alternative syntax wouldn't work, but it works just fine! For example:

Foo = TypedDict("Foo", {"one": str})
Bar = TypedDict("Bar", {"two": str})
class Baz(Foo, Bar):
    pass
yep = {"one": "yep", "two": "yep"}

This seemed to allow the field-combining behavior I was looking for, so I'll run with it for now.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions