From f2c1e5542ce73f32d66af72bdf0f2cbd5f5cd3b1 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Mon, 17 Aug 2026 12:12:09 +0400 Subject: [PATCH 1/2] docs: make the documented examples executable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit interrogate reported 100% docstring coverage while nothing checked whether any docstring was true: the examples were markdown ```python fences, which doctest never sees. rhiza's own test_docstrings.py said as much by skipping with "No doctests were found in any module". The fences are now >>> doctests. Those that can run, do — find_free_port, stop_server, get_server_status, and new examples on validate_input, DataManager and DataManager.get_chunk, which are pure and had no examples at all despite being the data contract. Those that cannot are marked # doctest: +SKIP with the reason stated in prose: plot() starts a server and opens a browser, run_server() blocks until the server stops. Their data-preparation lines still execute, so the setup stays honest even where the call is illustrative. This turned up one example that was already broken: plot()'s referenced an `sma` name that was never defined, so it would have raised NameError had anyone run it. It now passes a defined array. In the README, the stop_server and get_server_status examples were self-contained, so +RHIZA_SKIP is removed and they execute. The remaining four skips carry an HTML comment saying why — three reference series the reader supplies, and the `plot` block is an annotated signature that is not valid Python at a call site. make rhiza-test now runs 49 doctest assertions instead of skipping; check_doc_examples.py reports 56 examples across 9 objects, up from 0, and executes the README's Python fences with returncode 0. Closes #84 Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 8 +++- src/pycharting/__init__.py | 38 ++++++++------- src/pycharting/api/interface.py | 79 ++++++++++++++++++-------------- src/pycharting/core/server.py | 25 +++++----- src/pycharting/data/ingestion.py | 69 +++++++++++++++++++++++++++- 5 files changed, 149 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 5ac5b72..11950a2 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ When you run this script, PyCharting will: Once you have your OHLC series, you pass additional series to `plot` in two different ways: + ```python +RHIZA_SKIP overlays = { "SMA_50": sma(close, 50), # rendered on top of price @@ -104,6 +105,7 @@ plot( Each subplot value can be a plain array (line), a dict with options, or a list of dicts for multi-series panels: + ```python +RHIZA_SKIP subplots = { # Simple line (default) @@ -136,6 +138,7 @@ Supported series types: `"line"` (default), `"bar"`, `"scatter"`. Each entry acc You can overlay buy/sell arrows on the price chart by passing a `trades` array aligned with your index. Values: `1` (buy), `-1` (sell), `0` (no trade). + ```python +RHIZA_SKIP import numpy as np @@ -171,6 +174,7 @@ The public API is intentionally small and focused. All functions are available f ### `plot` + ```python +RHIZA_SKIP from typing import Dict, Any, Optional, Union @@ -216,7 +220,7 @@ The returned dict includes: ### `stop_server` -```python +RHIZA_SKIP +```python from pycharting import stop_server stop_server() @@ -226,7 +230,7 @@ Stops the active chart server if it is running. This is useful in long‑running ### `get_server_status` -```python +RHIZA_SKIP +```python from pycharting import get_server_status status = get_server_status() diff --git a/src/pycharting/__init__.py b/src/pycharting/__init__.py index f09fdc7..c666a8f 100644 --- a/src/pycharting/__init__.py +++ b/src/pycharting/__init__.py @@ -11,26 +11,24 @@ - **Easy to Use:** Simple Python API similar to matplotlib or plotly. Usage: - The main entry point is the `plot` function. - - ```python - from pycharting import plot, stop_server - import numpy as np - - # Prepare your data (numpy arrays or pandas Series) - index = np.arange(100) - open_data = np.random.rand(100) + 100 - high_data = open_data + 1 - low_data = open_data - 1 - close_data = open_data + 0.5 - - # Create and open the chart - plot(index, open_data, high_data, low_data, close_data) - - # ... keep the script running if needed ... - # input("Press Enter to stop...") - # stop_server() - ``` + The public surface is three functions, re-exported at the top level: + + >>> import pycharting + >>> sorted(pycharting.__all__) + ['__version__', 'get_server_status', 'plot', 'stop_server'] + + The main entry point is `plot`. It starts a local server and opens a + browser, so the example below is illustrative rather than executed here: + + >>> import numpy as np + >>> from pycharting import plot, stop_server + >>> index = np.arange(100) + >>> open_data = np.random.rand(100) + 100 + >>> high_data = open_data + 1 + >>> low_data = open_data - 1 + >>> close_data = open_data + 0.5 + >>> plot(index, open_data, high_data, low_data, close_data) # doctest: +SKIP + >>> stop_server() # doctest: +SKIP Exports: - `plot`: Main function to create and display charts. diff --git a/src/pycharting/api/interface.py b/src/pycharting/api/interface.py index 528c005..4b14922 100644 --- a/src/pycharting/api/interface.py +++ b/src/pycharting/api/interface.py @@ -137,29 +137,33 @@ def plot( - `server_running`: Boolean indicating if the server is active. Example: - ```python - import numpy as np - from pycharting import plot - - # 1. Prepare Data - n = 1000 - index = np.arange(n) - close = np.cumsum(np.random.randn(n)) + 100 - - # 2. Simple Line Chart - plot(index, close=close) - - # 3. Candlestick Chart - open_p = close + np.random.randn(n) * 0.5 - high = np.maximum(open_p, close) + np.abs(np.random.randn(n)) - low = np.minimum(open_p, close) - np.abs(np.random.randn(n)) - - plot( - index, open_p, high, low, close, - overlays={"SMA 20": sma}, - session_id="my-analysis" - ) - ``` + Every call below starts a local server and opens a browser, so they are + shown rather than executed. The data preparation is real, though: + + >>> import numpy as np + >>> from pycharting import plot + >>> n = 1000 + >>> index = np.arange(n) + >>> close = np.cumsum(np.random.randn(n)) + 100 + >>> len(close) + 1000 + + A single series renders as a line chart: + + >>> plot(index, close=close) # doctest: +SKIP + + Supplying open/high/low as well renders candlesticks: + + >>> open_p = close + np.random.randn(n) * 0.5 + >>> high = np.maximum(open_p, close) + np.abs(np.random.randn(n)) + >>> low = np.minimum(open_p, close) - np.abs(np.random.randn(n)) + >>> bool(np.all(high >= np.maximum(open_p, close))) + True + >>> plot( + ... index, open_p, high, low, close, + ... overlays={"SMA 20": close}, + ... session_id="my-analysis", + ... ) # doctest: +SKIP """ global _active_server @@ -323,12 +327,11 @@ def stop_server() -> None: If no server is running, this function does nothing and prints a message. Example: - ```python - from pycharting import stop_server + Calling it when nothing is running is safe and simply reports so: - # ... after done with analysis ... - stop_server() - ``` + >>> from pycharting import stop_server + >>> stop_server() + ⓘ No active server to stop """ global _active_server @@ -352,13 +355,21 @@ def get_server_status() -> ServerStatus: - `active_sessions`: Count of currently loaded datasets/sessions. Example: - ```python - from pycharting import get_server_status + With no server started, the reported status is inert: + + >>> from pycharting import get_server_status + >>> status = get_server_status() + >>> sorted(status) + ['active_sessions', 'running', 'server_info'] + >>> status["running"] + False + >>> status["server_info"] is None + True + + Once a server is up, ``server_info`` carries its host and port: - status = get_server_status() - if status['running']: - print(f"Server running at {status['server_info']['url']}") - ``` + >>> if status["running"]: # doctest: +SKIP + ... print(f"Server running at {status['server_info']['url']}") """ global _active_server diff --git a/src/pycharting/core/server.py b/src/pycharting/core/server.py index f096fbd..7c5e3ea 100644 --- a/src/pycharting/core/server.py +++ b/src/pycharting/core/server.py @@ -68,13 +68,17 @@ def find_free_port(start_port: int | None = None, end_port: int | None = None) - RuntimeError: If no free port can be found in the requested range. Example: - ```python - # Let the OS pick a guaranteed-free ephemeral port. - port = find_free_port() + Let the OS pick a guaranteed-free ephemeral port: + + >>> port = find_free_port() + >>> port > 0 + True + + Scan a preferred range instead: - # Scan a preferred range instead. - port = find_free_port(8000, 8010) - ``` + >>> port = find_free_port(8000, 8010) + >>> 8000 <= port < 8010 + True """ # No range requested: let the OS hand out a guaranteed-unique ephemeral port. if start_port is None: @@ -253,13 +257,10 @@ def run_server( None: This function blocks until the server stops. Example: - ```python - # Run server on localhost, finding a free port automatically - run_server() + These block until the server stops, so they are not executed here. - # Run on a specific port - run_server(port=8080) - ``` + >>> run_server() # doctest: +SKIP + >>> run_server(port=8080) # doctest: +SKIP """ # Determine port if port is None: diff --git a/src/pycharting/data/ingestion.py b/src/pycharting/data/ingestion.py index 18448ef..6ab08d0 100644 --- a/src/pycharting/data/ingestion.py +++ b/src/pycharting/data/ingestion.py @@ -54,6 +54,36 @@ def validate_input( Raises: DataValidationError: If any validation check fails. + + Example: + A single series is mapped to ``close`` and rendered as a line chart, + leaving the other price fields unset: + + >>> import numpy as np + >>> result = validate_input(np.arange(3), close=np.array([10.0, 11.0, 12.0])) + >>> result["close"].tolist() + [10.0, 11.0, 12.0] + >>> result["open"] is None + True + + With open and close supplied, high and low are filled in from them: + + >>> result = validate_input( + ... np.arange(3), + ... open=np.array([10.0, 11.0, 12.0]), + ... close=np.array([11.0, 10.0, 13.0]), + ... ) + >>> result["high"].tolist() + [11.0, 11.0, 13.0] + >>> result["low"].tolist() + [10.0, 10.0, 12.0] + + A series whose length disagrees with the index is rejected: + + >>> validate_input(np.arange(3), close=np.array([1.0, 2.0])) + Traceback (most recent call last): + ... + pycharting.data.ingestion.DataValidationError: Close length (2) does not match index length (3) """ # Convert index to numpy array if needed if isinstance(index, (pd.Index, pd.Series)): @@ -234,7 +264,19 @@ def to_array(data: pd.Series | np.ndarray | list | None, name: str) -> np.ndarra class DataManager: - """High-performance data container and manager.""" + """High-performance data container and manager. + + Validates and normalizes the supplied series once on construction, then + serves arbitrary slices of them cheaply — one slice per viewport move. + + Example: + >>> import numpy as np + >>> dm = DataManager(np.arange(5), close=np.arange(5, dtype=float) * 10) + >>> dm.length + 5 + >>> dm.close.tolist() + [0.0, 10.0, 20.0, 30.0, 40.0] + """ def __init__( self, @@ -332,7 +374,30 @@ def get_chunk( start_index: int | None = None, end_index: int | None = None, ) -> dict[str, Any]: - """Return a JSON-serializable slice of the data between ``start_index`` and ``end_index``.""" + """Return a JSON-serializable slice of the data between ``start_index`` and ``end_index``. + + Indices are clamped to the available range, so an over-wide request + returns what exists rather than raising. + + Example: + >>> import numpy as np + >>> dm = DataManager(np.arange(5), close=np.arange(5, dtype=float)) + >>> chunk = dm.get_chunk(1, 3) + >>> chunk["index"] + [1, 2] + >>> chunk["close"] + [1.0, 2.0] + + Omitting both bounds returns the whole series: + + >>> len(dm.get_chunk()["index"]) + 5 + + An end index past the last bar is clamped: + + >>> dm.get_chunk(3, 99)["close"] + [3.0, 4.0] + """ # Handle default values if start_index is None: start_index = 0 From 567e353d24d2cb6e4f1e0f3b6d09bb79dc46d409 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Mon, 17 Aug 2026 22:29:56 +0400 Subject: [PATCH 2/2] docs: document the output of the executable README examples The previous commit un-skipped the stop_server and get_server_status fences so they run, but left no ```result block. test_readme_runs concatenates every result block and compares it to the merged stdout of every executed python block, so it compared two printed lines against the empty string and failed in CI. Both lines are now documented where they belong: the "no active server" notice under stop_server, the status dict under get_server_status. Document order matches stdout order, so the concatenation lines up. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 11950a2..2783052 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,12 @@ from pycharting import stop_server stop_server() ``` +With no server running, it says so and does nothing: + +```result +ⓘ No active server to stop +``` + Stops the active chart server if it is running. This is useful in long‑running processes and demos to clean up after you are done exploring charts. ### `get_server_status` @@ -237,6 +243,12 @@ status = get_server_status() print(status) ``` +Before any chart has been plotted: + +```result +{'running': False, 'server_info': None, 'active_sessions': 0} +``` + Returns a small dict with: - `running`: whether the server is alive,