Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
3744b19
feat: run popen Message IO on Trio host threads
RonnyPfannschmidt Jul 22, 2026
beb36b9
feat: schedule worker remote_exec from the Trio nursery
RonnyPfannschmidt Jul 22, 2026
37a184f
refactor: drop eventlet and gevent execmodels
RonnyPfannschmidt Jul 22, 2026
60e6ce9
feat: bootstrap same-python popen worker via `python -m`
RonnyPfannschmidt Jul 22, 2026
dfe7391
add claude settings
RonnyPfannschmidt Jul 22, 2026
603dfc6
feat: provision foreign-python popen workers via uv
RonnyPfannschmidt Jul 22, 2026
7656368
test: add local ssh-connect harness + typing cleanups
RonnyPfannschmidt Jul 23, 2026
f153271
feat: run ssh gateways on the Trio path
RonnyPfannschmidt Jul 23, 2026
5197b1e
feat: ship the execnet wheel to ssh remotes for dev coordinators
RonnyPfannschmidt Jul 23, 2026
21096da
feat: add execnet-socketserver console command
RonnyPfannschmidt Jul 23, 2026
420693a
feat: run direct socket gateways on Trio with subprocess workers
RonnyPfannschmidt Jul 23, 2026
b83ff3b
feat: migrate installvia to a GATEWAY_START_SOCKET protocol message
RonnyPfannschmidt Jul 23, 2026
62b746b
refactor: remove the legacy socket coordinator code
RonnyPfannschmidt Jul 23, 2026
19d620d
feat: run popen via-gateways on Trio via GATEWAY_START_POPEN
RonnyPfannschmidt Jul 23, 2026
92969c5
feat: spawn arbitrary via sub-gateways with GATEWAY_START_SUB
RonnyPfannschmidt Jul 24, 2026
48d328c
feat: run vagrant_ssh gateways on the Trio path
RonnyPfannschmidt Jul 24, 2026
3d1d31e
refactor!: remove the legacy bootstrap stack and EXECNET_TRIO_HOST hatch
RonnyPfannschmidt Jul 24, 2026
0a24039
refactor: unify transports on StapledStream with sans-IO frame decoding
RonnyPfannschmidt Jul 24, 2026
f271da5
refactor: extract the LoopPortal cross-thread primitive
RonnyPfannschmidt Jul 24, 2026
0eb6cc5
feat: add the trio-native AsyncGateway core with low-level RawChannels
RonnyPfannschmidt Jul 24, 2026
ed5f99f
feat: layer the serialized AsyncChannel API on RawChannel
RonnyPfannschmidt Jul 24, 2026
80ad5df
feat: run async popen gateways with remote_exec inside the user's tri…
RonnyPfannschmidt Jul 24, 2026
b3b1ab8
feat: add AsyncGroup owning gateways as nursery child scopes
RonnyPfannschmidt Jul 24, 2026
8286b77
feat: make the via tunnel frame-native over raw channel routing
RonnyPfannschmidt Jul 24, 2026
57f899f
docs: record B.4 completion in the phase handoff
RonnyPfannschmidt Jul 24, 2026
f932084
feat: rebuild the sync API as a facade over the async gateway engine
RonnyPfannschmidt Jul 24, 2026
80e237a
docs: record B.5 completion in the phase handoff
RonnyPfannschmidt Jul 24, 2026
4f84335
feat: split the public surface into execnet.sync / execnet.trio / exe…
RonnyPfannschmidt Jul 25, 2026
33decae
docs: record phase B completion in the handoff
RonnyPfannschmidt Jul 25, 2026
51b9053
fix: attach the bridge session before it can serve messages
RonnyPfannschmidt Jul 25, 2026
a69b844
chore: add pytest-xdist to the dev group, ignore claude local settings
RonnyPfannschmidt Jul 25, 2026
72b0eef
docs: hand off phase C/D — worker axes gap analysis, supersede the B doc
RonnyPfannschmidt Jul 25, 2026
2742eb0
fix: drain asyncssh redirect cleanup in the local ssh harness
RonnyPfannschmidt Jul 25, 2026
0d123bd
docs: record PR #422 and the xfail audit in the phase C handoff
RonnyPfannschmidt Jul 26, 2026
2c78416
feat: introduce the boundary kit (Wakener/Mailbox/OneShot) in execnet…
RonnyPfannschmidt Jul 26, 2026
2a40a55
feat!: rebase the sync Channel onto the async core (single dispatch)
RonnyPfannschmidt Jul 26, 2026
811fd99
feat!: retire the ExecModel machinery; add the wait= spec axis
RonnyPfannschmidt Jul 26, 2026
845510e
feat: add execnet.aio -- the asyncio-native API over the Trio host
RonnyPfannschmidt Jul 26, 2026
b01d009
feat: gevent wait backend (wait=gevent) with opt-in tests
RonnyPfannschmidt Jul 26, 2026
5df051c
docs: record the landed boundary rethink (P1-P5) in the handoff
RonnyPfannschmidt Jul 26, 2026
7969f7e
docs: record the decided Phase C plan (axes matrix, trio-only core)
RonnyPfannschmidt Jul 26, 2026
b2f43c3
refactor: extract worker exec placement strategies behind the FIFO pump
RonnyPfannschmidt Jul 26, 2026
d8f22c5
feat!: answer info natively and apply chdir/nice/env from worker config
RonnyPfannschmidt Jul 26, 2026
cd2c871
feat: pure-async worker profile (execmodel=trio)
RonnyPfannschmidt Jul 26, 2026
36e904e
feat!: restore classic hybrid exec for execmodel=thread
RonnyPfannschmidt Jul 26, 2026
cbce183
feat: gevent worker profile and gevent-safe management ops
RonnyPfannschmidt Jul 26, 2026
87c5843
docs: record the landed Phase C worker profiles in the handoff
RonnyPfannschmidt Jul 26, 2026
3031810
feat!: run channel callbacks in an off-loop consumer task
RonnyPfannschmidt Jul 28, 2026
34c9953
fix: serialize ints below the signed int4 minimum via the long opcode
RonnyPfannschmidt Jul 28, 2026
0d36f7f
test: hypothesis stress tests for channel callbacks with a --stress knob
RonnyPfannschmidt Jul 28, 2026
f911ce7
refactor: name the main_thread_only deadlock-admission timeout
RonnyPfannschmidt Jul 28, 2026
8eee047
feat!: hide the standalone serializer from the public API
RonnyPfannschmidt Jul 28, 2026
6a33716
test: type-check the callback stress tests under mypy
RonnyPfannschmidt Jul 28, 2026
12fdbbe
refactor!: drop the injected remote shell script
RonnyPfannschmidt Jul 28, 2026
6718808
refactor!: drop the quitserver script
RonnyPfannschmidt Jul 28, 2026
7124fc0
refactor!: drop the socketserver restart loop script
RonnyPfannschmidt Jul 28, 2026
47e5b78
refactor!: drop the pywin32 windows service wrapper
RonnyPfannschmidt Jul 28, 2026
fa0d7a8
refactor: move the socket server to execnet._socketserver
RonnyPfannschmidt Jul 28, 2026
871dfd7
refactor!: drop the py2/py3 string coercion machinery
RonnyPfannschmidt Jul 28, 2026
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
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(uv run pytest:*)"
]
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ include/
.vagrant.d/
.config/
.local/
.claude/settings.local.json
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ repos:
- id: mypy
additional_dependencies:
- pytest
- trio
- hypothesis
- types-pywin32
- types-gevent
28 changes: 28 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@
------------------

