From f6eee30f17b5b304d8fd5328a690fe03a846b8c9 Mon Sep 17 00:00:00 2001 From: Dominik Krenn Date: Wed, 5 Nov 2025 14:27:58 +0100 Subject: [PATCH] added magic methods --- mathstream/README.md | 9 ++++++++ mathstream/number.py | 54 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/mathstream/README.md b/mathstream/README.md index 2d955ab..02510ef 100644 --- a/mathstream/README.md +++ b/mathstream/README.md @@ -43,6 +43,12 @@ b = StreamNumber(literal="1337") result = mul(a, b) print("".join(result.stream())) + +# same helpers are available via Python operators +total = a + b # calls mathstream.add under the hood +ratio = total / 2 # literal coercion is automatic +with StreamNumber(literal="10") as temp: + product = temp * ratio ``` Available operations: @@ -50,6 +56,8 @@ Available operations: - Introspection & helpers: `is_even`, `is_odd`, `free_stream`, `active_streams`, `tracked_files` - Lifecycle control: `collect_garbage`, `set_manual_free_only`, `manual_free_only_enabled` - Environment helpers: `clear_logs`, `StreamNumber.write_stage`, `engine.LOG_DIR` +- Python sugar: `StreamNumber` implements `+`, `-`, `*`, `/`, `%`, `**`, and their reflected counterparts. Raw `int`, `str`, or `pathlib.Path` operands are coerced automatically. +- Context manager support: `with StreamNumber(...) as sn:` ensures `.free()` is called at exit. ## How It Works @@ -73,6 +81,7 @@ Available operations: ### Common Pitfalls & Recoveries - **Accidentally freed files** – Automatic finalizers may delete staged outputs while you still hold the path elsewhere. Fix: call `set_manual_free_only(True)` at the start of long-lived workflows, or pass `delete_file=False` to `free_stream` when you need to keep the digits around manually. +- **Operator coercion surprises** – Arithmetic operators turn `int`, `str`, or `Path` operands into streamed numbers. If a string happens to be a *file path* instead of a literal, the actual file will be wrapped. Fix: be explicit (`StreamNumber(literal="...")`) when in doubt. - **Literal churn** – Recreating the same `StreamNumber(literal="123")` millions of times hammers the filesystem. Fix: stash the first instance, or cache the `.path` and rely on `StreamNumber(existing_path)` in hot loops. - **GC too aggressive** – Running `collect_garbage(0)` after every operation removes recently written files. Fix: raise the threshold (e.g., `collect_garbage(1000)`) or run GC only after you’ve freed all references. - **Chunk mismatch** – Some editors save files with BOMs or commas. `_normalize_stream` will raise `ValueError("Non-digit characters found...")`. Fix: sanitise input files (only ASCII digits with optional leading sign). diff --git a/mathstream/number.py b/mathstream/number.py index 96a299a..dba5bab 100644 --- a/mathstream/number.py +++ b/mathstream/number.py @@ -2,7 +2,8 @@ import hashlib import weakref from collections import Counter from pathlib import Path -from typing import Dict, Optional, Union +from typing import Dict, Optional, Union, Any +from .engine import add, sub, mul, div, mod, pow from .utils import ( register_log_file, @@ -117,6 +118,41 @@ class StreamNumber: def __exit__(self, exc_type, exc, tb): self.free() + def __add__(self, other): + return add(self, _coerce_operand(other)) + + def __sub__(self, other): + return sub(self, _coerce_operand(other)) + + def __mul__(self, other): + return mul(self, _coerce_operand(other)) + + def __truediv__(self, other): + return div(self, _coerce_operand(other)) + + def __mod__(self, other): + return mod(self, _coerce_operand(other)) + + def __pow__(self, other): + return pow(self, _coerce_operand(other)) + + def __radd__(self, other): + return add(_coerce_operand(other), self) + + def __rsub__(self, other): + return sub(_coerce_operand(other), self) + + def __rmul__(self, other): + return mul(_coerce_operand(other), self) + + def __rtruediv__(self, other): + return div(_coerce_operand(other), self) + + def __rmod__(self, other): + return mod(_coerce_operand(other), self) + + def __rpow__(self, other): + return pow(_coerce_operand(other), self) _ACTIVE_COUNTER: Counter[str] = Counter() @@ -163,3 +199,19 @@ def set_manual_free_only(enabled: bool) -> None: def manual_free_only_enabled() -> bool: """Return the current manual-free-only toggle.""" return _MANUAL_FREE_ONLY + + +def _coerce_operand(value: Any) -> StreamNumber: + """Convert supported operand types into a StreamNumber.""" + if isinstance(value, StreamNumber): + return value + if isinstance(value, (int,)): + return StreamNumber(literal=str(value)) + if isinstance(value, Path): + return StreamNumber(value) + if isinstance(value, str): + candidate = Path(value) + if candidate.exists(): + return StreamNumber(candidate) + return StreamNumber(literal=value) + raise TypeError(f"Unsupported operand type for StreamNumber: {type(value)!r}")