@@ -410,6 +410,14 @@ editor directs the user to copy the standalone `scripts/devlog_editor.py` into
|
||||
the project checkout, initialize and author the devlog, publish the changes, and
|
||||
recheck. It never edits, commits, or pushes an external project repository.
|
||||
|
||||
Project Editor requires the Lektor Python package because it uses the site's
|
||||
authoritative importer directly. If the launching Python cannot import Lektor,
|
||||
the editor automatically checks `LABYRICORN_LEKTOR_PYTHON`, a project-local
|
||||
`.venv`, common pipx Lektor environments, and the environment containing a
|
||||
`lektor` command. It relaunches itself with the first runtime that can import
|
||||
both Lektor and Tkinter. Set `LABYRICORN_LEKTOR_PYTHON` to an explicit Python
|
||||
executable when Lektor is installed somewhere else.
|
||||
|
||||
## Production filesystem and permissions
|
||||
|
||||
```text
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
_model: entry
|
||||
---
|
||||
title: This is a blog example, delete me later
|
||||
---
|
||||
date: 2026-08-13
|
||||
---
|
||||
author: Christopher Chambers
|
||||
---
|
||||
tags: ai, configuration, html, taggy
|
||||
---
|
||||
kicker: Blog
|
||||
---
|
||||
summary: Summary
|
||||
---
|
||||
published_urls:
|
||||
|
||||
---
|
||||
body:
|
||||
Body (markdown) woohoo!
|
||||
+103
-1
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
from typing import Any
|
||||
@@ -33,6 +34,104 @@ class ProjectEditorError(Exception):
|
||||
"""An expected project-editor problem suitable for display to a user."""
|
||||
|
||||
|
||||
def _python_has_lektor(executable: Path) -> bool:
|
||||
if not executable.is_file():
|
||||
return False
|
||||
options: dict[str, Any] = {
|
||||
"capture_output": True,
|
||||
"text": True,
|
||||
"timeout": 10,
|
||||
"check": False,
|
||||
}
|
||||
if os.name == "nt":
|
||||
options["creationflags"] = subprocess.CREATE_NO_WINDOW
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(executable), "-c", "import lektor, tkinter"],
|
||||
**options,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def _find_lektor_python(site_root: Path) -> Path | None:
|
||||
"""Locate a Python runtime that can import both Lektor and Tkinter."""
|
||||
candidates: list[Path] = []
|
||||
configured = os.environ.get("LABYRICORN_LEKTOR_PYTHON", "").strip()
|
||||
if configured:
|
||||
candidates.append(Path(configured).expanduser())
|
||||
if os.name == "nt":
|
||||
candidates.extend(
|
||||
(
|
||||
site_root / ".venv" / "Scripts" / "python.exe",
|
||||
Path(os.environ.get("LOCALAPPDATA", ""))
|
||||
/ "pipx"
|
||||
/ "pipx"
|
||||
/ "venvs"
|
||||
/ "lektor"
|
||||
/ "Scripts"
|
||||
/ "python.exe",
|
||||
Path.home()
|
||||
/ ".local"
|
||||
/ "pipx"
|
||||
/ "venvs"
|
||||
/ "lektor"
|
||||
/ "Scripts"
|
||||
/ "python.exe",
|
||||
)
|
||||
)
|
||||
else:
|
||||
candidates.extend(
|
||||
(
|
||||
site_root / ".venv" / "bin" / "python",
|
||||
Path.home() / ".local" / "pipx" / "venvs" / "lektor" / "bin" / "python",
|
||||
)
|
||||
)
|
||||
lektor_command = shutil.which("lektor")
|
||||
if lektor_command:
|
||||
candidates.append(
|
||||
Path(lektor_command).resolve().parent
|
||||
/ ("python.exe" if os.name == "nt" else "python")
|
||||
)
|
||||
|
||||
seen: set[Path] = set()
|
||||
for candidate in candidates:
|
||||
candidate = candidate.resolve()
|
||||
if candidate in seen:
|
||||
continue
|
||||
seen.add(candidate)
|
||||
if _python_has_lektor(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _relaunch_with_lektor(site_root: Path) -> bool:
|
||||
try:
|
||||
import lektor # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
if os.environ.get("LABYRICORN_LEKTOR_RELAUNCHED") == "1":
|
||||
return False
|
||||
executable = _find_lektor_python(site_root)
|
||||
if executable is None:
|
||||
return False
|
||||
gui_executable = executable
|
||||
if os.name == "nt":
|
||||
pythonw = executable.with_name("pythonw.exe")
|
||||
if pythonw.is_file():
|
||||
gui_executable = pythonw
|
||||
child_environment = {**os.environ, "LABYRICORN_LEKTOR_RELAUNCHED": "1"}
|
||||
subprocess.Popen(
|
||||
[str(gui_executable), str(Path(__file__).resolve()), *sys.argv[1:]],
|
||||
cwd=site_root,
|
||||
env=child_environment,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _load_project_importer() -> Any:
|
||||
try:
|
||||
return importlib.import_module("project_sources")
|
||||
@@ -691,8 +790,11 @@ def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--site-root", type=Path, default=Path(__file__).resolve().parents[1])
|
||||
args = parser.parse_args()
|
||||
site_root = args.site_root.resolve()
|
||||
if _relaunch_with_lektor(site_root):
|
||||
return 0
|
||||
try:
|
||||
run_gui(ProjectSourceRegistry(args.site_root))
|
||||
run_gui(ProjectSourceRegistry(site_root))
|
||||
except ProjectEditorError as exc:
|
||||
parser.exit(1, f"project-editor: ERROR: {exc}\n")
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user