* `#380 <https://github.com/pytest-dev/execnet/pull/380>`__: Add support for Python 3.13 and 3.14, and drop EOL 3.8 and 3.9.
* Trio host-thread Message IO for local ``popen`` + import bootstrap (coordinator and
worker). Adds a hard ``trio`` dependency. Disable with ``EXECNET_TRIO_HOST=0``.
Other gateway types and greenlet execmodels keep the legacy thread path.
* Removed ``execnet.script.shell``, an interactive remote prompt that injected its own
source into the pre-Trio socket server. The Trio socket server never execs an incoming
source line, so the module could no longer work against it.
* Removed ``execnet.script.quitserver``, which shut a socket server down by sending it
``"raise KeyboardInterrupt"`` to exec. It relied on the same removed handshake.
* Removed ``execnet.script.loop_socketserver``, a restart loop around a sibling
``socketserver.py`` file. The socket server serves connections in a loop itself
(``--once`` opts out), and the sibling-file path never resolved for an installed
execnet anyway.
* Removed ``execnet.script.socketserverservice``, the pywin32 Windows service wrapper.
Wrap the ``execnet-socketserver`` console command with a service host such as NSSM
instead; the socket gateway example documents how.
* Moved the socket server to ``execnet._socketserver`` and removed the now empty
``execnet.script`` package. The ``execnet-socketserver`` console command is unchanged
and remains the supported way to run it.
* Removed ``Gateway.reconfigure`` and ``Channel.reconfigure`` along with the
``py2str_as_py3str`` / ``py3str_as_py2str`` arguments of ``gateway_base.loads`` and
``gateway_base.load``. They configured string coercion between Python2 and Python3
peers, which execnet can no longer have: ``py2str_as_py3str`` was already unreachable
because only a Python2 serializer emits the opcode it gates. The ``RECONFIGURE``
message code stays reserved but is no longer sent or handled.
* The serializer dropped the retired ``PY2STRING`` and ``UNICODE`` opcodes and renamed
``PY3STRING`` to ``STRING``. Values dumped by execnet running on Python2 no longer
load. Opcode bytes are unchanged for every type that survives.


