From 34e75504daf0f54ba076a4b4782e6cb699b5dc5a Mon Sep 17 00:00:00 2001 From: SuMayaBee Date: Thu, 20 Aug 2026 03:43:48 +0600 Subject: [PATCH 1/2] chore: lint imports that are not at the top-level of a file --- .pre-commit-config.yaml | 6 +++--- pyproject.toml | 1 + src/panel_live_server/app.py | 10 +++++----- src/panel_live_server/cli.py | 20 ++++++++++++-------- src/panel_live_server/prompts.py | 7 ++++--- src/panel_live_server/screenshot.py | 6 ++++-- src/panel_live_server/utils.py | 9 +++++---- 7 files changed, 34 insertions(+), 25 deletions(-) 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..80d890e 100644 --- a/src/panel_live_server/app.py +++ b/src/panel_live_server/app.py @@ -67,12 +67,12 @@ 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 + 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 + from panel_live_server.pages import add_page # noqa: PLC0415 + from panel_live_server.pages import admin_page # noqa: PLC0415 + from panel_live_server.pages import feed_page # noqa: PLC0415 + from panel_live_server.pages import view_page # noqa: PLC0415 # Initialize the database _ = get_db() diff --git a/src/panel_live_server/cli.py b/src/panel_live_server/cli.py index c49bd45..665343c 100644 --- a/src/panel_live_server/cli.py +++ b/src/panel_live_server/cli.py @@ -118,8 +118,9 @@ 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 + # config stays local: ~53 ms, only commands that touch it need to pay for it. + from panel_live_server.config import default_panel_port # noqa: PLC0415 + from panel_live_server.config import reset_config # noqa: PLC0415 if port is None: port = default_panel_port() @@ -133,13 +134,14 @@ 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 + # app stays local: ~487 ms, only `pls serve` needs it. + 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 + import requests # noqa: PLC0415 if exc.errno != errno.EADDRINUSE: raise @@ -212,7 +214,7 @@ def mcp( 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 + 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,9 +252,10 @@ def status( Queries the health endpoint and reports the server status. """ - import requests + # requests and config stay local: ~34 ms and ~53 ms, only `pls status` needs them. + import requests # noqa: PLC0415 - from panel_live_server.config import default_panel_port + from panel_live_server.config import default_panel_port # noqa: PLC0415 if port is None: port = default_panel_port() @@ -459,7 +462,8 @@ 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 + # screenshot stays local: ~67 ms, only this command needs it. + from panel_live_server.screenshot import install_browser as _install_browser # noqa: PLC0415 typer.echo("Installing Chromium for the screenshot tool (one-time)...") code = _install_browser() diff --git a/src/panel_live_server/prompts.py b/src/panel_live_server/prompts.py index ef9f4d9..0ba8038 100644 --- a/src/panel_live_server/prompts.py +++ b/src/panel_live_server/prompts.py @@ -123,8 +123,9 @@ 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 + # jinja2 stays local: ~26 ms, only commands that render instructions need it. + from jinja2 import FileSystemLoader # noqa: PLC0415 + from jinja2.sandbox import SandboxedEnvironment # noqa: PLC0415 # Sandboxed because the override text is user-supplied config, not app code. return SandboxedEnvironment(loader=FileSystemLoader(str(_BUILTIN_DIR)), keep_trailing_newline=False) @@ -132,7 +133,7 @@ def _build_environment(): def _blocks_in(template: str) -> list[str]: """Return the block names declared by one shipped template.""" - from jinja2 import nodes + from jinja2 import nodes # noqa: PLC0415 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..0eac9fb 100644 --- a/src/panel_live_server/screenshot.py +++ b/src/panel_live_server/screenshot.py @@ -97,7 +97,8 @@ def is_browser_installed() -> bool: directly inside a running event loop. """ try: - from playwright.sync_api import sync_playwright + # playwright is an optional dependency; caught below if not installed. + from playwright.sync_api import sync_playwright # noqa: PLC0415 except ImportError: return False try: @@ -402,7 +403,8 @@ async def _ensure_browser(self): return self._browser try: - from playwright.async_api import async_playwright + # playwright is an optional dependency; caught below if not installed. + 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..9396f57 100644 --- a/src/panel_live_server/utils.py +++ b/src/panel_live_server/utils.py @@ -260,8 +260,8 @@ 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 + # panel stays local: it's heavy, and most importers of this module never call this. + from panel.io.mime_render import find_requirements as panel_find_requirements # noqa: PLC0415 return panel_find_requirements(code) except (ImportError, AttributeError): @@ -419,8 +419,9 @@ 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 + # bokeh and panel stay local: heavy, and most importers of this module never call this. + from bokeh.document import Document # noqa: PLC0415 + from panel.io.state import set_curdoc # noqa: PLC0415 return set_curdoc(Document()) From 21d29a5130ab37809dcce38d4ff87681775b9df4 Mon Sep 17 00:00:00 2001 From: SuMayaBee Date: Thu, 20 Aug 2026 04:23:54 +0600 Subject: [PATCH 2/2] chore: refactored the noqa --- src/panel_live_server/app.py | 16 +++++++--------- src/panel_live_server/cli.py | 23 ++++++----------------- src/panel_live_server/prompts.py | 10 ++++------ src/panel_live_server/screenshot.py | 8 ++++++-- src/panel_live_server/utils.py | 8 ++++++-- 5 files changed, 29 insertions(+), 36 deletions(-) diff --git a/src/panel_live_server/app.py b/src/panel_live_server/app.py index 80d890e..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. + # 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 # noqa: PLC0415 - from panel_live_server.pages import admin_page # noqa: PLC0415 - from panel_live_server.pages import feed_page # noqa: PLC0415 - from panel_live_server.pages import view_page # noqa: PLC0415 + 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 665343c..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,10 +122,6 @@ def serve( else: logging.basicConfig(level=logging.INFO) - # config stays local: ~53 ms, only commands that touch it need to pay for it. - from panel_live_server.config import default_panel_port # noqa: PLC0415 - from panel_live_server.config import reset_config # noqa: PLC0415 - if port is None: port = default_panel_port() @@ -134,15 +134,12 @@ def serve( # Reset the cached config singleton so it re-reads the env vars we just set reset_config() - # app stays local: ~487 ms, only `pls serve` needs it. + # 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 # noqa: PLC0415 - if exc.errno != errno.EADDRINUSE: raise url = f"http://{host}:{port}/api/health" @@ -213,7 +210,7 @@ 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. + # 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. @@ -252,11 +249,6 @@ def status( Queries the health endpoint and reports the server status. """ - # requests and config stay local: ~34 ms and ~53 ms, only `pls status` needs them. - import requests # noqa: PLC0415 - - from panel_live_server.config import default_panel_port # noqa: PLC0415 - if port is None: port = default_panel_port() @@ -462,9 +454,6 @@ def install_browser() -> None: installing (pixi users get it via `pixi run postinstall`). It lands in the same environment that runs `pls`. """ - # screenshot stays local: ~67 ms, only this command needs it. - from panel_live_server.screenshot import install_browser as _install_browser # noqa: PLC0415 - 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 0ba8038..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,18 +127,12 @@ def _load_overrides() -> dict[str, tuple[str, str]]: def _build_environment(): """Build the Jinja environment that loads the shipped templates.""" - # jinja2 stays local: ~26 ms, only commands that render instructions need it. - from jinja2 import FileSystemLoader # noqa: PLC0415 - from jinja2.sandbox import SandboxedEnvironment # noqa: PLC0415 - # 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 # noqa: PLC0415 - 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 0eac9fb..25a815a 100644 --- a/src/panel_live_server/screenshot.py +++ b/src/panel_live_server/screenshot.py @@ -97,7 +97,9 @@ def is_browser_installed() -> bool: directly inside a running event loop. """ try: - # playwright is an optional dependency; caught below if not installed. + # 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 @@ -403,7 +405,9 @@ async def _ensure_browser(self): return self._browser try: - # playwright is an optional dependency; caught below if not installed. + # 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 9396f57..a5cb748 100644 --- a/src/panel_live_server/utils.py +++ b/src/panel_live_server/utils.py @@ -260,7 +260,9 @@ def find_requirements(code: str) -> list[str]: List of required package names """ try: - # panel stays local: it's heavy, and most importers of this module never call this. + # 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) @@ -419,7 +421,9 @@ def _isolated_curdoc(): ``session_context``, which is the condition ``.servable()`` requires before it writes anything, so it quietly becomes a no-op here. """ - # bokeh and panel stay local: heavy, and most importers of this module never call this. + # 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