Repository navigation
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
87ed297 to
b837306
Compare
| - name: Install pypi deps | ||
| # orjson, a dev dependency used only by the examples, does not support | ||
| # free-threaded Python, so install the package and the test dependency | ||
| # (pook) at their locked versions instead of `poetry install` | ||
| run: | | ||
| poetry env use python${{ matrix.python-version }} | ||
| poetry install --only main | ||
| poetry run python - > "$RUNNER_TEMP/constraints.txt" <<'EOF' | ||
| import tomllib | ||
| with open("poetry.lock", "rb") as f: | ||
| for p in tomllib.load(f)["package"]: | ||
| print(f"{p['name']}=={p['version']}") | ||
| EOF | ||
| poetry run pip install pook -c "$RUNNER_TEMP/constraints.txt" |
There was a problem hiding this comment.
This looks like a major hack job. Why not do a proper fix? Is a proper fix difficult? This does not look like best practice.
There was a problem hiding this comment.
Agreed, it was a workaround. Replaced it with the proper fix in 2c4cf63 and af96494.
- Root cause:
orjson, a dev dependency used only by the examples, publishes no free-threaded wheels, and it sat in the same deprecateddev-dependenciestable as the test dependencies. The job could only avoid it by hand-building a constraints file and calling pip. 2c4cf63(build): moves[tool.poetry.dev-dependencies]into Poetry dependency groups,test(pook) anddev(black, mypy, stubs, Sphinx, orjson), with versions unchanged. A plainpoetry installstill installs both, and Poetry's deprecation warning is gone. Re-locking also merged a stale split for the dev-onlytyping-extensions(4.12.2 → 4.16.0 below 3.15); nothing else changed.af96494(ci): the free-threaded job now just runspoetry install --only main,test. The constraints script andpip installare deleted.pipx install poetrystays, because it is Poetry's recommended installation method and keeps Poetry on the runner's regular Python; the comment now says so.
All 16 checks pass on af96494. The PR description is updated, and F1 and F2 are rebased onto the new branch.
b837306 to
af96494
Compare
chipkent
left a comment
There was a problem hiding this comment.
Great PR direction overall — strong intent, good isolation of runtime code, and meaningful concurrency coverage.
My adversarial take: I don’t see a fundamental design flaw, but there are a few high-value edge cases and determinism risks worth tightening before upstreaming.
Adversarial review
1) Design correctness
- ✅ Scope discipline is good: CI + tests + tooling only, no runtime behavior changes.
- ✅ Free-threaded CI strategy is sound: explicitly running with
PYTHON_GIL=0and asserting GIL-off state is exactly what you want to prevent “false confidence” runs. - ✅ Dependency split (
main,testvsdev) is good architecture for CI portability and avoiding unnecessary deps (orjsonissue on free-threaded builds is a practical concern).
Adversarial concern: ensure this project’s contributor docs and Makefile assumptions still match the new Poetry grouping semantics. If make lint / docs targets implicitly relied on old dev-deps behavior, this can fail in fresh environments even if CI passes.
2) Code/test correctness risks to double-check
Given the PR shape (fake websocket server + threaded REST tests), the highest-risk failure modes are:
-
Flaky synchronization in multi-thread tests
- Look for sleeps/timeouts used as synchronization substitutes.
- Prefer explicit barriers/events/condition variables with bounded waits and clear failure messages.
-
Server teardown and port reuse races
- Fake server must always close sockets and join threads in
finallypaths. - Ensure tests cannot leak background threads between cases.
- Fake server must always close sockets and join threads in
-
“Passes locally, flaky in CI” timing assumptions
- 16 clients × reconnect/auth logic can produce scheduler-dependent ordering.
- Assertions should be order-insensitive unless protocol ordering is guaranteed.
-
Thread safety of captured request/message logs
- If fake server stores per-connection messages in shared dict/list, ensure writes are lock-protected (or per-thread isolated + merge).
-
REST header isolation checks
- Great test idea. Verify the test actually mutates per-request headers concurrently and checks both:
- response matches request-specific value
- client-level default headers remain unchanged afterward
- Great test idea. Verify the test actually mutates per-request headers concurrently and checks both:
3) Conventions / project fit
- ✅ Naming and test layout look consistent with a Python client repo (
test_rest,test_websocket, fake server helper). - ✅ CI expansion to 3.12–3.14 + 3.13t/3.14t is conventionally good.
⚠️ Ensure new tests are deterministic under Windows too (especially socket close semantics / timing), since your test plan includes Windows.
4) Merge readiness (adversarial verdict)
Status: Likely mergeable after a quick hardening pass.
Risk: Medium (not because of product/runtime changes, but because concurrency tests can silently be flaky and erode trust in CI).
Blocking issues found from provided context: none obvious.
Potential blockers to proactively eliminate before upstream PR:
- nondeterministic waits/sleeps in concurrency tests
- missing cleanup/join paths in fake server lifecycle
- shared mutable state in test infrastructure without locks
5) Suggested concrete improvements before opening upstream
- In
test_websocket/fake_server.py, ensure all shared collections tracking messages/connections are protected by a lock (or replaced with thread-safe queues) so test infrastructure itself doesn’t introduce races underPYTHON_GIL=0. - In
test_websocket/test_fake_server.py, replace timing-based waits (sleep-style sequencing) with explicit synchronization primitives (Event/Barrier) and boundedwait()assertions to reduce CI flakiness across Linux/Windows schedulers. - In
test_rest/test_concurrency.py, add a strict post-run assertion thatRESTClientdefault headers are byte-for-byte unchanged from pre-run state, so leakage detection is explicit and future regressions are caught immediately. - In free-threaded CI job setup, make failure logs include interpreter build flags / version details when GIL-off assertion fails, to speed root-cause analysis on runner image drift.
af96494 to
a165599
Compare
|
Addressed in
|
There was a problem hiding this comment.
🟡 Changes recommended
Closing or dropping a fake connection from a message hook deadlocks its handler and can hang server cleanup.
1 open issue
What changed in this PR
This PR adds free-threaded Python CI coverage and concurrency test infrastructure for the Massive Python client, without changing package runtime code.
Changes:
- Expands Python CI coverage and separates test dependencies from development tools.
- Adds a scriptable WebSocket server and tests for protocol behavior, reconnection, and parallel clients.
- Adds shared REST client concurrency coverage and isolates HTTP mocks between test classes.
| File | Description |
|---|---|
test_websocket/test_fake_server.py |
Tests fake-server behavior and parallel WebSocket clients. |
test_websocket/fake_server.py |
Adds a scriptable, per-instance WebSocket test server. |
test_rest/test_snapshots.py |
Re-registers mocks before a repeated request. |
test_rest/test_dividends.py |
Re-registers mocks before a repeated request. |
test_rest/test_concurrency.py |
Tests shared-client requests and header isolation across threads. |
test_rest/base.py |
Adds mock cleanup and reusable registration. |
pyproject.toml |
Splits test and development dependency groups. |
poetry.lock |
Updates dependency groups and consolidates typing-extensions. |
.github/workflows/test.yml |
Expands regular Python coverage and adds free-threaded test jobs. |
Review effort: Balanced
| async def close(self, code: int = 1000, reason: str = ""): | ||
| """Close with a close frame; 1000 is a clean close, e.g. 1011 is not.""" | ||
| await self.websocket.close(code, reason) | ||
| await self.closed.wait() |
There was a problem hiding this comment.
This PR no longer contains the fake WebSocket server. It is now scoped to free-threaded CI and the shared-RESTClient stress test (5fbf82d). The fake server moves to the WebSocket subscription PR, the first one that uses it, and the close/drop-from-hook deadlock is fixed there with a test.
BaseTest turned pook on and registered every mock in setUpClass but never turned it off. Mocks piled up across classes, and pook kept intercepting urllib3 after the REST mock tests had run. That breaks any later test that makes real HTTP requests: pook rejects them with PookNoMatches because it has no mock for them. The next commit adds such a test, which talks to a local HTTP server, and it fails in the full suite without this fix. The leak also hid a problem. Two tests repeat a request, and each mock answers only once, so their second request was served by a copy that an earlier class had registered. Both fail when run on their own. Turn pook off in tearDownClass. Let a test class name the mocks that must answer more than once; those are registered with persist. Other mocks still answer once, because the pagination tests depend on it: a persistent first-page mock would also match the request for the next page.
16 threads share one RESTClient and make 200 calls each against a local HTTP server that echoes every request back. The threads start together behind a barrier, and the server records how many requests it had in flight at once, so the test checks that they really overlapped. Every response is compared with the request that produced it: the path, the exact query string, the Authorization header, and the edge headers. Odd calls send per-request edge headers and even calls send none, so a header leaking from one thread into another call shows up as a mismatch. After the run, the client's default headers must be unchanged: same keys, same values, same order. The calls with headers go through RESTClient._get, which every endpoint method uses. Passing `options` to an endpoint method also adds it to the query string, and that would get in the way of checking the exact query.
Replace the deprecated [tool.poetry.dev-dependencies] section with a `test` group (pook, all the unit tests need) and a `dev` group (linters, type stubs, Sphinx, orjson for the examples). `poetry install` still installs both, so local and CI workflows are unchanged, while `poetry install --only main,test` installs just what `make test` needs. Versions are unchanged. Re-locking also merged a stale duplicate entry for typing-extensions (4.12.2 below Python 3.15, 4.16.0 from 3.15) into 4.16.0, a transitive dependency of the dev tools.
Add a job that runs `make test` on 3.13t and 3.14t with PYTHON_GIL=0, and extend the regular matrix to 3.12, 3.13 and 3.14. The free-threaded job installs only the main and test dependency groups: the dev group is not needed to run the tests, and orjson in it does not support free-threaded Python.
a165599 to
5fbf82d
Compare

