Files
mxaccessgw/code-reviews/Client.Python/findings.md
T
Joseph Doherty f0a4af62b9 Review the clients/ language clients; mark Server-001/003 resolved
Adds per-module code reviews for the five language clients under
clients/ (Client.Dotnet, Client.Go, Client.Java, Client.Python,
Client.Rust) at commit 3cc53a8 — 53 findings (4 High, 15 Medium,
34 Low; all Open). Extends REVIEW-PROCESS.md so a "module" may also be
a language client under clients/, not only a src/ project.

Marks Server-001 (Critical) and Server-003 (High) Resolved — fixed in
a8aafdf — and regenerates code-reviews/README.md (now 11 modules).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:51:00 -04:00

11 KiB

Code Review — Client.Python

Field Value
Module clients/python
Reviewer Claude Code
Review date 2026-05-18
Commit reviewed 3cc53a8
Status Reviewed
Open findings 12

Checklist coverage

# Category Result
1 Correctness & logic bugs Issues found: dead closed variable (Client.Python-004); float/bytes value-mapping assumptions (Client.Python-008).
2 mxaccessgw conventions Largely adheres; one missing export and a *_raw MXAccess-failure documentation gap (Client.Python-002, Client.Python-012).
3 Concurrency & thread safety Issue found: close() idempotency claim does not hold under concurrent close (Client.Python-006).
4 Error handling & resilience Issues found: inconsistent timeout-kwarg fallback (Client.Python-003); success == 0 default-value hazard (Client.Python-011); inconsistent cancel helpers (Client.Python-007).
5 Security No issues found — API keys redacted in repr and CLI output, TLS supported, no secret logging.
6 Performance & resource management Issue found: discover_hierarchy buffers the whole hierarchy in memory (Client.Python-005).
7 Design-document adherence Matches the design docs closely; minor CLI doc drift (Client.Python-001).
8 Code organization & conventions Issues found: MxGatewayCommandError omitted from __all__ (Client.Python-002); fragile circular-import workaround (Client.Python-010).
9 Testing coverage Issue found: write2, add_item2, bulk-size limits, TLS ca_file, and CLI command bodies untested (Client.Python-009).
10 Documentation & comments Issue found: stale "scaffold" package description (Client.Python-001).

Findings

Client.Python-001

Field Value
Severity Low
Category Documentation & comments
Location clients/python/pyproject.toml:8,25, clients/python/src/mxgateway_cli/commands.py:25
Status Open

Description: The package description in pyproject.toml still says "Async Python client scaffold" even though the client is fully implemented. Stale "scaffold" wording misrepresents maturity to anyone reading PyPI metadata. (The mxgw-py console-script name is itself consistent between pyproject.toml and the README.)

Recommendation: Update the pyproject.toml description to drop "scaffold"; keep README CLI examples in sync with the actual mxgw-py entry point.

Resolution: (open)

Client.Python-002

Field Value
Severity Low
Category Code organization & conventions
Location clients/python/src/mxgateway/__init__.py:27
Status Open

Description: MxGatewayCommandError is imported into __init__.py and is a documented public exception, but it is missing from __all__. It is the parent of MxAccessError and a meaningful catch target, so omitting it from the public surface is inconsistent — from mxgateway import * will not expose it and tooling that respects __all__ treats it as private.

Recommendation: Add "MxGatewayCommandError" to the __all__ list.

Resolution: (open)

Client.Python-003

Field Value
Severity Medium
Category Error handling & resilience
Location clients/python/src/mxgateway/client.py:125-137,155-173
Status Open

Description: stream_events_raw and query_active_alarms call the stub directly with a timeout kwarg when stream_timeout is set, with no TypeError fallback. galaxy.py:watch_deploy_events and _unary do have a fallback that strips timeout if the callable rejects it. This asymmetry means a fake/older stub that does not accept timeout crashes for gateway streams but not Galaxy streams. It is only masked today because stream_timeout defaults to None.

Recommendation: Apply the same try/except TypeError timeout-fallback pattern to stream_events_raw and query_active_alarms, or remove the fallback everywhere and standardise on a single behaviour.

Resolution: (open)

Client.Python-004

Field Value
Severity Low
Category Correctness & logic bugs
Location clients/python/src/mxgateway_cli/commands.py:386,402-404
Status Open

Description: In _smoke, the local variable closed is set to False and never reassigned; the finally block's if not closed: is therefore always true. This is dead/misleading code suggesting a removed early-close path.

Recommendation: Remove the closed variable and the if not closed: guard; call await session.close() directly in the finally block (or use async with session:).

Resolution: (open)

Client.Python-005

Field Value
Severity Medium
Category Performance & resource management
Location clients/python/src/mxgateway/galaxy.py:117-140
Status Open

Description: discover_hierarchy pages through the entire Galaxy object hierarchy and accumulates every GalaxyObject (each carrying its full attribute list) into a single in-memory list before returning. For a large Galaxy this is a very large allocation with no streaming alternative and no caller-side bound.

