Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 8 additions & 10 deletions src/panel_live_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down
23 changes: 8 additions & 15 deletions src/panel_live_server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__)

Expand Down Expand Up @@ -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()

Expand All @@ -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"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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:
Expand Down
9 changes: 4 additions & 5 deletions src/panel_live_server/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)]

Expand Down
10 changes: 8 additions & 2 deletions src/panel_live_server/screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
13 changes: 9 additions & 4 deletions src/panel_live_server/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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())

Expand Down
Loading