2.1.2 (2025-11-11)
------------------
Expand Down
91 changes: 57 additions & 34 deletions doc/basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ Examples for valid gateway specifications
``default`` via SSH through Vagrant's ``vagrant ssh`` command. It supports
the same additional parameters as regular SSH connections.

* ``popen//python=python2.7//nice=20`` specification of
a python subprocess using the ``python2.7`` executable which must be
* ``popen//python=python3.13//nice=20`` specification of
a python subprocess using the ``python3.13`` executable which must be
discoverable through the system ``PATH``; running with the lowest
CPU priority ("nice" level). By default current dir will be the
current dir of the instantiator.
Expand All @@ -57,12 +57,10 @@ Examples for valid gateway specifications
same interpreter as the one it is initiated from and additionally
remotely sets an environment variable ``NAME`` to ``value``.

* ``popen//execmodel=eventlet`` specifies a subprocess that uses the
same interpreter as the one it is initiated from but will run the
other side using eventlet for handling IO and dispatching threads.

* ``socket=192.168.1.4:8888`` specifies a Python Socket server
process that listens on ``192.168.1.4:8888``
process that listens on ``192.168.1.4:8888``. Such a server can be
started with the ``execnet-socketserver`` console command, e.g. run
anywhere with ``uvx --from execnet execnet-socketserver :8888``.

.. versionadded:: 1.5

Expand All @@ -88,10 +86,6 @@ a channel object whose symmetric counterpart channel
is available to the remotely executing source.


.. method:: Gateway.reconfigure([py2str_as_py3str=True, py3str_as_py2str=False])

Reconfigures the string-coercion behaviour of the gateway

.. _`Channel`:
.. _`channel-api`:

Expand Down Expand Up @@ -138,30 +132,29 @@ processes then you often want to call ``group.terminate()``
yourself and specify a larger or not timeout.


threading models: gevent, eventlet, thread, main_thread_only
threading models: thread, main_thread_only
====================================================================

.. versionadded:: 1.2 (status: experimental!)

execnet supports "main_thread_only", "thread", "eventlet" and "gevent"
as thread models on each of the two sides. You need to decide which
model to use before you create any gateways::
execnet supports "thread" and "main_thread_only" as thread models on
each of the two sides. You need to decide which model to use before
you create any gateways::

