[Python-ideas] PEP 3156 feedback
Antoine Pitrou
solipsis at pitrou.net
Tue Dec 18 11:01:36 CET 2012
Hello,
Here is my own feedback on the in-progress PEP 3156. Please discard it
if it's too early to give feedback :-))
Event loop API
--------------
I would like to say that I prefer Tornado's model: for each primitive
provided by Tornado, you can pass an explicit Loop instance which you
instantiated manually.
There is no module function or policy object hiding this mechanism:
it's simple, explicit and flexible (in other words: if you want a
per-thread event loop, just do it yourself using TLS :-)).
There are some requirements I've found useful:
- being able to instantiate multiple loops, either at the same time or
serially (this is especially nice for unit tests; Twisted has to use
a dedicated test runner just because their reactor doesn't support
multiple instances or restarts)
- being able to stop a loop explicitly: having to unregister all
handlers or delayed calls is a PITA in non-trivial situations (for
example you might have multiple protocol instances, each with a bunch
of timers, some perhaps even in third-party libraries; keeping track
of all this is the event loop's job)
* The optional sock_*() methods: how about having different ABCs, e.g.
the EventLoop ABC for basic behaviour, and the NetworkedEventLoop ABC
adding the socket helpers?
Protocols and transports
------------------------
We probably want to provide a Protocol base class and encourage people
to inherit it. It can provide useful functionality (perhaps write()
and writelines() shims? it can make mocking easier).
My own opinion about Twisted's API is that the Factory class is often
useless, and adds a cognitive burden. If you need a place to track all
protocols of a given kind (e.g. all connections), you can do it
yourself. Also, the Factory implies that you don't control how exactly
your protocol gets instantiated (unless you override some method on the
Factory I'm missing the name of: it is cumbersome).
So, when creating a client, I would pass it a protocol instance.
When creating a server, I would pass it a protocol class. Here the base
Protocol class comes into play, its __init__() could take the transport
as argument and set the "transport" attribute with it. Further args
could be optionally passed to the constructor:
class MyProtocol(Protocol):
def __init__(self, transport, my_personal_attribute):
Protocol.__init__(self, transport)
self.my_personal_attribute = my_personal_attribute
...
def listen(ioloop):
# Each new connection will instantiate a MyProtocol with "foobar"
# for my_personal_attribute.
ioloop.listen_tcp(("0.0.0.0", 8080), MyProtocol, "foobar")
(The hypothetical listen_tcp() is just a name: perhaps it's actually
start_serving(). It should accept any callable, not just a class:
therefore, you can define complex behaviour if you like)
I think the transport / protocol registration must be done early, not in
connection_made(). Sometimes you will want to do things on a protocol
before you know a connection is established, for example queue things
to write on the transport. An use case is a reconnecting TCP client:
the protocol will continue existing at times when the connection is
down.
Unconnected protocols need their own base class and API:
data_received()'s signature should be (data, remote_addr) or
(remote_addr, data). Same for write().
* writelines() sounds ambiguous for datagram protocols: does it send
those "lines" as a single datagram, or one separate datagram per
"line"? The equivalent code suggests the latter, but which one makes
more sense?
* connection_lost(): you definitely want to know whether it's you or the
other end who closed the connection. Typically, if the other end
closed the connection, you will have to run some cleanup steps, and
perhaps even log an error somewhere (if the connection was closed
unexpectedly).
Actually, I'm not sure it's useful to call connection_lost() when you
closed the connection yourself: are there any use cases?
Regards
Antoine.
More information about the Python-ideas
mailing list