fix(com): except pywintypes.com_error raised NameError without pywin32
Python evaluates an `except` clause on the way out of every failed call, not only the ones it is meant to catch. `pywintypes` is bound only when `sys.platform == "win32"` and the import succeeded, so on any host without pywin32 -- all of Linux, and Windows installed without the `[com]` extra -- an ordinary exception leaving `ComBackend._run` hit that clause and became `NameError: name 'pywintypes' is not defined`, burying the real error. Every Linux CI leg failed on exactly this, in test_ordinary_errors_are_unchanged. The timeout tests survived only because they matched an earlier `except`; nothing else in the suite sends a non-timeout exception through `_run`. Fixed with a module-level `_COM_ERROR` tuple: `(pywintypes.com_error,)` where the import worked, `()` where it did not. An empty tuple catches nothing, which is correct when no com_error type exists to be raised -- a placeholder Exception subclass would be worse than the bug, silently swallowing unrelated errors. Reproduced on Windows before fixing by deleting the module attribute, which resolves the name the same way the absent import does; that is now a test, alongside one pinning `_COM_ERROR` as a tuple. Verified through the locked environment CI uses: 1244 passed, 1 xfailed. README test-count snapshot 1243 -> 1245. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4e0f8a90fd
commit
e27071ab48
@@ -16,7 +16,7 @@ refine quality issues inside transactions, and deliver hashed artifacts with
|
|||||||
validation evidence.
|
validation evidence.
|
||||||
|
|
||||||
> **v1.5 release snapshot:** 154 tools · 6 resources · 5 prompt templates ·
|
> **v1.5 release snapshot:** 154 tools · 6 resources · 5 prompt templates ·
|
||||||
> 1243 collected tests. Runtime discovery through `system_about` is authoritative.
|
> 1245 collected tests. Runtime discovery through `system_about` is authoritative.
|
||||||
|
|
||||||
## Install in 10 seconds
|
## Install in 10 seconds
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -59,6 +59,18 @@ if _WIN32_AVAILABLE:
|
|||||||
else:
|
else:
|
||||||
_COM_IMPORTS_OK = False
|
_COM_IMPORTS_OK = False
|
||||||
|
|
||||||
|
#: What `_run` may catch as a COM failure. A *tuple*, because where pywin32 is
|
||||||
|
#: absent the right value is the empty one: `except ()` catches nothing, which
|
||||||
|
#: is exactly correct when no `com_error` type exists to be raised.
|
||||||
|
#:
|
||||||
|
#: `except pywintypes.com_error` used to be written directly, and Python
|
||||||
|
#: evaluates that name on the way out of every failed call — not only COM ones.
|
||||||
|
#: On any host without pywin32 (all of Linux, and Windows installed without the
|
||||||
|
#: `[com]` extra) it raised `NameError: name 'pywintypes' is not defined` and
|
||||||
|
#: buried the real exception. A placeholder Exception subclass would be worse
|
||||||
|
#: than the bug: it would silently swallow unrelated errors.
|
||||||
|
_COM_ERROR: tuple[type[BaseException], ...] = (pywintypes.com_error,) if _COM_IMPORTS_OK else ()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
@@ -682,7 +694,7 @@ class ComBackend(AutoCADBackend):
|
|||||||
"(press ESC in AutoCAD). Increase COM_CALL_TIMEOUT if a long "
|
"(press ESC in AutoCAD). Increase COM_CALL_TIMEOUT if a long "
|
||||||
"operation is expected."
|
"operation is expected."
|
||||||
) from e
|
) from e
|
||||||
except pywintypes.com_error as e:
|
except _COM_ERROR as e:
|
||||||
hr = e.args[0] if e.args else 0
|
hr = e.args[0] if e.args else 0
|
||||||
if hr in (-2147221246, -2147221005, -2147417842):
|
if hr in (-2147221246, -2147221005, -2147417842):
|
||||||
_COM_STATE.pop("app", None)
|
_COM_STATE.pop("app", None)
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ async def test_genuine_deadline_still_rebuilds_the_executor(com_backend, monkeyp
|
|||||||
assert await asyncio.wait_for(com_backend._run(lambda: "fresh"), timeout=3) == "fresh"
|
assert await asyncio.wait_for(com_backend._run(lambda: "fresh"), timeout=3) == "fresh"
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_value_error():
|
||||||
|
raise ValueError("nope")
|
||||||
|
|
||||||
|
|
||||||
async def test_ordinary_errors_are_unchanged(com_backend, monkeypatch):
|
async def test_ordinary_errors_are_unchanged(com_backend, monkeypatch):
|
||||||
"""Sanity: non-timeout failures never touched the executor and still don't."""
|
"""Sanity: non-timeout failures never touched the executor and still don't."""
|
||||||
monkeypatch.setattr(config.settings, "com_call_timeout", 5)
|
monkeypatch.setattr(config.settings, "com_call_timeout", 5)
|
||||||
@@ -130,3 +134,36 @@ async def test_ordinary_errors_are_unchanged(com_backend, monkeypatch):
|
|||||||
|
|
||||||
assert com_backend._executor is live
|
assert com_backend._executor is live
|
||||||
assert await com_backend._run(lambda x: x * 2, 21) == 42
|
assert await com_backend._run(lambda x: x * 2, 21) == 42
|
||||||
|
|
||||||
|
|
||||||
|
# ── the handler must be inert where pywin32 is absent ───────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
async def test_an_ordinary_error_survives_a_host_without_pywin32(com_backend, monkeypatch):
|
||||||
|
"""`except pywintypes.com_error` is evaluated on the way out of *every*
|
||||||
|
failed call, not only COM ones.
|
||||||
|
|
||||||
|
`pywintypes` is bound only when `sys.platform == "win32"` and the import
|
||||||
|
succeeded, so on Linux — and on a Windows box that installed without the
|
||||||
|
`[com]` extra — that clause raised `NameError: name 'pywintypes' is not
|
||||||
|
defined` and buried the real exception. Every Linux CI leg failed on
|
||||||
|
exactly this; the timeout tests survived only because they matched an
|
||||||
|
earlier `except`.
|
||||||
|
|
||||||
|
Deleting the module attribute reproduces the absent import faithfully: the
|
||||||
|
clause resolves the name through the module globals either way.
|
||||||
|
"""
|
||||||
|
monkeypatch.delattr(cb, "pywintypes", raising=False)
|
||||||
|
monkeypatch.setattr(config.settings, "com_call_timeout", 5)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="nope"):
|
||||||
|
await com_backend._run(_raise_value_error)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_the_com_error_clause_is_a_tuple_so_it_can_be_empty():
|
||||||
|
"""An empty tuple catches nothing, which is the correct behaviour when no
|
||||||
|
`com_error` type exists to be raised — a placeholder Exception subclass
|
||||||
|
would swallow unrelated errors instead."""
|
||||||
|
assert isinstance(cb._COM_ERROR, tuple)
|
||||||
|
if not cb._COM_IMPORTS_OK:
|
||||||
|
assert cb._COM_ERROR == ()
|
||||||
|
|||||||
Reference in New Issue
Block a user