Skip to content

Commit 4b4c5ca

Browse files
Technologicatclaude
andcommitted
net.client: fix tab completion on macOS (libedit parse_and_bind)
`unpythonic.net.client` issued readline.parse_and_bind("tab: complete") unconditionally. That's the GNU readline dialect. macOS ships `readline` backed by `libedit` (not GNU readline), which speaks a different `parse_and_bind` dialect — the GNU form is silently ignored there, so macOS users of the REPL client have had no working tab completion for some time. Add a `platform.system() == "Darwin"` branch that issues `readline.parse_and_bind("bind ^I rl_complete")` on macOS instead — the libedit dialect that actually wires up tab completion. Mirrors the pattern long-used in `raven.librarian.minichat`, and newly added in the same session to `mcpyrate.repl.macropython`. The `unpythonic.net` REPL subsystem remains documented as POSIX-only (see 2.0.0 CHANGELOG — `ptyproxy` depends on `termios`, unavailable on Windows). This commit does *not* make the client Windows-capable; it just brings `net.client`'s `parse_and_bind` into consistent shape with the rest of the fleet, so that when we eventually do the Windows-compat work for `unpythonic.net`, this one line is already correct. Also promotes the previous `# TODO: do we need to call this, PyPy doesn't support it?` comment to the same definitive form `# PyPy ignores this, but not needed there.` used in mcpyrate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4f0f4a5 commit 4b4c5ca

2 files changed

Lines changed: 10 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
**Fixed**:
66

7+
- `unpythonic.net.client`: tab completion now works on macOS. The REPL client issued `readline.parse_and_bind("tab: complete")` unconditionally, but macOS ships `readline` backed by `libedit` (not GNU readline), which speaks a different `parse_and_bind` dialect: the GNU form is silently ignored and tab completion does nothing. Now detects `platform.system() == "Darwin"` and issues `readline.parse_and_bind("bind ^I rl_complete")` on macOS instead. This fix is a prerequisite for eventually supporting `unpythonic.net.client/server` on MS Windows too — the `unpythonic.net` REPL subsystem is still documented as POSIX-only (see 2.0.0), but the `parse_and_bind` branch is now in the right shape for when the rest of the Windows-compat work happens.
78
- `unpythonic.misc.timer` and `unpythonic.timeutil.ETAEstimator`: switched the underlying clock from `time.monotonic()` to `time.perf_counter()`. Both are monotonic (guaranteed since Python 3.3), but `perf_counter` is documented as *"a clock with the highest available resolution to measure a short duration"*, whereas `monotonic`'s resolution is implementation-defined. On Windows specifically, `time.monotonic()` is backed by a low-resolution (~16 ms) tick counter, so a `with timer() as t: ...` block that ran in microseconds — such as a PyPy-JIT'd `for _ in range(int(1e6)): pass` — recorded `t.dt` as **exactly 0.0**, silently producing wrong results and, in downstream code that divided by it, a `ZeroDivisionError`. On POSIX the two clocks are usually backed by the same high-resolution source, so this was a latent Windows-only bug. `ETAEstimator` is not affected as a correctness bug at its typical per-task scale (seconds to minutes), but was switched for consistency. We give up `monotonic`'s "comparable across processes" guarantee, which neither class needs — both measure a dynamic extent in wall-clock time within a single process.
89
- `unpythonic.test.runner`: module discovery crashed on MS Windows with `re.error: bad escape (end of pattern) at position 0`. The runner used `re.sub(os.path.sep, ...)` to convert a relative path into a dotted module name, which worked by accident on POSIX (where `os.path.sep` is `/`, not a regex metacharacter), but on Windows `os.path.sep` is a lone backslash — an incomplete escape as a regex pattern. Fixed by using `str.replace` instead, which treats both arguments as literal strings. Affects any project that reuses `unpythonic.test.runner` for its own macro-enabled tests on Windows.
910

unpythonic/net/client.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
for a remote tab completer, and a separate client-side `input()` loop.)
3333
"""
3434

35+
import platform
3536
import readline # noqa: F401, input() uses the readline module if it has been loaded.
3637
import socket
3738
import select
@@ -171,7 +172,14 @@ class SessionExit(Exception):
171172
# Set up remote tab completion, using a custom completer for readline.
172173
# https://stackoverflow.com/questions/35115208/is-there-any-way-to-combine-readline-rlcompleter-and-interactiveconsole-in-pytho
173174
readline.set_completer(controller.complete)
174-
readline.parse_and_bind("tab: complete") # TODO: do we need to call this, PyPy doesn't support it?
175+
# macOS ships `readline` backed by `libedit`, which speaks a
176+
# different `parse_and_bind` dialect than GNU readline. Detect
177+
# by platform to keep tab completion working on Macs. See
178+
# https://stackoverflow.com/questions/7116038/python-repl-tab-completion-on-macos
179+
if platform.system() == "Darwin": # macOS
180+
readline.parse_and_bind("bind ^I rl_complete")
181+
else: # Linux, Windows (pyreadline3)
182+
readline.parse_and_bind("tab: complete") # PyPy ignores this, but not needed there.
175183

176184
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: # remote REPL session
177185
sock.connect((host, repl_port)) # TODO: IPv6 support

0 commit comments

Comments
 (0)