diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 21a1b36..480edeb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,14 +15,14 @@ repos: exclude: \.min\.js$ - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 + rev: v0.15.5 hooks: - - id: ruff + - id: ruff-check args: ["check", "--select", "I", "--fix"] files: "^src/" - id: ruff-format types_or: [python, pyi] - - id: ruff + - id: ruff-check args: [--fix] - repo: https://github.com/hoxbro/clean_notebook rev: v0.1.15 diff --git a/pyproject.toml b/pyproject.toml index 64d3e3e..ca809ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,6 +140,7 @@ select = [ "W", # pycodestyle warnings "I", # isort "PIE", + "PLC0415", # import should be at the top-level of a file "T20", "RUF006", "UP004", diff --git a/src/panel_live_server/app.py b/src/panel_live_server/app.py index 4dbb64c..b03804f 100644 --- a/src/panel_live_server/app.py +++ b/src/panel_live_server/app.py @@ -66,13 +66,11 @@ def _build_websocket_origins(address: str, port: int) -> list[str]: def main(address: str = "localhost", port: int = 5077, show: bool = True) -> None: """Start the Panel server.""" - # panel and the pages stay local: ~580 ms and ~930 ms, only `pls serve` needs them. - import panel as pn + # noqa on both: only `pls serve` renders pages, and hoisting these would make + # every other command pay ~580 ms for panel and ~930 ms for the pages module. + import panel as pn # noqa: PLC0415 - from panel_live_server.pages import add_page - from panel_live_server.pages import admin_page - from panel_live_server.pages import feed_page - from panel_live_server.pages import view_page + import panel_live_server.pages as pages_module # noqa: PLC0415 # Initialize the database _ = get_db() @@ -86,10 +84,10 @@ def main(address: str = "localhost", port: int = 5077, show: bool = True) -> Non # Configure pages pages = { - "/view": view_page, - "/feed": feed_page, - "/admin": admin_page, - "/add": add_page, + "/view": pages_module.view_page, + "/feed": pages_module.feed_page, + "/admin": pages_module.admin_page, + "/add": pages_module.add_page, } # Configure extra patterns for Tornado handlers (REST API endpoints) diff --git a/src/panel_live_server/cli.py b/src/panel_live_server/cli.py index c49bd45..54c37dd 100644 --- a/src/panel_live_server/cli.py +++ b/src/panel_live_server/cli.py @@ -18,9 +18,12 @@ prepend_env_dll_paths(os.environ) +import requests import typer from panel_live_server import __version__ +from panel_live_server.config import default_panel_port +from panel_live_server.config import reset_config from panel_live_server.install import SERVER_NAME from panel_live_server.install import InstallError from panel_live_server.install import claude_desktop_config_path @@ -30,6 +33,7 @@ from panel_live_server.install import resolve_pls_command from panel_live_server.install import vscode_config_path from panel_live_server.prompts import render_instructions +from panel_live_server.screenshot import install_browser as _install_browser logger = logging.getLogger(__name__) @@ -118,9 +122,6 @@ def serve( else: logging.basicConfig(level=logging.INFO) - from panel_live_server.config import default_panel_port - from panel_live_server.config import reset_config - if port is None: port = default_panel_port() @@ -133,14 +134,12 @@ def serve( # Reset the cached config singleton so it re-reads the env vars we just set reset_config() - from panel_live_server.app import main as app_main + # noqa: app pulls in panel, ~487 ms that only `pls serve` needs. + from panel_live_server.app import main as app_main # noqa: PLC0415 try: app_main(address=host, port=port, show=show) except OSError as exc: - # requests stays local: it costs ~34 ms and only this recovery path needs it. - import requests - if exc.errno != errno.EADDRINUSE: raise url = f"http://{host}:{port}/api/health" @@ -211,8 +210,8 @@ def mcp( if prompts: os.environ["PANEL_LIVE_SERVER_PROMPTS_FILE"] = prompts - # Kept local: importing server pulls in panel, ~930 ms that every other command would pay. - from panel_live_server.server import mcp as mcp_server + # noqa: server pulls in panel, ~930 ms that every other command would pay. + from panel_live_server.server import mcp as mcp_server # noqa: PLC0415 # server.py renders at import time, so re-render here (~1 ms) or an earlier import silently wins. mcp_server.instructions = render_instructions() @@ -250,10 +249,6 @@ def status( Queries the health endpoint and reports the server status. """ - import requests - - from panel_live_server.config import default_panel_port - if port is None: port = default_panel_port() @@ -459,8 +454,6 @@ def install_browser() -> None: installing (pixi users get it via `pixi run postinstall`). It lands in the same environment that runs `pls`. """ - from panel_live_server.screenshot import install_browser as _install_browser - typer.echo("Installing Chromium for the screenshot tool (one-time)...") code = _install_browser() if code == 0: diff --git a/src/panel_live_server/prompts.py b/src/panel_live_server/prompts.py index ef9f4d9..10ff65b 100644 --- a/src/panel_live_server/prompts.py +++ b/src/panel_live_server/prompts.py @@ -36,6 +36,10 @@ import sys from pathlib import Path +from jinja2 import FileSystemLoader +from jinja2 import nodes +from jinja2.sandbox import SandboxedEnvironment + logger = logging.getLogger(__name__) _BUILTIN_DIR = Path(__file__).parent / "templates" / "prompts" @@ -123,17 +127,12 @@ def _load_overrides() -> dict[str, tuple[str, str]]: def _build_environment(): """Build the Jinja environment that loads the shipped templates.""" - from jinja2 import FileSystemLoader - from jinja2.sandbox import SandboxedEnvironment - # Sandboxed because the override text is user-supplied config, not app code. return SandboxedEnvironment(loader=FileSystemLoader(str(_BUILTIN_DIR)), keep_trailing_newline=False) def _blocks_in(template: str) -> list[str]: """Return the block names declared by one shipped template.""" - from jinja2 import nodes - source = (_BUILTIN_DIR / template).read_text(encoding="utf-8") return [node.name for node in _build_environment().parse(source).find_all(nodes.Block)] diff --git a/src/panel_live_server/screenshot.py b/src/panel_live_server/screenshot.py index 5f7d67b..25a815a 100644 --- a/src/panel_live_server/screenshot.py +++ b/src/panel_live_server/screenshot.py @@ -97,7 +97,10 @@ def is_browser_installed() -> bool: directly inside a running event loop. """ try: - from playwright.sync_api import sync_playwright + # noqa: not about import cost. server.py, client.py, and endpoints.py all + # import this module at module level, so a broken playwright install would + # take `pls mcp` down entirely instead of just disabling screenshots. + from playwright.sync_api import sync_playwright # noqa: PLC0415 except ImportError: return False try: @@ -402,7 +405,10 @@ async def _ensure_browser(self): return self._browser try: - from playwright.async_api import async_playwright + # noqa: same reason as is_browser_installed above. Keeping this + # nested turns a broken install into PlaywrightUnavailableError + # rather than an import-time failure of every module downstream. + from playwright.async_api import async_playwright # noqa: PLC0415 except ImportError as e: raise PlaywrightUnavailableError(_INSTALL_HINT) from e diff --git a/src/panel_live_server/utils.py b/src/panel_live_server/utils.py index 1d94d8a..a5cb748 100644 --- a/src/panel_live_server/utils.py +++ b/src/panel_live_server/utils.py @@ -260,8 +260,10 @@ def find_requirements(code: str) -> list[str]: List of required package names """ try: - # Import panel's find_requirements function - from panel.io.mime_render import find_requirements as panel_find_requirements + # noqa: this module must not import panel at module level. On Windows, + # cli.py imports it to fix the DLL search path *before* any heavy import, + # so panel's native extensions would fail to load if pulled in here. + from panel.io.mime_render import find_requirements as panel_find_requirements # noqa: PLC0415 return panel_find_requirements(code) except (ImportError, AttributeError): @@ -419,8 +421,11 @@ def _isolated_curdoc(): ``session_context``, which is the condition ``.servable()`` requires before it writes anything, so it quietly becomes a no-op here. """ - from bokeh.document import Document - from panel.io.state import set_curdoc + # noqa on both: same reason as find_requirements above. This module is imported + # by cli.py on Windows to fix the DLL search path before any heavy import, so + # bokeh and panel must not be pulled in at module level. + from bokeh.document import Document # noqa: PLC0415 + from panel.io.state import set_curdoc # noqa: PLC0415 return set_curdoc(Document())