Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/rules/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Apply these rules to ALL new and modified Python code. This project is a Python

### Types

- NEVER use type annotations in the src directory tree. Types of input parameters and returns should be indicated in the docstrings.
- NEVER use type annotations in the src directory tree, with one exception. Types of input parameters and returns should be indicated in the docstrings. The exception is a property, which may carry an inline return annotation so that the rendered documentation shows the type beside the property name; a property annotated this way keeps a one-line docstring rather than a `Returns:` block.
- Annotate all test function/method parameters and return values, including `-> None` for functions (and `__init__`) that return nothing.
- Use modern generic syntax (`list[str]`, `dict[str, int]`, `X | None`) for Python 3.11+.

Expand Down
18 changes: 14 additions & 4 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:

# Ruff is the linter of record for every rule it implements.
# TODO Add `ruff format --check src tests` once the source has been
# reformatted; mypy stays off while src is deliberately unannotated.
# reformatted.
- name: Ruff
run: |
ruff check src tests
Expand All @@ -42,15 +42,25 @@ jobs:
run: |
flake8 --select=E12,E13 src tests

# Every module under tests/ is checked. src/ is deliberately unannotated and
# carries no stub of its own, so mypy is kept off it by the excludes and the
# overrides in pyproject.toml; this step matches `--mypy` in
# scripts/run-all-checks.sh.
- name: Mypy
run: |
MYPYPATH=src mypy tests

- name: Pyroma
run: |
pyroma .

# The published .pyi stubs must keep describing the runtime API, which is assembled
# dynamically: most of Qube's methods are bound on at import time.
# The two published stubs, __init__.pyi and typedefs.pyi, must keep describing the
# runtime API, which is assembled dynamically: most of Qube's methods are bound on
# at import time. The allowlist accepts only the deliberate absence of stubs for
# the internal modules.
- name: Stubtest
run: |
python -m mypy.stubtest polymath --mypy-config-file pyproject.toml
python -m mypy.stubtest polymath --mypy-config-file pyproject.toml --allowlist .stubtest-allowlist

# -W makes warnings errors; docs/conf.py sets nitpicky = True, so a
# cross-reference with no target fails the build too.
Expand Down
21 changes: 21 additions & 0 deletions .stubtest-allowlist
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Allowlist for `mypy.stubtest`, read by scripts/run-all-checks.sh and CI.
#
# The only supported imports are "from polymath import ..." and
# "from polymath.typedefs import ...", so __init__.pyi and typedefs.pyi are the only
# stubs and describe the whole public API. The modules below are implementation
# detail and deliberately carry no stub; stubtest reports each one as "failed to
# find stubs", which this file accepts. Nothing else is accepted: a public name
# missing from the two stubs still fails the check.
polymath\.qube
polymath\.unit
polymath\.scalar
polymath\.boolean
polymath\.vector
polymath\.pair
polymath\.vector3
polymath\.quaternion
polymath\.polynomial
polymath\.matrix
polymath\.matrix3
polymath\.extensions
polymath\.extensions\.[a-z_]+
34 changes: 24 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,30 @@ required to run exactly that set. Run it after any change.
concatenation) and `I001` (its fix collapses the column-aligned imports used throughout).
Read the comment before re-enabling one.
- Single quotes (`[tool.ruff.format] quote-style = "single"`).
- **Never use type annotations anywhere under `src/`** — parameter and return types belong in the
docstrings. **Annotate all test functions and methods**, including `-> None`.
- The package ships a PEP 561 `py.typed` marker, so public type information goes in `.pyi` stubs
alongside the modules. A stub replaces its module entirely for type checkers: whatever the stub
omits becomes invisible downstream, so a new stub must cover the module's whole public surface.
`stubtest` enforces exactly that and runs in the check script and in CI, so adding, renaming or
re-signing any public member means updating its stub in the same change. Most of `Qube`'s methods
are bound on at import time from `extensions/`, and they all have to appear in `qube.pyi`.
Signature shapes in the stubs are exact; types come from the docstrings where those state one
and are `Any` where they do not, which is deliberate rather than an omission to fill in blindly.
- **No type annotations under `src/`, with one exception** — parameter and return types belong in
the docstrings. The exception is a **property**, which may carry an inline return annotation:
a property has no parameters, and Sphinx renders the annotation as the property's type beside
its name, so `Qube.shape` reads as `property shape: tuple[int, ...]`. A property documented only
through a `Returns:` block renders the type in a separate trailing line instead, so the two
styles do not mix; annotate the property and leave its docstring a one-line summary. Where the
annotation names something from `polymath.typedefs`, quote it and import it under
`if TYPE_CHECKING:` — that module imports `Qube`, so a runtime import from `qube.py` is
circular. **Annotate all test functions and methods**, including `-> None`.
- The package ships a PEP 561 `py.typed` marker, and **exactly two stubs** carry the public type
information: `__init__.pyi`, which declares every public class in full, and `typedefs.pyi`. The
only supported imports are `from polymath import ...` and `from polymath.typedefs import ...`, so
no other module has a stub and none may be added: a per-module stub would make an import such as
`from polymath.scalar import ...` look supported. A stub replaces its module entirely for type
checkers: whatever the stub omits becomes invisible downstream, so the two stubs must cover the
whole public surface. `stubtest` enforces exactly that and runs in the check script and in CI, so
adding, renaming or re-signing any public member means updating `__init__.pyi` in the same change.
Most of `Qube`'s methods are bound on at import time from `extensions/`, and they all have to
appear under `Qube` in `__init__.pyi`. Signature shapes in the stubs are exact; types come from
the docstrings where those state one, from the inline annotation for a property that has one, and
are `Any` where neither does, which is deliberate rather than an omission to fill in blindly.
stubtest would otherwise compare each stub-less module against itself, so `[tool.mypy] exclude`
and the override list in `pyproject.toml`, and `.stubtest-allowlist`, each name those modules
explicitly; a new module goes in all three.
- `qube.py` holds only what defines an object: the class constants, `__init__`, the construction
path, low-level access, the properties and the cache. Everything else lives in `extensions/` and
is bound onto `Qube` by `extensions/__init__.py`. Two rules keep that working. First,
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ from polymath import (Boolean, Matrix, Matrix3, Pair, Quaternion, Qube, Scalar,
Vector, Vector3)
```

# Type Annotations

PolyMath ships type stubs and a `py.typed` marker, so a type checker such as mypy
understands the signatures of every class. The
`polymath.typedefs`[![image](https://raw.githubusercontent.com/SETI/rms-polymath/main/icons/link.png)](https://rms-polymath.readthedocs.io/en/latest/module.html#module-polymath.typedefs)
module supplements the stubs with aliases naming what each constructor accepts, for use in
your own annotations:

```python
from polymath import Scalar, Vector3
from polymath.typedefs import Vector3Like

