Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MicroPython compatibility #2976

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 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
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,59 @@ jobs:
if-no-files-found: error
include-hidden-files: true

micropython:
name: MicroPython compatibility
runs-on: ubuntu-latest
needs: [ pre-commit, towncrier, package ]
steps:
- name: Build MicroPython
run: |
# The following script also works on macOS, except that due to a bug,
# threading must be enabled, i.e. THREAD must be omitted from the disabled
# feature list.
cd $RUNNER_TEMP
git clone https://github.com/micropython/micropython -b v1.24.0 --depth 1

# Create a variant with the same configuration as the PyScript build.
cd micropython/ports/unix
cp -a variants/standard variants/pyscript
grep require ../webassembly/variants/pyscript/manifest.py \
>> variants/pyscript/manifest.py
for feature in BTREE FFI SOCKET SSL TERMIOS THREAD; do
echo "MICROPY_PY_$feature = 0" >> variants/pyscript/mpconfigvariant.mk
done

export VARIANT=pyscript
make submodules
make -j $(nproc)

- name: Checkout
uses: actions/[email protected]

- name: Set up Python
uses: actions/[email protected]
with:
python-version: "3.13"

- name: Get Packages
uses: actions/[email protected]
with:
name: Packages-toga-core
path: dist

- name: Test
run: |
pip install dist/toga_core-*.whl
site_packages=$(python -c '
import sys
print([path for path in sys.path if "site-packages" in path][0])
')

cd core
export MICROPYPATH="$site_packages:.frozen"
$RUNNER_TEMP/micropython/ports/unix/build-pyscript/micropython \
micropython_check.py

core-coverage:
name: Coverage
needs: core
Expand Down
1 change: 1 addition & 0 deletions changes/2976.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A subset of Toga is now compatible with MicroPython.
51 changes: 51 additions & 0 deletions core/micropython_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
#
# This script should be run under MicroPython to check that it can import all the Toga
# modules required by Invent.

# The top-level Toga module must be imported first, to enable the standard library
# compatibility shims.
#
# isort: off
import toga

# isort: on
import sys
import traceback

# Access attributes to trigger lazy import.
failures = 0
for name in [
"App",
"Font",
"Image",
"Window",
#
"Widget",
"Box",
"Button",
"DateInput",
"Divider",
"ImageView",
"Label",
"MultilineTextInput",
"PasswordInput",
"ProgressBar",
"Slider",
"Switch",
"TextInput",
"TimeInput",
]:
try:
getattr(toga, name)
except Exception:
failures += 1
print(f"Failed to import toga.{name}:")
traceback.print_exc()
print()

if failures:
print(f"{failures} names failed to import")
sys.exit(1)
else:
print("All names imported successfully")
3 changes: 1 addition & 2 deletions core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ classifiers = [
]
dependencies = [
"travertino >= 0.3.0",
# limited to <=3.9 for the `group` argument for `entry_points()`
"importlib_metadata >= 4.4.0; python_version <= '3.9'",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -114,6 +112,7 @@ relative_files = true
# See notes in the root pyproject.toml file.
source = ["src"]
source_pkgs = ["toga"]
omit = ["**/toga/compat/**"] # Only used by MicroPython.

[tool.coverage.paths]
source = [
Expand Down
37 changes: 25 additions & 12 deletions core/src/toga/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from __future__ import annotations
# Enable the standard library compatibility shims before doing anything else.
#
# __future__ imports must be at the very top of the file, and MicroPython doesn't
# currently include a __future__ module, so this file can't contain any __future__
# imports. Other modules imported after `compat` can use __future__ as normal.
import sys

if sys.implementation.name != "cpython": # pragma: no cover
from . import compat # noqa: F401
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idle thought - should the compat module name be based on sys.implementation.name in some way? I don't have any idea which other implementations we'd be concerned about, but at the very least it would be clear from the namespace that toga.compat.micropython is a micropython compatibility shim.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there won't be any other implementations in the foreseeable future, let's cross that bridge when we come to it. I see this compat package as a temporary stopgap anyway – virtually all of it should eventually be upstreamed to micropython-lib.


import importlib
import warnings
from pathlib import Path
from importlib import import_module

toga_core_imports = {
# toga.app imports
Expand Down Expand Up @@ -83,15 +90,21 @@


def __getattr__(name):
try:
module_name = toga_core_imports[name]
except KeyError:
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") from None
else:
module = importlib.import_module(module_name)
if module_name := toga_core_imports.get(name):
module = import_module(module_name)
value = getattr(module, name)
globals()[name] = value
return value
else:
# MicroPython apparently doesn't attempt a submodule import when __getattr__
# raises AttributeError, so we need to do it manually.
try:
value = import_module(f"{__name__}.{name}")
except ImportError:
raise AttributeError(
f"module '{__name__}' has no attribute '{name}'"
) from None

globals()[name] = value
return value


class NotImplementedWarning(RuntimeWarning):
Expand All @@ -104,7 +117,7 @@ def warn(cls, platform: str, feature: str) -> None:
warnings.warn(NotImplementedWarning(f"[{platform}] Not implemented: {feature}"))


def _package_version(file: Path | str | None, name: str) -> str:
def _package_version(file, name):
try:
# Read version from SCM metadata
# This will only exist in a development environment
Expand Down
Loading