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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,13 @@ archived by series under [docs/changelog/](docs/changelog/); see the
("missing library stubs or py.typed marker") and every value it returned
was `Any` in the application, although nearly every public function is
annotated. The wheel now carries the marker, and the wheel build fails
if it goes missing.
if it goes missing. The root package also names its public surface in
`__all__`: under PEP 561 a type checker treats a bare `from .x import Y`
in `__init__.py` as private, so with the marker alone pyright (and so
Pylance) refused the README's own `from offline_protocol_sdk import
ProtocolManager`, and `mypy --strict` reported that the module "does not
explicitly export" it. A test pins the list to the attributes the package
exposes, and the wheel harness refuses a wheel without the marker.
- **Python: a Linux box that could no longer advertise recovers on its own.**
Linux loses track of the controller's advertising when a central-role
connection completes while advertising is on: the controller keeps
Expand Down
28 changes: 27 additions & 1 deletion bindings/python/offline_protocol_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from ._callback_reentrancy import make_handle_maps_reentrant as _make_reentrant

_make_reentrant(_generated)
del _generated, _make_reentrant

# Platform managers
from .secure_storage import SecureStorage # noqa: F401
Expand All @@ -20,3 +19,30 @@
from .ble_manager import BleManager # noqa: F401
from .ble_peripheral import BlePeripheral # noqa: F401
from .protocol_manager import ProtocolManager # noqa: F401

# The public surface, by name. The package ships a PEP 561 marker, and under
# it a type checker treats a bare `from .x import Y` above as private: pyright
# (and so Pylance) refuses `from offline_protocol_sdk import ProtocolManager`,
# the README's own quick start, and `mypy --strict` reports that the module
# "does not explicitly export" it. Only the `+=` forms below are ones every
# checker is required to read, so no unpacking or `list()` here.
# tests/test_public_surface.py pins this list to the attributes the package
# exposes, so a manager added above without a line here fails in CI rather
# than in a user's editor.
__all__ = []
__all__ += _generated.__all__
__all__ += [
"SecureStorage",
"AppStateStorage",
"account_storage_namespace",
"TransportManager",
"TransportState",
"InternetManager",
"PeerEntry",
"PeerStreamManager",
"BleManager",
"BlePeripheral",
"ProtocolManager",
]

del _generated, _make_reentrant
68 changes: 68 additions & 0 deletions bindings/python/tests/test_public_surface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Pins the root package's ``__all__`` to the attributes it exposes.

The package ships a PEP 561 marker, and under it a type checker treats a
bare ``from .x import Y`` in ``__init__.py`` as private: pyright (and so
Pylance) refuses ``from offline_protocol_sdk import ProtocolManager``, the
README's own quick start, and ``mypy --strict`` reports that the module
"does not explicitly export" it. Only a name in ``__all__`` is public. A
manager added to the root without a line in ``__all__`` imports fine at
runtime and fails only in a user's editor, so the list is checked here
against what the package really exposes.
"""

from __future__ import annotations

import inspect

import offline_protocol_sdk as sdk

# Written out, not derived: the names the README and the example import from
# the root, which the generated module's own ``__all__`` does not cover.
HAND_WRITTEN_EXPORTS = (
"SecureStorage",
"AppStateStorage",
"account_storage_namespace",
"TransportManager",
"TransportState",
"InternetManager",
"PeerEntry",
"PeerStreamManager",
"BleManager",
"BlePeripheral",
"ProtocolManager",
)


def _public_attributes() -> set[str]:
# The imports in __init__.py also bind each submodule as an attribute.
# Those stay reachable as ``offline_protocol_sdk.secure_storage`` whatever
# ``__all__`` says, and are not part of the star-import surface.
return {
name
for name, value in vars(sdk).items()
if not name.startswith("_") and not inspect.ismodule(value)
}


def test_all_names_every_public_attribute_and_nothing_else() -> None:
listed = set(sdk.__all__)
exposed = _public_attributes()
assert listed == exposed, (
f"missing from __all__: {sorted(exposed - listed)}; "
f"listed but not exposed: {sorted(listed - exposed)}"
)


def test_all_has_no_duplicates() -> None:
assert len(sdk.__all__) == len(set(sdk.__all__))


def test_the_hand_written_managers_are_listed() -> None:
for name in HAND_WRITTEN_EXPORTS:
assert name in sdk.__all__, name


def test_the_generated_surface_is_listed() -> None:
from offline_protocol_sdk import offline_protocol as generated

assert set(generated.__all__) <= set(sdk.__all__)
13 changes: 13 additions & 0 deletions scripts/tests/test-build-wheel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,19 @@ if [ "$HAVE_ZLIB" = 1 ]; then
else
echo " skip - no zlib headers to build the refusal fixture"
fi
# The PEP 561 marker: setuptools wheels a package without it happily, so the
# required-files check at the end of the script is the only thing that
# notices, and this is the only place that proves it does.
copy_package "$WORK/nomarker"
rm "$WORK/nomarker/bindings/python/offline_protocol_sdk/py.typed"
if (cd "$WORK/nomarker" && bash bindings/python/scripts/build-wheel.sh "linux-$ARCH" "$WORK/libfixture.so" 1.2.3) >"$WORK/nomarker.log" 2>&1; then
fail "a package without the PEP 561 marker was wheeled"
elif grep -q "is missing \['offline_protocol_sdk/py.typed'\]" "$WORK/nomarker.log"; then
pass "a package without the PEP 561 marker is refused"
else
fail "a package without the PEP 561 marker was refused for another reason"
cat "$WORK/nomarker.log" >&2
fi
copy_package "$WORK/badversion"
if (cd "$WORK/badversion" && bash bindings/python/scripts/build-wheel.sh "linux-$ARCH" "$WORK/libfixture.so" 1.2.3+local) >/dev/null 2>&1; then
fail "a version with no Python spelling was wheeled"
Expand Down
Loading