def speed(velocity: Vector3Like) -> Scalar:
return Vector3.as_vector3(velocity).norm()
```

`ScalarLike` accepts anything that `Scalar` converts: a number, a nested sequence, a NumPy
array, or any PolyMath object. `BooleanLike`, `PairLike`, `VectorLike`, `Vector3Like`,
`MatrixLike`, `Matrix3Like`, `QuaternionLike`, and `QubeLike` do the same for the other
classes, and `ValsType` and `MaskType` name what the `values` and `mask` properties return.
Each alias is an ordinary runtime object, so it can be imported and used anywhere. See the
[User Guide](https://rms-polymath.readthedocs.io/en/latest/user_guide/user_guide_typing.html)
for details.

# Features

The PolyMath classes are:
Expand Down Expand Up @@ -779,6 +803,8 @@ Information on contributing to this package can be found in the
# Links

* [Documentation](https://rms-polymath.readthedocs.io)
* [User Guide](https://rms-polymath.readthedocs.io/en/latest/user_guide/user_guide.html)
* [Developer Guide](https://rms-polymath.readthedocs.io/en/latest/dev_guide/dev_guide.html)
* [Repository](https://github.com/SETI/rms-polymath)
* [Issue tracker](https://github.com/SETI/rms-polymath/issues)
* [PyPi](https://pypi.org/project/rms-polymath)
Expand Down
10 changes: 9 additions & 1 deletion docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@

# You can set these variables from the command line, and also
# from the environment for the first two.
#
# SPHINXBUILD points into the project virtualenv rather than relying on PATH, because a
# sphinx-build found on PATH is often an older installation. The docs need Sphinx 9 (see
# the docs extra in pyproject.toml); on earlier versions every polymath.typedefs alias
# reference goes unresolved and the -W build fails. Override VENV (as
# scripts/run-all-checks.sh does) to build against another environment.
MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
VENV ?= $(MAKEFILE_DIR)../venv
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SPHINXBUILD ?= $(VENV)/bin/sphinx-build
SOURCEDIR = .
BUILDDIR = _build

Expand Down
22 changes: 12 additions & 10 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@
# The suffix(es) of source filenames.
source_suffix = ['.rst', '.md']

# The docstrings wrap variable names in single backticks. Napoleon renders the name of
# each entry in a `Parameters:` block in bold, so the default role must be `strong` for a
# mention of that same name in the surrounding prose to match it. Double backticks mark
# code expressions, and italics mark math symbols that are not variable names, such as
# *x*-axis. An API symbol that should link to its own entry carries an explicit role
# instead.
default_role = 'strong'

# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.
Expand All @@ -79,7 +87,7 @@
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = False
napoleon_use_ivar = True
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_preprocess_types = False
Expand Down Expand Up @@ -107,15 +115,9 @@
# Napoleon splits a type such as "(bool, optional)" on the comma and looks up each
# piece, so the trailing "optional" of every optional parameter arrives here.
(r'py:class', r'optional'),
# Anything NumPy can turn into an array: a nested sequence, a scalar, an ndarray or
# another PolyMath object. There is no single class that expresses it.
(r'py:class', r'array-like'),
# A single number, as opposed to an array of them.
(r'py:class', r'scalar'),
# Anything convertible to a Vector, in the same sense as "array-like".
(r'py:class', r'vector-like'),
# Anything the surrounding class can convert into itself.
(r'py:class', r'convertible'),
# The sentinel a binary operator returns to defer to the other operand. It is a
# builtin constant rather than a class, so a py:class lookup cannot match it.
(r'py:class', r'NotImplemented'),
]

# MyST-Parser settings
Expand Down
21 changes: 21 additions & 0 deletions docs/dev_guide/dev_guide.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
===============
Developer Guide
===============

This guide is for people who modify, extend, test, or release PolyMath. It explains how
the code is organized, how the pieces cooperate, and how to add a feature or a subclass
without breaking the contracts the rest of the package relies on.

.. toctree::
:maxdepth: 3

dev_guide_introduction
dev_guide_layout
dev_guide_environment
dev_guide_architecture
dev_guide_extensions
dev_guide_subclasses
dev_guide_typing
dev_guide_extending
dev_guide_conventions
dev_guide_internal_api
Loading