Recommendation: Offer an async-generator variant (e.g. iter_hierarchy()) that yields objects/pages as they arrive, keeping discover_hierarchy() as a convenience wrapper. At minimum document the memory characteristic.

Resolution: (open)

Client.Python-006

Field Value
Severity Low
Category Concurrency & thread safety
Location clients/python/src/mxgateway/client.py:74-82, clients/python/src/mxgateway/galaxy.py:85-93, clients/python/src/mxgateway/session.py:38-55
Status Open

Description: close() on the clients and Session.close() use a plain self._closed check-then-set with an await between, with no lock. If two coroutines call close() concurrently both can pass the guard before either sets it, causing a double channel.close() / double CloseSession RPC. Single-task usage is the documented contract, so impact is low, but the idempotency guarantee asserted in docstrings only holds for sequential calls.

Recommendation: Set self._closed = True before the await, or guard with an asyncio.Lock, so the idempotency claim holds under concurrent close.

Resolution: (open)

Client.Python-007

Field Value
Severity Low
Category Error handling & resilience
Location clients/python/src/mxgateway/client.py:204-213
Status Open

Description: _canceling_iterator (gateway event stream) does not catch asyncio.CancelledError to invoke call.cancel() explicitly — it relies on the finally block. galaxy.py:_canceling_iterator does explicitly catch CancelledError, cancel, and re-raise. The two are functionally equivalent today, but the inconsistency between near-identical helpers invites future divergence.

Recommendation: Make the two _canceling_iterator helpers identical, ideally by factoring a single shared helper.

Resolution: (open)

Client.Python-008

Field Value
Severity Low
Category Correctness & logic bugs
Location clients/python/src/mxgateway/values.py:62-67,83-88
Status Open

Description: to_mx_value maps any Python float to VT_R8/MX_DATA_TYPE_DOUBLE with no handling for nan/inf, which are serialised and forwarded to MXAccess which may reject or mis-handle them. bytes is mapped to VT_RECORD/MX_DATA_TYPE_UNKNOWN, a questionable default. The data_type keyword exists but Session.write never forwards it.

Recommendation: Document the float/bytes mapping assumptions, optionally validate finiteness, and consider plumbing the data_type keyword through Session.write/write2.

Resolution: (open)

Client.Python-009

Field Value
Severity Medium
Category Testing coverage
Location clients/python/tests/
Status Open

Description: Several non-trivial public paths are untested: Session.write2/add_item2 request construction; the bulk-size limit _ensure_bulk_size/MAX_BULK_ITEMS guard; the None-argument TypeError guards in bulk methods; the TLS ca_file read path in create_channel; most CLI command bodies; and map_rpc_error's default (non-auth) branch.

Recommendation: Add tests for write2/add_item2 request shape, the bulk-size ValueError, the ca_file TLS branch, the generic map_rpc_error fallthrough, and at least one happy-path CLI command using a fake stub.

Resolution: (open)

Client.Python-010

Field Value
Severity Low
Category Code organization & conventions
Location clients/python/src/mxgateway/session.py:404, clients/python/src/mxgateway_cli/commands.py:422-425
Status Open

Description: session.py ends with a module-level late import from .client import GatewayClient # noqa: E402 purely to satisfy a string type hint, and commands.py:_session does a function-local import. Both work around a circular dependency that from __future__ import annotations (already in effect) makes unnecessary. _session also lacks a return type annotation.

Recommendation: Drop the runtime late import in session.py and use a TYPE_CHECKING-guarded import for the hint; add the -> Session return annotation to commands.py:_session.

Resolution: (open)

Client.Python-011

Field Value
Severity Low
Category Error handling & resilience
Location clients/python/src/mxgateway/errors.py:122-148
Status Open

Description: ensure_mxaccess_success raises MxAccessError if any mx_status.success == 0. This treats success == 0 as the failure sentinel, but 0 is also the proto3 scalar default for an unset MxStatusProxy. If the gateway ever returns a reply with an unpopulated status entry (e.g. a partially-filled bulk result), the client raises MxAccessError even though no real failure occurred.

Recommendation: Confirm against the proto/gateway contract whether success is guaranteed populated for every statuses entry; if not, key the failure decision on an explicit failure field rather than the success == 0 default.

Resolution: (open)

Client.Python-012

Field Value
Severity Low
Category mxaccessgw conventions
Location clients/python/src/mxgateway/client.py:84-108, clients/python/src/mxgateway/session.py:57-77
Status Open

Description: Session.invoke_raw does not run ensure_mxaccess_success while Session.invoke does, so a caller using invoke_raw for parity tests gets a reply where an MXAccess HRESULT failure is silently embedded with no exception. This is by design but under-documented — the README's "preserve raw replies" sentence does not state that *_raw methods skip MXAccess-failure detection entirely.

Recommendation: Document explicitly (README + docstring) that *_raw methods surface MXAccess HRESULT/status failures only inside the reply and do not raise MxAccessError, so parity-test callers know to inspect protocol_status/hresult/statuses themselves.

Resolution: (open)