Architecture¶
Goals¶
rangeslib provides a small, typed, Pythonic range pipeline API inspired by
modern C++ ranges without attempting to reproduce C++ iterator and view
semantics exactly. The current design prioritizes:
a small public facade,
predictable eager results,
compatibility with ordinary Python iterables,
readable pipeline syntax,
type information that survives common public operations,
simple extension rules for maintainers.
Laziness, iterator/sentinel pairs, borrowed-range semantics, and C++
customization point objects are explicitly outside the current architecture.
views.all fills the role closest to C++ views::all by eagerly materializing
an existing Python iterable into a reusable Range.
Package layout¶
src/rangeslib/
├── __init__.py Public package surface: Range, ranges, views
├── ranges.py Lowercase generator facade
├── views.py Lowercase adaptor facade and public typing protocols
├── _core.py Range, RangeAdaptor, and RangeGenerator
├── _generators.py Private source implementations
├── _adaptors.py Private transformation implementations
└── py.typed PEP 561 marker for downstream type checkers
The src layout prevents accidental imports from the repository root. Modules
beginning with _ are implementation details. Application code should depend
only on Range, ranges, and views.
Dependency direction¶
__init__.py
├── ranges.py ───────> _generators.py ──┐
└── views.py ────────> _adaptors.py ────┼──> _core.py
└──> Range / base contracts
Private implementation modules do not depend on the public facade modules. That direction keeps the facade replaceable and avoids circular imports.
Public API boundary¶
The package root intentionally exports only:
from rangeslib import Range, ranges, views
ranges exposes source factories and views exposes adaptor factories. The
private implementation classes are tested directly where useful, but they are
not part of the compatibility contract. Adding a new public source or adaptor
means adding it to the appropriate lowercase facade and that facade’s
__all__, not exporting the implementation class from rangeslib.__init__.
Data flow and pipeline dispatch¶
The conceptual data flow is:
range source -> Range[T]
Iterable[T] -> adaptor -> Range[U]
Range[T] | adaptor -> Range[U]
ordinary Iterable[T] | adaptor -> Range[U]
Iterable[T] -> views.to(factory) -> factory-defined result
RangeAdaptor.__ror__ enables built-in iterables such as list, str, and
built-in range to start a pipeline. Range.__or__ invokes the callable on
the right side and preserves the result type supplied by that callable.
For example:
from rangeslib import ranges, views
result = ranges.iota(1, 5) | views.transform(str) | views.to(tuple)
The dispatch sequence is:
ranges.iotaconstructsRange[int].Range.__or__calls the transform adaptor.TransformmaterializesRange[str].Range.__or__callsTo.Todelegates totupleand returnstuple[str, ...].
Range invariants¶
Range[T] is a mutable collections.UserList subclass with positional
construction:
Range(1, 2, 3) # three integer elements
Range([1, 2, 3]) # one list element
The positional constructor is an intentional API choice. Because UserList
reconstructs subclasses internally for several operations, Range explicitly
implements those reconstruction operations so the positional constructor does
not turn results into nested single elements.
The following operations preserve Range and element order:
slicing, including stepped and reverse slices,
left and right concatenation with compatible iterables,
multiplication and reverse multiplication,
shallow copying.
Range.copy() and copy.copy(range_value) copy the outer container but do not
deep-copy contained objects.
Eager execution and iterator consumption¶
Most adaptors return a fully materialized Range. views.to is the exception:
it delegates result construction and consumption behavior to the supplied
callable.
Eager output does not require every adaptor to consume its input in the same way. The important contracts are:
Category |
Examples |
One-shot iterator behavior |
|---|---|---|
Bounded prefix |
positive |
consumes only the selected prefix |
Early stopping |
|
stops at first failing value; that failing value has been consumed |
Full materialization |
|
consumes the finite input before returning |
Shortest-input combination |
|
stops when one participating iterable ends |
Target-defined |
|
depends on the target callable |
Negative take deliberately follows Python slice-stop semantics, equivalent to
list(iterable)[:count], so the complete input must be known first. drop
likewise materializes the full input because its semantics are implemented as a
Python slice.
Infinite iterables¶
The library is not a lazy ranges framework. Only operations that can terminate
without exhausting their input are appropriate for infinite iterables. For
example, positive take and counted can bound itertools.count(), while
reverse, filter, transform, drop, stride, and grouping operations do
not return when applied directly to an unbounded iterable.
Pipeline order therefore matters:
import itertools
from rangeslib import views
finite = itertools.count() | views.take(5) | views.transform(str)
The bounded operation must appear before an adaptor that requires complete materialization.
Validation and edge-case policy¶
Validation is adaptor-specific rather than governed by a blanket “all negative sizes are invalid” rule.
The public contract is:
counted(count)requirescount >= 0.adjacent(width),adjacent_transform(..., width),chunk(size),slide(width), andstride(step)require a positive integer.split(separator)rejects an empty separator when the adaptor is applied.take(count)anddrop(count)intentionally accept negative values and use Python slicing semantics.repeat(value, count)andindices(count)follow built-in list/range behavior, so non-positive counts produce empty results.join_with([])is valid and is equivalent to flattening with no inserted values.
These behaviors are public compatibility contracts and must be covered by public-facade tests.
Typing architecture¶
The distribution contains py.typed, so type checkers may consume inline
annotations under PEP 561. The code uses Python 3.12 type-parameter syntax.
Factories whose input type is inferable from an argument, such as filter,
transform, chunk_by, join_with, and split, return their concrete typed
adaptor classes. Factories such as reverse, take, counted, enumerate,
adjacent, chunk, and join do not receive the pipeline element type as a
factory argument. Their public signatures therefore use private structural
protocols with generic call methods so the input type can be inferred when the
adaptor is actually called or piped.
Some projection and variadic callable operations necessarily use Any where a
single Python signature cannot express dynamic tuple indexing or arbitrary
callable arity precisely. Those uses should stay localized to the facade and
implementation that require them.
Static type checking is part of CI. tests/typecheck/public_api.py contains
representative assert_type checks for the public API.
Error philosophy¶
The library raises ordinary Python exceptions rather than wrapping them in a library-specific hierarchy. Configuration that is unambiguously invalid is validated by the relevant adaptor. Errors produced by user callables, indexing, target factories, or incompatible values propagate naturally.
A new adaptor should not silently normalize invalid configuration unless that normalization directly mirrors a well-known Python operation and is documented as part of the public contract.
Extending the library¶
A new source should:
be implemented in
_generators.py,return
Rangedirectly,be exposed as a lowercase factory in
ranges.py,have public-facade behavior tests,
document validation and edge cases.
A new adaptor should:
subclass
RangeAdaptorin_adaptors.py,document input shape, output shape, materialization, and validation,
be exposed through a lowercase factory in
views.py,preserve useful public typing through a concrete generic return type or a structural protocol,
have both implementation tests where useful and public-facade contract tests,
be added to the usage/API documentation.
Do not expand the package-root namespace simply to expose a new implementation class.
Compatibility policy¶
During the 0.x series, public API changes may still occur. Nevertheless:
changes to
Range,ranges, orviewsshould be documented in the changelog,breaking public changes should include migration guidance,
private modules beginning with
_may change without migration support,observable iterator-consumption behavior should be treated as part of the public contract when users can reasonably depend on it.
Testing strategy¶
The test suite has three complementary layers:
implementation tests for private generator/adaptor mechanics,
public-facade tests that exercise the API applications actually import,
collection-contract tests for
Rangebehavior inherited fromUserList.
Coverage is enforced as a guardrail, not as a substitute for behavioral contracts. CI also runs static typing, linting, documentation builds, package builds, and an installed-wheel smoke test.
Developer automation lives in scripts/. scripts/format.sh is intentionally
a local-only mutating command, while scripts/check_all.sh,
scripts/run_tests.sh, scripts/check_package.sh, and
scripts/generate_docs.sh are invoked directly by GitHub Actions. Keeping
executable policy in repository scripts prevents local and CI commands from
drifting. Documentation deployment is triggered only after the complete CI
workflow succeeds on main, and it builds the exact commit SHA that passed CI.
C++ correspondence¶
Names and broad behavior are inspired by C++20, C++23, and C++26 ranges where useful, but Python conventions take priority:
Python iterables replace iterator/sentinel pairs.
Rangeis an eager list-backed result rather than a general-purpose lazy view.tuples replace C++ reference tuples.
normal Python callables replace customization point objects.
validation follows each Python-facing operation rather than attempting to duplicate C++ concepts or constraints.