# content of threadmodel.py
import execnet
# locally use "eventlet", remotely use "thread" model
execnet.set_execmodel("eventlet", "thread")
# locally use "thread", remotely use "main_thread_only" model
execnet.set_execmodel("thread", "main_thread_only")
gw = execnet.makegateway()
print (gw)
print (gw.remote_status())
print (gw.remote_exec("channel.send(1)").receive())

You need to have eventlet installed in your environment and then
you can execute this little test file::
You can execute this little test file::

$ python threadmodel.py
<Gateway id='gw0' receive-live, eventlet model, 0 active channels>
<RInfo 'numchannels=0, numexecuting=0, execmodel=thread'>
<Gateway id='gw0' receive-live, thread model, 0 active channels>
<RInfo 'numchannels=0, numexecuting=0, execmodel=main_thread_only'>
1

How to execute in the main thread
Expand Down Expand Up @@ -219,23 +212,53 @@ configure a tracing mechanism:

.. _`dumps/loads`:
.. _`dumps/loads API`:
.. _`serialization`:

Cross-interpreter serialization of Python objects
Sending objects over a channel
=======================================================

.. versionadded:: 1.1
A channel carries only **simple builtin data**: ``None``, ``bool``,
``int``, ``float``, ``complex``, ``bytes``, ``str`` and arbitrarily nested
``list`` / ``tuple`` / ``set`` / ``frozenset`` / ``dict`` of those -- plus
**channel references**, which arrive as channels on the peer. That is the
entire contract.

execnet does **not** pickle and does **not** encode rich objects for you:
arbitrary instances, functions, ``datetime``, dataclasses, pydantic models,
numpy arrays, enums, etc. have no wire representation. This is deliberate;
encoded / rich-object channels are out of scope for execnet.

Sending an unsupported value raises ``DumpError`` (a subclass of
``DataFormatError``); a corrupt or protocol-mismatched payload on receive
raises ``LoadError``. These signal a **caller error to resolve** -- reduce
the value to simple data before sending -- not a transport failure. The
standalone serializer itself is an internal implementation detail
(``execnet.gateway_base``) and is not part of the public API.

Encode rich objects yourself
-------------------------------------------------------

Turning a rich object into simple data (and back) is the caller's job. Use
an established encoding mechanism rather than expecting the channel to do it:

- **pydantic**: ``model.model_dump(mode="json")`` reduces a model to simple
data (``datetime`` -> ISO string, ``UUID`` / ``Enum`` / ``Decimal`` ->
primitives); ``Model.model_validate(...)`` rebuilds it on the other side.
``TypeAdapter`` covers non-model types.

::

Execnet exposes a function pair which you can safely use to
store and load values from different Python interpreters
(e.g. Python2 and Python3, PyPy and Jython). Here is
a basic example::
channel.send(model.model_dump(mode="json"))
# peer:
model = MyModel.model_validate(channel.receive())

>>> import execnet
>>> dump = execnet.dumps([1,2,3])
>>> execnet.loads(dump)
[1,2,3]
- **pytest** does exactly this above execnet: pytest-xdist ships
``TestReport`` objects with the ``pytest_report_to_serializable`` /
``pytest_report_from_serializable`` hooks (rich report <-> simple dict)
around ``channel.send`` / ``channel.receive``.

For more examples see :ref:`dumps/loads examples`.
- **stdlib**: ``dataclasses.asdict(obj)``, ``dt.isoformat()`` /
``datetime.fromisoformat``, or ``json`` with a ``default=`` hook.

.. autofunction:: execnet.dumps(spec)
.. autofunction:: execnet.loads(spec)
Channels are the one non-builtin you *can* send: nested channel references
pass through intact, so callbacks and sub-streams need no encoding.
153 changes: 0 additions & 153 deletions doc/example/hybridpython.rst

This file was deleted.

19 changes: 0 additions & 19 deletions doc/example/py3topy2.py

This file was deleted.

Loading
Loading