|
12 | 12 | from mkdocs.config import config_options |
13 | 13 | from mkdocs.config.defaults import MkDocsConfig |
14 | 14 | from mkdocs.plugins import BasePlugin |
| 15 | +from packaging.requirements import Requirement |
| 16 | + |
| 17 | +try: # Python 3.11+ |
| 18 | + import tomllib |
| 19 | +except ModuleNotFoundError: # pragma: no cover - fallback for 3.10 |
| 20 | + import tomli as tomllib # type: ignore[no-redef] |
15 | 21 |
|
16 | 22 | logger = logging.getLogger("flet.docs.examples_gallery") |
17 | 23 |
|
@@ -46,10 +52,118 @@ def _ensure_pip_available() -> None: |
46 | 52 |
|
47 | 53 |
|
48 | 54 | def _web_path_ready(path: Path) -> bool: |
49 | | - """Return True when ``path`` points to a usable web client.""" |
| 55 | + """Return True when `path` points to a usable web client.""" |
50 | 56 | return path.is_dir() and path.joinpath("index.html").is_file() |
51 | 57 |
|
52 | 58 |
|
| 59 | +def _find_pyproject(start: Path, stop: Optional[Path] = None) -> Optional[Path]: |
| 60 | + """Walk up from start looking for pyproject.toml, stopping at `stop` if provided.""" |
| 61 | + current = start.resolve() |
| 62 | + stop = stop.resolve() if stop else None |
| 63 | + while True: |
| 64 | + candidate = current / "pyproject.toml" |
| 65 | + if candidate.is_file(): |
| 66 | + return candidate |
| 67 | + if current == current.parent or (stop and current == stop): |
| 68 | + return None |
| 69 | + current = current.parent |
| 70 | + |
| 71 | + |
| 72 | +def _version_from_specifiers(specifiers) -> Optional[str]: |
| 73 | + """Extract a deterministic version from a SpecifierSet. |
| 74 | +
|
| 75 | + Prioritises exact pins to avoid drift; otherwise falls back to the lowest |
| 76 | + acceptable bound (>=, >, ~=) to stay compatible with the source tree. |
| 77 | + """ |
| 78 | + for operator in ("==", "==="): |
| 79 | + for spec in specifiers: |
| 80 | + if spec.operator == operator: |
| 81 | + return spec.version |
| 82 | + |
| 83 | + for operator in ("~=", ">=", ">"): |
| 84 | + for spec in specifiers: |
| 85 | + if spec.operator == operator: |
| 86 | + return spec.version |
| 87 | + |
| 88 | + return None |
| 89 | + |
| 90 | + |
| 91 | +def _version_from_pyproject(pyproject: Path) -> Optional[str]: |
| 92 | + """ |
| 93 | + Read the flet dependency from pyproject.toml and return a pinned/lowest version. |
| 94 | + """ |
| 95 | + try: |
| 96 | + data = tomllib.loads(pyproject.read_text()) |
| 97 | + except Exception as exc: # noqa: BLE001 |
| 98 | + logger.debug("Unable to parse %s: %s", pyproject, exc) |
| 99 | + return None |
| 100 | + |
| 101 | + deps = data.get("project", {}).get("dependencies") or [] |
| 102 | + for raw in deps: |
| 103 | + try: |
| 104 | + req = Requirement(raw) |
| 105 | + except Exception: # noqa: BLE001 |
| 106 | + continue |
| 107 | + if req.name != "flet": |
| 108 | + continue |
| 109 | + candidate = _version_from_specifiers(req.specifier) |
| 110 | + if candidate: |
| 111 | + return candidate |
| 112 | + return None |
| 113 | + |
| 114 | + |
| 115 | +def _resolve_flet_version( |
| 116 | + env: Mapping[str, str], src_root: Path, docs_dir: Path |
| 117 | +) -> str: |
| 118 | + """Resolve the Flet version similar to `flet publish`. |
| 119 | +
|
| 120 | + Order: |
| 121 | + - Explicit override: FLET_WEB_VERSION. |
| 122 | + - Version pinned in the gallery's pyproject.toml (walk up from src). |
| 123 | + - The version exposed by the local flet package |
| 124 | + (patched in releases or derived from git tags). |
| 125 | + - DEFAULT_VERSION from flet.version. |
| 126 | +
|
| 127 | + This keeps docs builds reproducible for historical commits and avoids |
| 128 | + pulling newer incompatible clients. |
| 129 | + """ |
| 130 | + default_version: Optional[str] = None |
| 131 | + try: |
| 132 | + from flet import version as flet_version |
| 133 | + |
| 134 | + default_version = getattr(flet_version, "DEFAULT_VERSION", None) |
| 135 | + except Exception as exc: # noqa: BLE001 |
| 136 | + logger.debug("Unable to preload flet version defaults: %s", exc) |
| 137 | + |
| 138 | + if env.get("FLET_WEB_VERSION"): |
| 139 | + return str(env["FLET_WEB_VERSION"]) |
| 140 | + |
| 141 | + pyproject = _find_pyproject(src_root, stop=docs_dir.parent) |
| 142 | + pinned = _version_from_pyproject(pyproject) if pyproject else None |
| 143 | + if pinned: |
| 144 | + return pinned |
| 145 | + |
| 146 | + try: |
| 147 | + from flet import version as flet_version |
| 148 | + |
| 149 | + resolved = flet_version.version or getattr( |
| 150 | + flet_version, "DEFAULT_VERSION", None |
| 151 | + ) |
| 152 | + resolved = resolved or default_version |
| 153 | + if resolved: |
| 154 | + return str(resolved) |
| 155 | + except Exception as exc: # noqa: BLE001 |
| 156 | + logger.debug("Unable to resolve flet version from package: %s", exc) |
| 157 | + |
| 158 | + if default_version: |
| 159 | + return str(default_version) |
| 160 | + |
| 161 | + raise RuntimeError( |
| 162 | + "FLET_WEB_PATH is not set and Flet version could not be determined. " |
| 163 | + "Set FLET_WEB_VERSION or provide FLET_WEB_PATH pointing to a built client." |
| 164 | + ) |
| 165 | + |
| 166 | + |
53 | 167 | def _locate_existing_web_client(env: Mapping[str, str]) -> Optional[Path]: |
54 | 168 | """Check configured env or installed packages for a usable web client.""" |
55 | 169 | configured = env.get("FLET_WEB_PATH") |
@@ -77,7 +191,10 @@ def _locate_existing_web_client(env: Mapping[str, str]) -> Optional[Path]: |
77 | 191 |
|
78 | 192 |
|
79 | 193 | def _download_packaged_web_client(version: str) -> Optional[Path]: |
80 | | - """Download the pre-built flet-web wheel and expose its web folder.""" |
| 194 | + """Download the pre-built flet-web wheel and expose its web folder. |
| 195 | +
|
| 196 | + Uses a temp/versioned cache to avoid repeated downloads during local dev. |
| 197 | + """ |
81 | 198 | cache_root = Path(tempfile.gettempdir()) / "flet-web" |
82 | 199 | target_root = cache_root / version |
83 | 200 | web_dir = target_root / "flet_web" / "web" |
@@ -110,42 +227,56 @@ def _download_packaged_web_client(version: str) -> Optional[Path]: |
110 | 227 | ) |
111 | 228 | except subprocess.CalledProcessError as exc: |
112 | 229 | logger.warning("Unable to download flet-web==%s: %s", version, exc) |
113 | | - return None |
| 230 | + logger.info("Falling back to latest available pre-release flet-web wheel.") |
| 231 | + try: |
| 232 | + subprocess.run( |
| 233 | + [ |
| 234 | + sys.executable, |
| 235 | + "-m", |
| 236 | + "pip", |
| 237 | + "install", |
| 238 | + "--no-deps", |
| 239 | + "--only-binary", |
| 240 | + ":all:", |
| 241 | + "--pre", |
| 242 | + "flet-web", |
| 243 | + "--target", |
| 244 | + str(target_root), |
| 245 | + ], |
| 246 | + check=True, |
| 247 | + ) |
| 248 | + except subprocess.CalledProcessError as exc2: |
| 249 | + logger.warning("Unable to download a fallback flet-web wheel: %s", exc2) |
| 250 | + return None |
114 | 251 |
|
115 | 252 | return web_dir if _web_path_ready(web_dir) else None |
116 | 253 |
|
117 | 254 |
|
118 | | -def _ensure_web_client(env: dict[str, str]) -> Path: |
119 | | - """Ensure a web client exists and return the path to use for publishing.""" |
| 255 | +def _ensure_web_client(env: dict[str, str], src_root: Path, docs_dir: Path) -> Path: |
| 256 | + """Ensure a web client exists and return the path to use for publishing. |
| 257 | +
|
| 258 | + Prefers an already-available client (env or packaged). If none exists, |
| 259 | + resolves a version and downloads the wheel into a temp cache. |
| 260 | + """ |
120 | 261 | existing = _locate_existing_web_client(env) |
121 | 262 | if existing: |
122 | 263 | return existing |
123 | 264 |
|
124 | | - try: |
125 | | - from flet import version as flet_version |
126 | | - except Exception as exc: # noqa: BLE001 |
127 | | - raise RuntimeError( |
128 | | - "FLET_WEB_PATH is not set and the flet version could not be determined to " |
129 | | - "download a packaged web client. Set FLET_WEB_PATH manually to a built " |
130 | | - "client (e.g. client/build/web)." |
131 | | - ) from exc |
132 | | - |
133 | | - target_version = flet_version.version or getattr( |
134 | | - flet_version, "DEFAULT_VERSION", "" |
135 | | - ) |
| 265 | + target_version = _resolve_flet_version(env, src_root, docs_dir) |
136 | 266 | downloaded = _download_packaged_web_client(target_version) |
137 | 267 | if downloaded: |
138 | 268 | return downloaded |
139 | 269 |
|
140 | 270 | raise RuntimeError( |
141 | 271 | "FLET_WEB_PATH is not set and no packaged web client could be downloaded " |
142 | | - f"(tried flet-web=={target_version}). Provide FLET_WEB_PATH pointing to a " |
143 | | - "built client or ensure network access to download the flet-web wheel." |
| 272 | + f"(tried flet-web=={target_version} and latest pre-release). Provide " |
| 273 | + "FLET_WEB_PATH pointing to a built client or ensure network access to " |
| 274 | + "download the flet-web wheel." |
144 | 275 | ) |
145 | 276 |
|
146 | 277 |
|
147 | 278 | def _latest_mtime(root: Path, ignored: Optional[Iterable[Path]] = None) -> float: |
148 | | - """Return the newest mtime under ``root`` ignoring any ``ignored`` directories.""" |
| 279 | + """Return the newest mtime under `root` ignoring any `ignored` directories.""" |
149 | 280 | root = root.resolve() |
150 | 281 | ignore_roots = tuple(p.resolve() for p in ignored or ()) |
151 | 282 | latest = 0.0 |
@@ -177,6 +308,7 @@ class ExamplesGalleryPlugin(BasePlugin): |
177 | 308 | - FLET_SKIP_EXAMPLES_GALLERY=1 — skip building (useful for fast local docs iteration). |
178 | 309 | - FLET_WEB_PATH — optional path to a built Flet web client; if unset the plugin |
179 | 310 | downloads the matching flet-web wheel into a temp cache. |
| 311 | + - FLET_WEB_VERSION — optional version override for the wheel download. |
180 | 312 |
|
181 | 313 | Config options (mkdocs.yml): |
182 | 314 | - enabled: bool (default True) |
@@ -254,7 +386,7 @@ def on_pre_build(self, config: MkDocsConfig) -> None: |
254 | 386 | extra_env = self.config.get("env") or {} |
255 | 387 | env.update({k: str(v) for k, v in extra_env.items()}) |
256 | 388 |
|
257 | | - web_client_path = _ensure_web_client(env) |
| 389 | + web_client_path = _ensure_web_client(env, src_root, docs_dir) |
258 | 390 | env["FLET_WEB_PATH"] = str(web_client_path) |
259 | 391 |
|
260 | 392 | _ensure_pip_available() |
|
0 commit comments