Python: Replace import magic with an auto-loader

As discussed on Reddit, the magic import logic is not very tool friendly and a little too magic perhaps. Instead, this patch introduces an automatic loader (`slint.loader`), which can traverse `sys.path` and lazily load `.slint` files by attribute lookup.

Closes #4856
This commit is contained in:
Simon Hausmann 2024-04-18 15:51:28 +02:00 committed by Simon Hausmann
parent 93307707cd
commit f86f4993fa
6 changed files with 48 additions and 52 deletions

View file

@ -74,9 +74,8 @@ export component AppWindow inherits Window {
```python
import slint
import appwindow_slint
class App(appwindow_slint.AppWindow):
class App(slint.loader.appwindow.AppWindow):
@slint.callback
def request_increase_value(self):
self.counter = self.counter + 1
@ -93,7 +92,7 @@ app.run()
The following example shows how to instantiate a Slint component in Python:
**`ui.slint`**
**`app.slint`**
```slint
export component MainWindow inherits Window {
@ -111,21 +110,25 @@ export component MainWindow inherits Window {
The exported component is exposed as a Python class. To access this class, you have two
options:
1. Call `slint.load_file("ui.slint")`. The returned object is a [namespace](https://docs.python.org/3/library/types.html#types.SimpleNamespace),
1. Call `slint.load_file("app.slint")`. The returned object is a [namespace](https://docs.python.org/3/library/types.html#types.SimpleNamespace),
that provides the `MainWindow` class:
```python
import slint
components = slint.load_file("ui.slint")
components = slint.load_file("app.slint")
main_window = components.MainWindow()
```
2. Import the `.slint` file as module by treating it like a Python module where the `.slint` extension is replaced with `_slint`:
2. Use Slint's auto-loader, which lazily loads `.slint` files from `sys.path`:
```python
import slint # needs to come first
from ui_slint import MainWindow
main_window = MainWindow()
import slint
# Look for for `app.slint` in `sys.path`:
main_window = slint.loader.app.MainWindow()
```
Any attribute lookup in `slint.loader` is searched for in `sys.path`. If a directory with the name exists, it is returned as a loader object, and subsequent
attribute lookups follow the same logic. If the name matches a file with the `.slint` extension, it is automatically loaded with `load_file` and the
[namespace](https://docs.python.org/3/library/types.html#types.SimpleNamespace) is returned.
### Accessing Properties
[Properties](../slint/src/language/syntax/properties) declared as `out` or `in-out` in `.slint` files are visible as properties on the component instance.

View file

@ -7,7 +7,7 @@ build-backend = "maturin"
[project]
name = "slint"
version = "1.6.0a5"
version = "1.6.0a6"
requires-python = ">= 3.10"
authors = [
{name = "Slint Team", email = "info@slint.dev"},

View file

@ -189,30 +189,31 @@ def load_file(path, quiet=False, style=None, include_paths=None, library_paths=N
return module
class SlintModuleLoader:
def create_module(self, spec):
class SlintAutoLoader:
def __init__(self, base_dir=None):
if base_dir:
self.local_dirs = [base_dir]
else:
self.local_dirs = None
def __getattr__(self, name):
for path in self.local_dirs or sys.path:
dir_candidate = os.path.join(path, name)
if os.path.isdir(dir_candidate):
loader = SlintAutoLoader(dir_candidate)
setattr(self, name, loader)
return loader
file_candidate = dir_candidate + ".slint"
if os.path.isfile(file_candidate):
type_namespace = load_file(file_candidate)
setattr(self, name, type_namespace)
return type_namespace
return None
def exec_module(self, module):
m = load_file(module.__name__)
module.__dict__.update(m.__dict__)
class SlintModuleFinder:
def find_spec(self, name, path, target=None):
if "." in name:
return None
if not name.endswith("_slint"):
return None
candidate_filename = name.removesuffix("_slint") + ".slint"
for path in sys.path:
candidate = os.path.join(path, candidate_filename)
if os.path.exists(candidate):
return ModuleSpec(os.path.realpath(candidate), SlintModuleLoader())
return None
loader = SlintAutoLoader()
def _callback_decorator(callable, info):
@ -235,8 +236,6 @@ def callback(global_name=None, name=None):
return lambda callback: _callback_decorator(callback, info)
sys.meta_path.append(SlintModuleFinder())
Image = native.PyImage
Color = native.PyColor
Brush = native.PyBrush

View file

@ -3,25 +3,23 @@
import pytest
from slint import slint as native
from slint import loader
import sys
import os
def test_magic_import():
import test_load_file_slint as compiledmodule
instance = compiledmodule.App()
instance = loader.test_load_file.App()
del instance
def test_magic_import_path():
oldsyspath = sys.path
with pytest.raises(ModuleNotFoundError, match="No module named 'printerdemo_slint'"):
import printerdemo_slint
assert loader.printerdemo == None
try:
sys.path.append(os.path.join(os.path.dirname(__file__),
"..", "..", "..", "examples", "printerdemo", "ui"))
import printerdemo_slint
instance = printerdemo_slint.MainWindow()
"..", "..", ".."))
instance = loader.examples.printerdemo.ui.printerdemo.MainWindow()
del instance
finally:
sys.path = oldsyspath