mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 19:21:50 +02:00
feat(plugin manager): add cli commands
This commit is contained in:
0
bec_widgets/utils/bec_plugin_manager/__init__.py
Normal file
0
bec_widgets/utils/bec_plugin_manager/__init__.py
Normal file
83
bec_widgets/utils/bec_plugin_manager/create/widget.py
Normal file
83
bec_widgets/utils/bec_plugin_manager/create/widget.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import copier
|
||||||
|
import typer
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_lib.plugin_helper import plugin_repo_path
|
||||||
|
from bec_lib.utils.plugin_manager._constants import ANSWER_KEYS
|
||||||
|
from bec_lib.utils.plugin_manager._util import existing_data, git_stage_files, make_commit
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_plugin_manager.edit_ui import open_and_watch_ui_editor
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
_app = typer.Typer(rich_markup_mode="rich")
|
||||||
|
|
||||||
|
|
||||||
|
def _commit_added_widget(repo: Path, name: str):
|
||||||
|
git_stage_files(repo, [".copier-answers.yml"])
|
||||||
|
git_stage_files(repo / repo.name / "bec_widgets" / "widgets" / name, [])
|
||||||
|
make_commit(repo, f"plugin-manager added new widget: {name}")
|
||||||
|
logger.info(f"Committing new widget {name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _widget_exists(widget_list: list[dict[str, str]], name: str):
|
||||||
|
return name in [w["name"] for w in widget_list]
|
||||||
|
|
||||||
|
|
||||||
|
def _editor_cb(ctx: typer.Context, value: bool):
|
||||||
|
if value and not ctx.params["use_ui"]:
|
||||||
|
raise typer.BadParameter("Can only open the editor if creating a .ui file!")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@_app.command()
|
||||||
|
def widget(
|
||||||
|
name: Annotated[str, typer.Argument(help="Enter a name for your widget in snake_case")],
|
||||||
|
use_ui: Annotated[
|
||||||
|
bool, typer.Option(prompt=True, help="Generate a .ui file for use in bec-designer.")
|
||||||
|
] = True,
|
||||||
|
open_editor: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
prompt=True, help="Open the created widget in bec-designer.", callback=_editor_cb
|
||||||
|
),
|
||||||
|
] = True,
|
||||||
|
):
|
||||||
|
"""Create a new widget plugin with the given name.
|
||||||
|
|
||||||
|
If [bold white]use_ui[/bold white] is set, a bec-designer .ui file will also be created. If \
|
||||||
|
[bold white]open_editor[/bold white] is additionally set, the .ui file will be opened in \
|
||||||
|
bec-designer and the compiled python version will be updated when changes are made."""
|
||||||
|
if (formatted_name := name.lower().replace("-", "_")) != name:
|
||||||
|
logger.warning(f"Adjusting widget name from {name} to {formatted_name}")
|
||||||
|
if not formatted_name.isidentifier():
|
||||||
|
logger.error(
|
||||||
|
f"{name} is not a valid name for a widget (even after converting to {formatted_name}) - please enter something in snake_case"
|
||||||
|
)
|
||||||
|
exit(-1)
|
||||||
|
logger.info(f"Adding new widget {formatted_name} to the template...")
|
||||||
|
try:
|
||||||
|
repo = Path(plugin_repo_path())
|
||||||
|
plugin_data = existing_data(repo, [ANSWER_KEYS.VERSION, ANSWER_KEYS.WIDGETS])
|
||||||
|
if _widget_exists(plugin_data[ANSWER_KEYS.WIDGETS], formatted_name):
|
||||||
|
logger.error(f"Widget {formatted_name} already exists!")
|
||||||
|
exit(-1)
|
||||||
|
plugin_data[ANSWER_KEYS.WIDGETS].append({"name": formatted_name, "use_ui": use_ui})
|
||||||
|
copier.run_update(
|
||||||
|
repo,
|
||||||
|
data=plugin_data,
|
||||||
|
defaults=True,
|
||||||
|
unsafe=True,
|
||||||
|
overwrite=True,
|
||||||
|
vcs_ref=plugin_data[ANSWER_KEYS.VERSION],
|
||||||
|
)
|
||||||
|
_commit_added_widget(repo, formatted_name)
|
||||||
|
except Exception:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
logger.error("exiting...")
|
||||||
|
exit(-1)
|
||||||
|
logger.success(f"Added widget {formatted_name}!")
|
||||||
|
if open_editor:
|
||||||
|
open_and_watch_ui_editor(formatted_name)
|
105
bec_widgets/utils/bec_plugin_manager/edit_ui.py
Normal file
105
bec_widgets/utils/bec_plugin_manager/edit_ui.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_lib.plugin_helper import plugin_repo_path
|
||||||
|
from watchdog.events import (
|
||||||
|
DirCreatedEvent,
|
||||||
|
DirModifiedEvent,
|
||||||
|
DirMovedEvent,
|
||||||
|
FileCreatedEvent,
|
||||||
|
FileModifiedEvent,
|
||||||
|
FileMovedEvent,
|
||||||
|
FileSystemEvent,
|
||||||
|
FileSystemEventHandler,
|
||||||
|
)
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
|
||||||
|
from bec_widgets.utils.plugin_utils import get_custom_classes
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class RecompileHandler(FileSystemEventHandler):
|
||||||
|
def __init__(self, in_file: Path, out_file: Path) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.in_file = str(in_file)
|
||||||
|
self.out_file = str(out_file)
|
||||||
|
self._pyside_import_re = re.compile(r"from PySide6\.(.*) import ")
|
||||||
|
self._widget_import_re = re.compile(
|
||||||
|
r"^from ([a-zA-Z_]*) import ([a-zA-Z_]*)$", re.MULTILINE
|
||||||
|
)
|
||||||
|
self._widget_modules = {
|
||||||
|
c.name: c.module for c in (get_custom_classes("bec_widgets") + get_all_plugin_widgets())
|
||||||
|
}
|
||||||
|
|
||||||
|
def on_created(self, event: DirCreatedEvent | FileCreatedEvent) -> None:
|
||||||
|
self.recompile(event)
|
||||||
|
|
||||||
|
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
|
||||||
|
self.recompile(event)
|
||||||
|
|
||||||
|
def on_moved(self, event: DirMovedEvent | FileMovedEvent) -> None:
|
||||||
|
self.recompile(event)
|
||||||
|
|
||||||
|
def recompile(self, event: FileSystemEvent) -> None:
|
||||||
|
if event.src_path == self.in_file or event.dest_path == self.in_file:
|
||||||
|
self._recompile()
|
||||||
|
|
||||||
|
def _recompile(self):
|
||||||
|
logger.success(".ui file modified, recompiling...")
|
||||||
|
code = subprocess.call(
|
||||||
|
["pyside6-uic", "--absolute-imports", self.in_file, "-o", self.out_file]
|
||||||
|
)
|
||||||
|
logger.success(f"compilation exited with code {code}")
|
||||||
|
if code == 0:
|
||||||
|
logger.success("updating imports...")
|
||||||
|
self._update_imports()
|
||||||
|
|
||||||
|
def _update_imports(self):
|
||||||
|
with open(self.out_file, "r+") as f:
|
||||||
|
initial = f.read()
|
||||||
|
f.seek(0)
|
||||||
|
qtpy_imports = re.sub(
|
||||||
|
self._pyside_import_re, lambda ob: f"from qtpy.{ob.group(1)} import ", initial
|
||||||
|
)
|
||||||
|
print(self._widget_modules)
|
||||||
|
print(re.findall(self._widget_import_re, qtpy_imports))
|
||||||
|
widget_imports = re.sub(
|
||||||
|
self._widget_import_re,
|
||||||
|
lambda ob: (
|
||||||
|
f"from {module} import {ob.group(2)}"
|
||||||
|
if (module := self._widget_modules.get(ob.group(2))) is not None
|
||||||
|
else ob.group(1)
|
||||||
|
),
|
||||||
|
qtpy_imports,
|
||||||
|
)
|
||||||
|
f.write(widget_imports)
|
||||||
|
f.truncate()
|
||||||
|
|
||||||
|
|
||||||
|
def open_and_watch_ui_editor(widget_name: str):
|
||||||
|
logger.info(f"Opening the editor for {widget_name}... ")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from bec_widgets.utils.bec_designer import open_designer
|
||||||
|
except ImportError:
|
||||||
|
logger.error("BEC Widgets must be installed to use the UI editor tool")
|
||||||
|
exit(127)
|
||||||
|
|
||||||
|
repo = Path(plugin_repo_path())
|
||||||
|
widget_dir = repo / repo.name / "bec_widgets" / "widgets" / widget_name
|
||||||
|
ui_file = widget_dir / f"{widget_name}.ui"
|
||||||
|
ui_outfile = widget_dir / f"{widget_name}_ui.py"
|
||||||
|
recompile_handler = RecompileHandler(ui_file, ui_outfile)
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(recompile_handler, str(ui_file.parent))
|
||||||
|
observer.start()
|
||||||
|
try:
|
||||||
|
open_designer([str(ui_file)])
|
||||||
|
finally:
|
||||||
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
|
logger.info("Editing session ended, exiting...")
|
@ -38,6 +38,7 @@ dev = [
|
|||||||
"pytest-xvfb~=3.0",
|
"pytest-xvfb~=3.0",
|
||||||
"pytest~=8.0",
|
"pytest~=8.0",
|
||||||
"pytest-cov~=6.1.1",
|
"pytest-cov~=6.1.1",
|
||||||
|
"watchdog~=6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
Reference in New Issue
Block a user