diff --git a/README.md b/README.md index 3908794..c80d582 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ pathview_server/ # Python package (pip install pathview) ├── cli.py # CLI entry point (pathview serve) └── static/ # Bundled frontend (generated at build time) + scripts/ ├── config/ # Configuration files for extraction │ ├── schemas/ # JSON schemas for validation @@ -496,8 +497,9 @@ npm run dev # Starts Vite dev server (separate terminal) **Key properties:** - **Process isolation** — each session gets its own Python subprocess +- **Host environment** — workers run with the same Python used to install pathview, so all packages in the user's environment are available in the code editor - **Namespace persistence** — variables persist across exec/eval calls within a session -- **Dynamic packages** — packages from `PYTHON_PACKAGES` (the same config used by Pyodide) are pip-installed on first init +- **Dynamic packages** — packages from `PYTHON_PACKAGES` (the same config used by Pyodide) are pip-installed on first init if not already present - **Session TTL** — stale sessions cleaned up after 1 hour of inactivity - **Streaming** — simulations stream via SSE, with the same code injection support as Pyodide diff --git a/pathview_server/app.py b/pathview_server/app.py index 5bf56ba..7a2fbda 100644 --- a/pathview_server/app.py +++ b/pathview_server/app.py @@ -21,8 +21,6 @@ from flask import Flask, request, jsonify, send_from_directory from flask_cors import CORS -from pathview_server.venv import get_venv_python - # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- @@ -44,7 +42,7 @@ def __init__(self, session_id: str): self.last_active = time.time() self.lock = threading.Lock() self.process = subprocess.Popen( - [get_venv_python(), "-u", WORKER_SCRIPT], + [sys.executable, "-u", WORKER_SCRIPT], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/pathview_server/cli.py b/pathview_server/cli.py index e82b7d1..fa81466 100644 --- a/pathview_server/cli.py +++ b/pathview_server/cli.py @@ -7,7 +7,6 @@ import webbrowser from pathview_server import __version__ -from pathview_server.venv import VENV_DIR, ensure_venv def main(): @@ -30,8 +29,6 @@ def main(): args = parser.parse_args() - ensure_venv() - from pathview_server.app import create_app app = create_app(serve_static=not args.debug) @@ -52,7 +49,7 @@ def open_browser_when_ready(): threading.Thread(target=open_browser_when_ready, daemon=True).start() print(f"PathView v{__version__}") - print(f" Python venv: {VENV_DIR}") + print(f" Python: {sys.executable}") print(f"Running at http://{args.host}:{args.port}") if args.host == "0.0.0.0": diff --git a/pathview_server/venv.py b/pathview_server/venv.py deleted file mode 100644 index df8e28a..0000000 --- a/pathview_server/venv.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Virtual environment management for PathView worker subprocesses. - -Creates and manages a dedicated venv at ~/.pathview/venv so that simulation -dependencies (pathsim, pathsim-chem, numpy, etc.) are installed in isolation -rather than polluting the user's global/active environment. -""" - -import subprocess -import sys -from pathlib import Path - -VENV_DIR = Path.home() / ".pathview" / "venv" - - -def get_venv_python() -> str: - """Return path to the venv's Python executable.""" - if sys.platform == "win32": - return str(VENV_DIR / "Scripts" / "python.exe") - return str(VENV_DIR / "bin" / "python") - - -def ensure_venv() -> str: - """Create the venv if it doesn't exist. Returns venv Python path.""" - python = get_venv_python() - if Path(python).exists(): - return python - - print("Creating PathView virtual environment...") - VENV_DIR.parent.mkdir(parents=True, exist_ok=True) - subprocess.run([sys.executable, "-m", "venv", str(VENV_DIR)], check=True) - # Upgrade pip in the venv - subprocess.run([python, "-m", "pip", "install", "--upgrade", "pip", "--quiet"], check=True) - print(f" Virtual environment created at {VENV_DIR}") - return python diff --git a/pathview_server/worker.py b/pathview_server/worker.py index 2546e74..ec9fbf8 100644 --- a/pathview_server/worker.py +++ b/pathview_server/worker.py @@ -160,7 +160,6 @@ def initialize(packages: list[dict] | None = None) -> None: send({"type": "stderr", "value": f"Optional package {pkg.get('import', '?')} failed: {e}\n"}) # Import numpy AFTER packages are installed (numpy comes with pathsim). - # In a fresh venv without simulation packages, numpy won't be available. try: exec("import numpy as np", _namespace) except Exception: diff --git a/pyproject.toml b/pyproject.toml index 23493c9..77d2293 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,8 @@ dependencies = [ "flask-cors>=4.0", "numpy", "waitress>=3.0", + "pathsim", + "pathsim-chem==0.2rc3", ] [project.optional-dependencies] diff --git a/scripts/extract.py b/scripts/extract.py index 45360d5..f486d2c 100644 --- a/scripts/extract.py +++ b/scripts/extract.py @@ -694,6 +694,10 @@ def extract(self) -> dict: class DependencyExtractor: """Extract and generate dependency configuration.""" + # Packages managed by the extraction script in pyproject.toml. + # These are added/updated automatically; all other dependencies are left untouched. + MANAGED_PACKAGE_NAMES = {"pathsim", "pathsim-chem"} + def __init__(self, config_loader: ConfigLoader): self.config = config_loader @@ -735,6 +739,59 @@ def extract(self) -> dict: "extracted_versions": extracted_versions } + def update_pyproject(self, packages: list[dict], project_root: Path) -> None: + """Update pyproject.toml dependencies with pinned package versions. + + Adds/updates entries for packages from requirements-pyodide.txt + while leaving all other dependencies (flask, numpy, etc.) untouched. + """ + pyproject_path = project_root / "pyproject.toml" + if not pyproject_path.exists(): + print(" Warning: pyproject.toml not found, skipping") + return + + text = pyproject_path.read_text(encoding="utf-8") + + # Build pip specs for managed packages (e.g. "pathsim==0.17.0") + new_specs: list[str] = [] + for pkg in packages: + base_name = pkg["pip"].split(">=")[0].split("==")[0].split("<=")[0].split("<")[0].split(">")[0] + if base_name in self.MANAGED_PACKAGE_NAMES: + new_specs.append(pkg["pip"]) + + # Parse the existing dependencies list + pattern = re.compile( + r'(dependencies\s*=\s*\[)(.*?)(\])', + re.DOTALL, + ) + match = pattern.search(text) + if not match: + print(" Warning: Could not find dependencies in pyproject.toml, skipping") + return + + existing_block = match.group(2) + + # Keep non-managed dependencies + kept: list[str] = [] + for line in existing_block.strip().splitlines(): + dep = line.strip().strip(",").strip('"').strip("'") + if not dep: + continue + dep_name = re.split(r'[><=~!\[]', dep)[0].strip() + if dep_name not in self.MANAGED_PACKAGE_NAMES: + kept.append(dep) + + # Append managed packages + all_deps = kept + new_specs + + # Rebuild the dependencies block + dep_lines = ",\n".join(f' "{d}"' for d in all_deps) + new_block = f"dependencies = [\n{dep_lines},\n]" + text = text[:match.start()] + new_block + text[match.end():] + + pyproject_path.write_text(text, encoding="utf-8") + print(f" Updated pyproject.toml dependencies: {', '.join(new_specs)}") + # ============================================================================= # TypeScript Generation @@ -1009,8 +1066,11 @@ def main(): if extract_all or args.deps: print("\nExtracting dependencies...") - deps = DependencyExtractor(config).extract() + dep_extractor = DependencyExtractor(config) + deps = dep_extractor.extract() generator.write_dependencies(deps) + project_root = Path(__file__).parent.parent + dep_extractor.update_pyproject(deps["packages"], project_root) # Track extracted data for registry generation blocks = None diff --git a/src/lib/constants/dependencies.ts b/src/lib/constants/dependencies.ts index 7a0e1fb..c168ab5 100644 --- a/src/lib/constants/dependencies.ts +++ b/src/lib/constants/dependencies.ts @@ -1,7 +1,7 @@ // Auto-generated by scripts/extract.py - DO NOT EDIT // Source: scripts/config/requirements-pyodide.txt, scripts/config/pyodide.json -export const PATHVIEW_VERSION = '0.6.1'; +export const PATHVIEW_VERSION = '0.7.0'; export const PYODIDE_VERSION = '0.26.2'; export const PYODIDE_CDN_URL = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/pyodide.mjs`; diff --git a/tests/conftest.py b/tests/conftest.py index d3ceb8f..e5a0b75 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,13 +3,11 @@ import pytest from pathview_server.app import create_app, _sessions, _sessions_lock -from pathview_server.venv import ensure_venv @pytest.fixture() def app(): """Create a Flask test app (API-only, no static serving).""" - ensure_venv() application = create_app(serve_static=False) application.config["TESTING"] = True yield application