Windows: three bugs that stopped mbasic importing at all¶
Created: 2026-08-04
Status: Fixed
Regression test: tests/regression/integration/test_windows_imports.py
mbasic advertises Windows support - Operating System :: OS Independent in
pyproject.toml, a Windows section in docs/user/INSTALL.md, a %APPDATA%
settings path in the code. It could not be imported on Windows at all. Three
separate bugs, each fatal on its own.
None of them needs a Windows machine to reproduce, because none of them is about Windows behavior - they are about the environment being different.
1. tty and termios imported at module scope¶
src/basic_builtins.py opened with:
import tty
import termios
Neither module exists in the Windows standard library. INKEY$ needs them, but
only inside the branch that is skipped when sys.platform == 'win32', so the
imports were paying a cost the code never intended. Importing
src.basic_builtins raised ModuleNotFoundError, and since src.interpreter
imports it, so did src.interpreter, src.interactive,
src.immediate_executor and src.ui. That is the whole interpreter.
Guarded now, with the names set to None and an explicit check in the INKEY$
POSIX branch. The check goes before sys.stdin.isatty() deliberately: with no
terminal layer there is no reason to touch stdin at all, and isatty() itself
raises on a closed or substituted stdin.
2. curses imported unconditionally by the I/O package¶
src/iohandler/__init__.py did from .curses_io import CursesIOHandler, and
curses_io.py does import curses. curses is POSIX-only in the standard
library, so importing any I/O handler - including the plain console one -
failed on Windows.
Guarded exactly the way src/ui/__init__.py already guards its optional Tk and
curses UI backends: CursesIOHandler becomes None and _has_curses_io
becomes False. Nothing in the tree consumes the package-level name, so this
costs nothing; tests/test_breakpoint_simple.py imports the submodule directly
and still gets a clear ImportError.
3. A shipped JSON read in the locale's codepage¶
This is the subtle one, and the reason it is worth writing down.
src/ui/keybindings.py read its config at module scope:
with open(_config_path, 'r') as f: # no encoding=
_config = json.load(f)
Without encoding=, Python decodes using the locale's preferred encoding - on
Windows the ANSI codepage. src/ui/curses_keybindings.json ships twelve
non-ASCII bytes: the arrows U+2191 and U+2193, as UTF-8. On any Windows whose
codepage is not UTF-8 - cp932 (Japanese), cp936, cp949, cp950, cp874 - that
decode fails:
UnicodeDecodeError: 'cp932' codec can't decode byte 0x91 in position 2461
UnicodeDecodeError is a ValueError, not an OSError, so it also slipped
past the except (ImportError, OSError) guard in src/ui/__init__.py that
exists precisely to keep an unusable optional backend from taking down the CLI.
The result: on a Japanese Windows with urwid installed, mbasic --ui cli died
before printing anything.
Two fixes, because either alone leaves a trap:
- every read of a file we ship now names
encoding='utf-8'- the two readers insrc/ui/keybindings.pyandsrc/ui/keybinding_loader.py, and the help-file readers insrc/ui/help_widget.pyandsrc/ui/tk_help_browser.py(68 of the shipped help documents contain non-ASCII); - the guards in
src/ui/__init__.pynow catchValueErrortoo, so a data file that will not decode can never again take the CLI down with it.
Note src/ui/keybinding_loader.py wraps its read in except Exception: pass,
so the codepage mismatch there did not crash - it silently returned no
keybindings at all. Quieter, and worse.
Reproducing without Windows¶
The regression test simulates both environmental differences directly:
python3 tests/regression/integration/test_windows_imports.py
- Missing modules: a
sys.meta_pathhook installed through a temporarysitecustomize.pyrefusestermios,tty,curses,fcntl,pty,grp,pwd,resource,readlineandmsvcrt, raisingModuleNotFoundErrorthe way Python really does. - Non-UTF-8 default encoding:
LC_ALL=CwithPYTHONUTF8=0andPYTHONCOERCECLOCALE=0makesopen()default to ASCII. Both of those variables are needed - Python otherwise coerces the C locale to C.UTF-8 (PEP 538) or turns on UTF-8 mode (PEP 540) and the bug hides.
By hand:
LC_ALL=C PYTHONCOERCECLOCALE=0 PYTHONUTF8=0 python3 -c "import src.ui.keybindings"
34 checks, against 18 failures on the code before the fix. It runs the real
mbasic under each simulation and under both at once.
What the test does not do is simulate Windows. sys.platform stays
'linux', so every if sys.platform == 'win32' branch - the msvcrt paths,
the %APPDATA% settings directory, cls in clear_screen - is never executed.
It pins the import dimension, which is what was broken. Do not read a passing
run as "mbasic works on Windows".
Also changed: the declared Python floor¶
pyproject.toml said requires-python = ">=3.8", which was never achievable.
src/parser.py and src/input_sanitizer.py - both on the unconditional startup
path - annotate with PEP 585 builtin generics (tuple[str, bool]). Those are
evaluated at import time and cannot be subscripted before 3.9, and there is no
from __future__ import annotations anywhere in the tree. A 3.8 install would
have succeeded and then failed on first run. Now >=3.9, with the 3.8
classifier dropped.
Still broken on Windows (runtime, not imports)¶
Fixing the imports gets mbasic to start. It does not make it work well:
andINKEY$andINPUT$swallow special keys.Both fixed - see WINDOWS_CONSOLE_KEYS.md, which also covers the POSIXLOCATEprints garbage.INKEY$bug found underneath them.- No line editing or history.
readlineis absent;pyreadline3is the usual remedy and is not offered in any extra inpyproject.toml. SAVEwrites CRLF and mangles high-bit program text, becausesrc/editing/manager.pyopens user files in text mode with noencoding=. That one is deliberately untouched here: it changes how user data is written, anddocs/user/FILE_FORMAT_COMPATIBILITY.mdmakes claims about it that need deciding on purpose rather than in passing.- The state directory is split.
src/settings.pycorrectly uses%APPDATA%/mbasic, butsrc/ui/auto_save.pyandsrc/ui/recent_files.pyusePath.home()/'.mbasic'. --compile-ccannot work:src/codegen_backend.pyshells out to/usr/bin/env.