Skip to content
Merged
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
23 changes: 14 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ jobs:
with:
python-version: "3.12"

- name: Install build tooling
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Set up uv
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: false

- name: Build sdist and wheel
run: python -m build
run: uv build

- name: Validate metadata and README rendering
run: twine check --strict dist/*
run: uvx twine check --strict dist/*

- name: Verify tag matches declared version
if: github.event_name == 'release'
Expand Down Expand Up @@ -137,6 +137,11 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Set up uv
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: false

- name: Download distributions
uses: actions/download-artifact@v4
with:
Expand All @@ -145,9 +150,8 @@ jobs:

- name: Install the wheel
run: |
python -m pip install --upgrade pip
pip install dist/*.whl
pip check
uv pip install --system dist/*.whl
uv pip check --system

- name: Stage tests outside the source tree
run: |
Expand Down Expand Up @@ -230,6 +234,7 @@ jobs:
publish-pypi:
name: Publish to PyPI
needs: [publish-testpypi]
if: github.event_name == 'release'
runs-on: ubuntu-latest
timeout-minutes: 10

Expand Down
69 changes: 9 additions & 60 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,74 +55,23 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

# Two small wheels download faster than a cache restores.
- name: Set up uv
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: false

- name: Show environment
run: |
python --version
pip --version
uv --version

- name: Install package
run: |
python -m pip install --upgrade pip
pip install -e .
run: uv pip install --system -e .

- name: Show resolved dependencies
run: pip list

- name: Verify package imports
run: python -c "import aether as ae; print('aether from', ae.__file__)"

- name: Verify lazy namespace resolves
run: |
python - <<'PY'
import aether as ae

names = [
"Model",
"Conv2d", "Dense", "BatchNorm",
"MaxPool2d", "AvgPool2d", "GlobalAvgPool",
"Dropout", "SpatialDropout",
"ReLU", "LeakyReLU",
"AdamW",
"SoftmaxCategoricalCrossEntropy",
"CategoricalAccuracy", "RegressionAccuracy",
"Compose", "Rescale", "StandardScaler", "ToTensor",
"to_tensor",
]

missing = [n for n in names if not hasattr(ae, n)]
if missing:
raise SystemExit(f"lazy namespace failed to resolve: {missing}")
print(f"resolved {len(names)} public symbols")
PY

- name: Verify required dependencies are declared
run: |
python - <<'PY'
import importlib.util

for pkg in ("numpy", "safetensors"):
if importlib.util.find_spec(pkg) is None:
raise SystemExit(
f"{pkg} not installed - it is used by the test suite "
f"but missing from pyproject.toml dependencies"
)
print("numpy and safetensors present")
PY

- name: Confirm runner has no GPU backend
run: |
python - <<'PY'
import importlib.util

if importlib.util.find_spec("cupy") is not None:
raise SystemExit(
"CuPy is installed on a CPU-only runner - this workflow "
"would report coverage it cannot actually verify"
)
print("CuPy absent - GPU tests will be skipped")
PY
run: uv pip list --system

- name: Run test suite
run: python -m unittest discover -s tests -v
10 changes: 9 additions & 1 deletion aether/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ from aether.losses.categorical_crossentropy import (
)

# Direct module imports resolve stub lookups instantly:
from aether.metrics.accuracy import Accuracy as Accuracy
from aether.metrics.accuracy import (
CategoricalAccuracy as CategoricalAccuracy,
)
Expand All @@ -35,10 +36,13 @@ from aether.metrics.accuracy import (
from aether.model import Model as Model
from aether.optimizers.adam import Adam as Adam
from aether.optimizers.adam import AdamW as AdamW
from aether.optimizers.adam import Optimizer as Optimizer
from aether.preprocessing.transforms import Compose as Compose
from aether.preprocessing.transforms import Preprocess as Preprocess
from aether.preprocessing.transforms import Rescale as Rescale
from aether.preprocessing.transforms import StandardScaler as StandardScaler
from aether.preprocessing.transforms import ToTensor as ToTensor
from aether.preprocessing.transforms import deserialize as deserialize
from aether.preprocessing.transforms import to_tensor as to_tensor

__all__ = [
Expand All @@ -62,15 +66,19 @@ __all__ = [
"CategoricalCrossEntropy",
"SoftmaxCategoricalCrossEntropy",
# metrics
"Accuracy",
"CategoricalAccuracy",
"RegressionAccuracy",
# optimizers
"Optimizer",
"Adam",
"AdamW",
# preprocessing
"to_tensor",
"Preprocess",
"ToTensor",
"StandardScaler",
"Rescale",
"Compose"
"Compose",
"deserialize",
]
52 changes: 52 additions & 0 deletions tests/config/test_lazy_namespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import ast
import importlib
import pathlib

import aether
import tests.base_case as base_case

AETHER_ROOT = pathlib.Path(aether.__file__).parent


def _stub_exports(stub_path):
"""(re-exported names, __all__) declared by a .pyi stub."""
tree = ast.parse(stub_path.read_text())
reexported = {
alias.asname
for node in ast.walk(tree) if isinstance(node, ast.ImportFrom)
for alias in node.names if alias.asname == alias.name
}
declared_all = next(
set(ast.literal_eval(node.value))
for node in tree.body
if isinstance(node, ast.Assign)
and any(getattr(target, "id", None) == "__all__" for target in node.targets)
)
return reexported, declared_all


class TestLazyNamespace(base_case.AetherBaseTestCase):
"""Every package with an __init__.pyi: the lazy map and the stub list the same
public names, and each one resolves."""

def test_lazy_map_matches_stub_and_resolves(self):
stubs = sorted(AETHER_ROOT.rglob("__init__.pyi"))
self.assertTrue(stubs, "no __init__.pyi stubs found")

for stub in stubs:
package = ".".join(("aether", *stub.parent.relative_to(AETHER_ROOT).parts))
with self.subTest(package=package):
module = importlib.import_module(package)
runtime_all = set(module.__all__)
reexported, declared_all = _stub_exports(stub)

self.assertEqual(reexported, declared_all,
f"{stub.name}: imports and __all__ disagree")
self.assertEqual(runtime_all, declared_all,
"lazy map and stub list different public names")
unresolved = [name for name in sorted(runtime_all)
if not hasattr(module, name)]
self.assertEqual(unresolved, [], "lazy names that fail to resolve")


base_case.register_test_suites(globals(), template_cls=TestLazyNamespace)
Loading