diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 214377d..7724343 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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' @@ -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: @@ -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: | @@ -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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 43872fb..4d914fd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/aether/__init__.pyi b/aether/__init__.pyi index ac4b789..d04e571 100644 --- a/aether/__init__.pyi +++ b/aether/__init__.pyi @@ -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, ) @@ -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__ = [ @@ -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", ] \ No newline at end of file diff --git a/tests/config/test_lazy_namespace.py b/tests/config/test_lazy_namespace.py new file mode 100644 index 0000000..adf938f --- /dev/null +++ b/tests/config/test_lazy_namespace.py @@ -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)