📊

markdown preview enhancedのコードチャンクでuvを利用する

に公開

TL;DR

末尾のコード(run_with_uv.py)を同一階層において、

```python {cmd=python args=["run_with_uv.py", "numpy"]}

で囲い、実行する

実行例

v3.13でnumpyを利用したコードを実行する

# python {cmd=python args=["run_with_uv.py", "numpy","version=3.13", "$input_file"]}
import numpy as np
print(np.array([1,1]))
Installed 1 package in 149ms
[1 1]

numpyを利用したコードを実行する

# python {cmd=python args=["run_with_uv.py", "numpy", "$input_file"]}
import numpy as np
print(np.array([1,1]))
Downloading numpy (17.5MiB)
 Downloading numpy
Installed 1 package in 72ms
[1 1]

pandasを利用したコードを実行する

# python {cmd=python args=["run_with_uv.py", "pandas","version=3.13", "$input_file"]}
import pandas as pd
import numpy as np
print(np.array([1,1]))
Installed 6 packages in 136ms
[1 1]

特に何も指定しないとそのまま実行される

# python {cmd=python args=["run_with_uv.py", "$input_file"]}
print([1,1])
[1, 1]

ファイル名も指定しなくてOK

# python {cmd=python args=["run_with_uv.py"]}
print([1,1])
[1, 1]

バージョンの変更もできる-その1

# python {cmd=python args=["run_with_uv.py", "version=3.11"]}
import sys
print(sys.version)
3.11.12 (main, Apr  9 2025, 03:55:30) [Clang 14.0.3 ]

バージョンの変更もできる-その2

# python {cmd=python args=["run_with_uv.py", "v=3.13"]}
import sys
print(sys.version)
3.13.3 (main, Apr  9 2025, 03:55:34) [Clang 14.0.3 ]

やっていること

markdown preview enhancedのコードチャンクは下記のコードで実行されている。

https://github.com/shd101wyy/crossnote/blob/develop/src/code-chunk/code-chunk.ts

特定のコマンドがパースされて、実行される。ホストOSのuvを利用したいと思った場合にはいくつかの方法があるが、ここではpythonのsubprocessを介して実行することにした。

依存関係に関してはpythonの実行時に渡される引数をパースして、スクリプト内に書き込み、uvのPEP723に準拠した、ファイル内での依存関係を解決して実行できる機能を利用する。

script.py
# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "numpy",
# ]
# ///
import numpy as np
...

uv runを実行して、スクリプト内の依存関係を動的に解決しつつ実行する。

def run_script(script_path):
    """Run the script with uv and return the result."""
    try:
        return subprocess.run(
            ["uv", "run", script_path], capture_output=True, text=True
        )

実行例

例えば

python {cmd=python args=["run_with_uv.py", "pandas","version=3.13", "$input_file"]}

と書くことで実行するスクリプトの上部に

# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "pandas",
# ]
# ///

が記載され、uv runが実行される。

以上

付録:コード

run_with_uv.py
#!/usr/bin/env python3
# Save this as run_with_uv.py in your project directory
# Usage patterns:
# With dependency:       {cmd=python args=["run_with_uv.py", "numpy", "version=3.13", "$input_file"]}
# Multiple dependencies: {cmd=python args=["run_with_uv.py", "numpy,pandas", "version=3.13", "$input_file"]}
# No version:            {cmd=python args=["run_with_uv.py", "numpy", "$input_file"]}
# No deps or version:    {cmd=python args=["run_with_uv.py", "$input_file"]}
# Simplest form:         {cmd=python args=["run_with_uv.py"]}

import os
import re
import subprocess
import sys
import tempfile

default_python_version = "3.11"  # Default Python version


def parse_arguments():
    """Parse command line arguments and return dependencies, Python version, and input file path."""
    dependencies = []
    python_version = None  # Default Python version
    input_file = None

    # Parse arguments
    for arg in sys.argv[1:]:
        if arg.startswith("version=") or arg.startswith("v="):
            # Extract version from version=X.Y format
            version_match = re.search(r"[version|v]=(\d+\.\d+)", arg)
            if version_match:
                python_version = version_match.group(1)
        elif "code_chunk" in arg:
            # This is likely the input file
            input_file = arg
        elif not arg.startswith("version=") and not arg.startswith("v="):
            # Assume this is a dependency list if it's not a version or input file
            if "," in arg:
                # Multiple dependencies
                dependencies = [dep.strip() for dep in arg.split(",") if dep.strip()]
            else:
                # Single dependency
                dependencies = [arg.strip()]

    # If no input file was found in args, try to find it in the current directory
    if input_file is None:
        # Look for code_chunk files in the current directory
        import glob

        code_chunks = glob.glob("*_code_chunk.*")
        if code_chunks:
            # Sort by creation time, newest first
            code_chunks.sort(key=os.path.getctime, reverse=True)
            input_file = code_chunks[0]

    return dependencies, python_version, input_file


def read_input_file(input_file):
    """Read content from the input file."""
    if not input_file:
        print(
            "Error: No input file found. Make sure to include $input_file in your arguments "
            "or run this in a directory with code_chunk files.",
            file=sys.stderr,
        )
        return None

    if not os.path.exists(input_file):
        print(f"Error: Input file '{input_file}' not found.", file=sys.stderr)
        return None

    try:
        with open(input_file, "r") as f:
            return f.read()
    except Exception as e:
        print(f"Error reading input file: {e}", file=sys.stderr)
        return None


def create_temp_script(original_content, dependencies, python_version):
    """Create a temporary script file with the appropriate headers."""
    with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as temp_file:
        temp_path = temp_file.name

        # Write script headers
        temp_file.write("# /// script\n")
        if python_version is None:
            temp_file.write(f'# requires-python = ">={default_python_version}"\n')
        else:
            temp_file.write(f'# requires-python = "=={python_version}.*"\n')

        # Add dependencies if specified
        if dependencies:
            temp_file.write("# dependencies = [\n")
            for dep in dependencies:
                temp_file.write(f'#   "{dep}",\n')
            temp_file.write("# ]\n")

        temp_file.write("# ///\n")

        # Write the original content
        temp_file.write(original_content)

    return temp_path


def run_script(script_path):
    """Run the script with uv and return the result."""
    try:
        return subprocess.run(
            ["uv", "run", script_path], capture_output=True, text=True
        )
    except Exception as e:
        print(f"Error running script: {e}", file=sys.stderr)
        return None


def filter_output(output):
    """Filter installation messages from the output."""
    filtered_lines = []
    for line in output.splitlines():
        # Skip installation messages
        if not line.strip() or "Installed" in line and "packages" in line:
            continue
        filtered_lines.append(line)

    return "\n".join(filtered_lines)


def main():
    """Main function to orchestrate the process."""
    # Parse command line arguments
    dependencies, python_version, input_file = parse_arguments()

    # Read input file content
    original_content = read_input_file(input_file)
    if original_content is None:
        return 1

    # Create temporary script file
    temp_path = create_temp_script(original_content, dependencies, python_version)

    try:
        # Run script
        result = run_script(temp_path)
        if result is None:
            return 1
        # Always print errors
        if result.stderr:
            print(result.stderr, end="", file=sys.stderr)

        # Filter and print output
        print(result.stdout.strip())

        return result.returncode
    finally:
        # Clean up
        if os.path.exists(temp_path):
            os.remove(temp_path)


if __name__ == "__main__":
    sys.exit(main())

Discussion