Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions python-stdlib/selectors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# selectors

This library implements a subset of CPython's
[`selectors`](https://docs.python.org/3/library/selectors.html) module as a
thin layer over MicroPython's built-in
[`select.poll()`](https://docs.micropython.org/en/latest/library/select.html).
It lets code written for CPython's high-level I/O multiplexing API run
unchanged on MicroPython, on both the unix port and bare-metal ports.

## Example

```python
import selectors
import socket

sel = selectors.DefaultSelector()

server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(socket.getaddrinfo("0.0.0.0", 8080)[0][-1])
server.listen(5)
sel.register(server, selectors.EVENT_READ, "accept")

while True:
for key, events in sel.select(timeout=1):
if key.data == "accept":
conn, addr = server.accept()
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, "client")
else:
data = key.fileobj.recv(512)
if data:
key.fileobj.send(data)
else:
sel.unregister(key.fileobj)
key.fileobj.close()
```

## Supported API

- `EVENT_READ`, `EVENT_WRITE`
- `SelectorKey(fileobj, fd, events, data)` named tuple
- `BaseSelector` with `register()`, `unregister()`, `modify()`, `select()`,
`close()`, `get_key()`, `get_map()` and context manager support
- `PollSelector`, and `DefaultSelector` as an alias for it

Error handling follows CPython: invalid event masks or file objects raise
`ValueError`, registering an object twice or using an unregistered object
raises `KeyError`, and `get_key()` on a closed selector raises `RuntimeError`.

`select(timeout)` accepts `None` (block), a value `<= 0` (poll without
waiting) or a timeout in seconds (float allowed), and returns a list of
`(key, events)` tuples. Error and hang-up conditions are reported as both
read and write readiness, masked by the events the object was registered for,
the same way as CPython's `PollSelector` does.

## Differences from CPython

- File objects are tracked by identity, not by file descriptor, because
sockets on bare-metal ports have no `fileno()`. Any object supported by
`select.poll()` can be registered, e.g. sockets, SSL sockets, UARTs or
`sys.stdin`. On the unix port an integer file descriptor can be registered
as well.
- `SelectorKey.fd` is the result of `fileno()` when the object provides it,
otherwise `-1`.
- `get_map()` returns a plain `dict` keyed by the registered file object, so
it cannot be indexed by file descriptor. Treat it as read-only.
- Two different objects sharing the same file descriptor are not detected as
a duplicate registration.
- Only `PollSelector` is provided; `SelectSelector`, `EpollSelector`,
`DevpollSelector` and `KqueueSelector` are not available.

## Installation

Use `mip` via `mpremote`:

```bash
> mpremote mip install selectors
```

See [Package
management](https://docs.micropython.org/en/latest/reference/packages.html) for
more details on using `mip` and `mpremote`.
3 changes: 3 additions & 0 deletions python-stdlib/selectors/manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
metadata(version="0.1.0", description="CPython compatible selectors module based on select.poll.")

module("selectors.py")
190 changes: 190 additions & 0 deletions python-stdlib/selectors/selectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
"""selectors - CPython compatible selectors module for MicroPython

Thin layer over MicroPython's select.poll(), usable like the CPython module:

import selectors
sel = selectors.DefaultSelector()
sel.register(sock, selectors.EVENT_READ, data)
for key, mask in sel.select(timeout):
...

Differences from CPython:
- objects are keyed by identity, not by file descriptor (bare-metal
sockets have no fileno()); SelectorKey.fd is fileno() when available,
else -1
- get_map() returns a plain dict keyed by file object (treat it as
read-only)
- only PollSelector is provided, DefaultSelector is an alias for it

MIT license; Copyright (c) 2026 Pavel Revak <pavelrevak@gmail.com>
"""

import select as _select
from collections import namedtuple as _namedtuple

EVENT_READ = 1 << 0
EVENT_WRITE = 1 << 1

_EVENTS_ALL = EVENT_READ | EVENT_WRITE
_POLLIN = _select.POLLIN
_POLLOUT = _select.POLLOUT

SelectorKey = _namedtuple("SelectorKey", ["fileobj", "fd", "events", "data"])


def _fileobj_to_fd(fileobj):
"""Return the file descriptor of fileobj, -1 if it has none"""
if isinstance(fileobj, int):
fd = fileobj
else:
try:
fd = int(fileobj.fileno())
except (AttributeError, TypeError, ValueError, OSError):
return -1
if fd < 0:
raise ValueError("Invalid file descriptor: %d" % fd)
return fd


def _check_events(events):
if not events or events & ~_EVENTS_ALL:
raise ValueError("Invalid events: %r" % (events,))


def _poll_mask(events):
mask = 0
if events & EVENT_READ:
mask |= _POLLIN
if events & EVENT_WRITE:
mask |= _POLLOUT
return mask


class BaseSelector:
"""Selector abstract base class"""

def register(self, fileobj, events, data=None):
raise NotImplementedError

def unregister(self, fileobj):
raise NotImplementedError

def modify(self, fileobj, events, data=None):
self.unregister(fileobj)
return self.register(fileobj, events, data)

def select(self, timeout=None):
raise NotImplementedError

def close(self):
pass

def get_map(self):
raise NotImplementedError

def get_key(self, fileobj):
"""Return the key registered for fileobj.

Raises KeyError if not registered, RuntimeError if closed.
"""
mapping = self.get_map()
if mapping is None:
raise RuntimeError("Selector is closed")
if fileobj not in mapping:
raise KeyError("%r is not registered" % (fileobj,))
return mapping[fileobj]

def __enter__(self):
return self

def __exit__(self, *args):
self.close()


class PollSelector(BaseSelector):
"""Selector based on MicroPython select.poll()"""

def __init__(self):
self._poll = _select.poll()
self._map = {}

def _lookup(self, fileobj):
if self._map is None or fileobj not in self._map:
raise KeyError("%r is not registered" % (fileobj,))
return self._map[fileobj]

def register(self, fileobj, events, data=None):
if self._map is None:
raise ValueError("Selector is closed")
_check_events(events)
if fileobj in self._map:
raise KeyError("%r is already registered" % (fileobj,))
key = SelectorKey(fileobj, _fileobj_to_fd(fileobj), events, data)
try:
self._poll.register(fileobj, _poll_mask(events))
except (TypeError, OSError): # not a stream object
# no chaining: MicroPython warns on 'raise ... from'
raise ValueError("Invalid file object: %r" % (fileobj,))
self._map[fileobj] = key
return key

def unregister(self, fileobj):
key = self._lookup(fileobj)
del self._map[fileobj]
try:
self._poll.unregister(fileobj)
except OSError:
pass # object may already be closed
return key

def modify(self, fileobj, events, data=None):
key = self._lookup(fileobj)
_check_events(events)
if events != key.events:
self._poll.modify(fileobj, _poll_mask(events))
elif data is key.data:
return key
key = SelectorKey(fileobj, key.fd, events, data)
self._map[fileobj] = key
return key

def select(self, timeout=None):
"""Wait for registered objects to become ready or timeout expire.

timeout: None blocks, <= 0 polls, else seconds (float allowed).
Returns a list of (key, events) tuples.
"""
if self._map is None:
raise ValueError("Selector is closed")
if timeout is None:
timeout_ms = -1
elif timeout <= 0:
timeout_ms = 0
else:
# round up, so a short timeout does not turn into a busy poll
timeout_ms = int(timeout * 1000)
if timeout_ms < timeout * 1000:
timeout_ms += 1
ready = []
for fileobj, revents in self._poll.ipoll(timeout_ms):
key = self._map.get(fileobj)
if key is None:
continue
# error/hangup flags wake both directions, like CPython
events = 0
if revents & ~_POLLIN:
events |= EVENT_WRITE
if revents & ~_POLLOUT:
events |= EVENT_READ
ready.append((key, events & key.events))
return ready

def close(self):
self._map = None
self._poll = None

def get_map(self):
return self._map


DefaultSelector = PollSelector
Loading
Loading