How to make ob-python and UV work together
It is been a while! I have been very busy between work and family, but I never stopped using Emacs.
So for work I ended up moving from Clojure to Scala to now Python. Since uv is now the winning project manager for Python, I have been using in my side projects.
As always I like to experiment via org babel and one of the things that make me satisfied is to try a new library on the fly in an Org Mode block.
I didn't seem to find that feature in ob-python.el (nor did I find in ob-clojure to be honest) so I just built it on the fly.
So uv
comes with scripts that can load dependencies, and this is the org block I wanted to run:
#+begin_src python :uv nil :results output # /// script # dependencies = [ # "pydantic", # "hypothesis-jsonschema", # ] # /// from hypothesis import given from hypothesis_jsonschema import from_schema from pydantic import BaseModel class Item(BaseModel): name: str qty: int = 1 schema = Item.model_json_schema() strategy = from_schema(schema) @given(strategy) def test_item(data): item = Item.model_validate(data) assert item.qty >= 0 # B. Manual strategy for fields (more control) from hypothesis import given, strategies as st from pydantic import BaseModel class Item(BaseModel): name: str qty: int name_s = st.text(min_size=1, max_size=50) qty_s = st.integers(min_value=0, max_value=1000) @given(st.builds(Item, name=name_s, qty=qty_s)) def test_item_manual(item): print(f"---- {item.qty}") assert item.qty >= 0 print(qty_s.example()) test_item_manual() #+end_src
As you can see here I am learning about pydantic and hypothesis-jsonschema, which together allow you to do property based testing in Python.
Below the code to make ob-python.el digest that block:
(add-to-list 'org-babel-header-args:python '(uv . :any) ) (defcustom org-babel-python-uv-default-dependencies '() "Default packages to add when running Python via uv. These are appended to dependencies detected from the block body (uv script header) and any provided explicitly to :uv." :package-version '(Org . "9.7") :group 'org-babel :type '(repeat string)) (defun org-babel-python--uv-deps-from-body (body) "Parse uv script header in BODY and return a list of dependencies. Looks for a header starting with =# /// script= and ending at a line =# ///=, and collects all quoted strings within." (let ((deps nil)) (save-match-data (when (string-match "^#\\s-*///\\s-*script" body) (let* ((start (match-end 0)) (end (or (string-match "^#\\s-*///\\s-*$" body start) (string-match "^#\\s-*///" body start) (length body))) (header (substring body start end)) (pos 0)) (while (string-match "\\(['\"]\\)\\([^\"']+\\)\\1" header pos) (push (match-string 2 header) deps) (setq pos (match-end 0)))))) (nreverse (delete-dups deps)))) (defun org-babel-python--uv-collect-deps (uv-param body) "Return list of dependencies for :uv header UV-PARAM and BODY." (let* ((explicit (cond ((listp uv-param) uv-param) (t nil))) (deps (append explicit (org-babel-python--uv-deps-from-body body) org-babel-python-uv-default-dependencies))) (message "hey-- %s " deps) (delete-dups deps))) (defun org-babel-python--build-uv-command (deps) "Return an =uv run= command string using DEPS." (message "hey123-- %s " (concat "uv run " (mapconcat (lambda (d) (concat "--with " (shell-quote-argument d))) deps " ") (when deps " ") "python")) (concat "uv run " (mapconcat (lambda (d) (concat "--with " (shell-quote-argument d))) deps " ") (when deps " ") "python")) (defun org-babel-execute:python (body params) "Execute Python BODY according to PARAMS. This function is called by =org-babel-execute-src-block'." (let* ((uv-param (cdr (assq :uv params))) (uv-cmd (when uv-param (org-babel-python--build-uv-command (org-babel-python--uv-collect-deps uv-param body)))) (org-babel-python-command (or uv-cmd (cdr (assq :python params)) org-babel-python-command)) (session (org-babel-python-initiate-session (cdr (assq :session params)))) (graphics-file (and (member "graphics" (assq :result-params params)) (org-babel-graphical-output-file params))) (result-params (cdr (assq :result-params params))) (result-type (cdr (assq :result-type params))) (return-val (when (eq result-type 'value) (cdr (assq :return params)))) (preamble (cdr (assq :preamble params))) (async (org-babel-comint-use-async params)) (full-body (concat (org-babel-expand-body:generic body params (org-babel-variable-assignments:python params)) (when return-val (format (if session "\n%s" "\nreturn %s") return-val)))) (result (org-babel-python-evaluate session full-body result-type result-params preamble async graphics-file))) (org-babel-reassemble-table result (org-babel-pick-name (cdr (assq :colname-names params)) (cdr (assq :colnames params))) (org-babel-pick-name (cdr (assq :rowname-names params)) (cdr (assq :rownames params))))))
The idea is to extract the dependencies from the body of the block
with a regex and run uv run --with <dep> --with <dep1> ... python
,
and reuse ob-python functionality for the rest.
I didn't test it much but it works.
Hopefully makes it easier and inspires others to try things out in Python.
Happy hacking!