Fixes #<issue: F7>
Summary
People use this client from multi-threaded programs. Free-threaded Python (3.13t and 3.14t) removes the GIL, and the GIL can hide race conditions. This PR runs the unit tests on free-threaded Python, adds a test that shares one
RESTClientbetween many threads, and stops pook mocks leaking between REST test classes.The package itself does not change. Only tests, CI and the dev-dependency layout do.
Changes
CI. A new
test-free-threadedjob runsmake teston 3.13t and 3.14t withPYTHON_GIL=0. It first checks that the GIL is really off, and fails if it is not. The regular matrix now also covers 3.12, 3.13 and 3.14.The job installs Poetry with
pipx, the installer Poetry recommends, which keeps Poetry on the runner's normal Python whatever Python is under test.Dependency groups. The deprecated
[tool.poetry.dev-dependencies]table is split into two Poetry dependency groups:test:pook, the only extra package the unit tests need;dev: black, mypy, thetypes-*stubs, Sphinx and its plugins, andorjson.The free-threaded job installs only
mainandtest, becauseorjson(3.13.0 in the lock) has no free-threaded wheel. A plainpoetry installstill installs everything, so local development, linting, docs and releases work as before, and Poetry stops printing its deprecation warning. No versions change; re-locking merged a stale duplicate entry fortyping-extensions, a dev-only dependency, into 4.16.0.Shared-client stress test (
test_rest/test_concurrency.py). 16 threads share oneRESTClientand make 200 calls each against a local HTTP server that echoes each request back. The test checks that the threads really ran at the same time (the server saw more than one request in flight), that every response matches the request that produced it (path, exact query string,Authorizationheader and per-request edge headers), that per-request headers never show up on another call (odd calls send edge headers, even calls send none), and that the client's default headers are unchanged afterwards.The calls that send headers use
RESTClient._get, which every endpoint method calls. Today, passingoptionsto an endpoint method also adds it to the query string (fixed in a separate PR), and that would get in the way of checking the exact query.Pook mocks no longer leak between test classes (
test_rest/base.py).BaseTestturned pook on insetUpClassbut never turned it off, so pook kept intercepting HTTP for every later test. The stress test above therefore failed in the full suite: pook rejected its requests to the local server.The leak also hid a problem in two tests,
test_list_stocks_dividends_ticker_any_ofandtest_get_snapshot_indices. Each makes the same request twice, and each mock answers once. Their second request was answered by a copy left over from an earlier class, so both failed when run on their own.BaseTestnow turns pook off intearDownClass. A test class can list mocks that must answer more than once (persistent_mocks), and those are registered with pook'spersist. All other mocks still answer once, because the pagination tests depend on it: a first-page mock would also match the request for the next page.Test plan
make testpasses on 3.10 to 3.14 (Ubuntu and Windows), and on 3.13t and 3.14t withPYTHON_GIL=0.make lintpasses on 3.10 and 3.14.test_dividendsandtest_snapshotspass when run on their own._concat_headers).Suggested order: land this before the WebSocket PRs, which rely on the free-threaded CI job.