astral-sh / uv

An extremely fast Python package and project manager, written in Rust.
https://docs.astral.sh/uv
Apache License 2.0
26.84k stars 778 forks source link

Running unit tests with pytest in scripts #9061

Open wrdls opened 1 week ago

wrdls commented 1 week ago

I use uv to run scripts with inline dependencies.

Sometimes I have a need to add a unit test to scripts. Usually my use case is something like the below example where the normalize() function starts off simple and grows in complexity over time (e.g. to handle edge cases) and I want to test these changes.

my_script.py

def normalize(paths):
  return [p.rstrip("/") for p in paths]

def test_normalize():
  assert ["foo", "bar"] == normalize(["foo/", "bar"])

If the scripts keeps growing in size, it probably makes sense to convert this to a project, but at least initially this feels like overkill.

Before uv, I'd execute these like this with the dependencies installed system wide or in a venv:

python my_script.py  # run the script
pytest my_script.py  # test the script

What would be the best way to approach this in uv? I feel like I'm missing something obvious, so please let me know if this is the case.

Ideally I'd be able to run it like

uv run my_script.py   # run the script with uv
uv run pytest my_script.py  # test the script with uv??

Another option I thought of was converting the inline dependencies to a venv (e.g., uv venv --from-script my_script.py). This adds steps and complexity for testing, but avoids it for runtime.

dylwil3 commented 1 week ago

Yep, both of those should work! Let me know if either does not. If you don't want to install pytest in your project, you can also use uvx pytest my_script.py

zanieb commented 1 week ago

uv run pytest my_script.py won't work because we won't read the PEP 723 metadata anymore. Nor will uvx. I'm not sure we have a good solution yet. I think we need a new uv run option.

dylwil3 commented 1 week ago

Edit: Ignore this, see OP's comment below. (My examples worked with pytest because I didn't actually import the dependency.)


Really? Both of those worked for me (but I'm doing something shady somewhere...?):

➜  uvexamples uv init inline
Initialized project `inline` at `/Users/dmbp/Documents/dev/uvexamples/inline`
➜  uvexamples cd inline
➜  inline git:(main) ✗ hx my_script.py

(copied and pasted OP's script)

➜  inline git:(main) ✗ uv add --script my_script.py "rich"
Updated `my_script.py`
➜  inline git:(main) ✗ uv run my_script.py
Reading inline script metadata from `my_script.py`
⠙ Resolving dependencies...                                                             INFO add_decision: root @ 0a0.dev0 without checking dependencies
⠙ rich==13.9.4                                                                          INFO add_decision: rich @ 13.9.4 without checking dependencies
⠙ markdown-it-py==3.0.0                                                                 INFO add_decision: markdown-it-py @ 3.0.0 without checking dependencies
⠙ pygments==2.18.0                                                                      INFO add_decision: pygments @ 2.18.0 without checking dependencies
⠙ mdurl==0.1.2                                                                          INFO add_decision: mdurl @ 0.1.2 without checking dependencies
Installed 4 packages in 10ms
➜  inline git:(main) ✗ uvx pytest my_script.py
⠙ Resolving dependencies...                                                             INFO add_decision: root @ 0a0.dev0 without checking dependencies
⠙ pytest==8.3.3                                                                         INFO add_decision: pytest @ 8.3.3 without checking dependencies
⠙ iniconfig==2.0.0                                                                      INFO add_decision: iniconfig @ 2.0.0 without checking dependencies
⠙ packaging==24.2                                                                       INFO add_decision: packaging @ 24.2 without checking dependencies
⠙ pluggy==1.5.0                                                                         INFO add_decision: pluggy @ 1.5.0 without checking dependencies
================================= test session starts ==================================
platform darwin -- Python 3.11.5, pytest-8.3.3, pluggy-1.5.0
rootdir: /Users/dmbp/Documents/dev/uvexamples/inline
configfile: pyproject.toml
collected 1 item

my_script.py .                                                                   [100%]

================================== 1 passed in 0.00s ===================================
➜  inline git:(main) ✗
wrdls commented 1 week ago

This doesn't work for me with the following script.

# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "rich",
# ]
# ///
import rich

def normalize(paths):
  return [p.rstrip("/") for p in paths]

def test_normalize():
  assert ["foo", "bar"] == normalize(["foo/", "bar"])

if __name__ == "__main__":
  rich.print(normalize(["foo/"]))

Testing in Docker to get a clean environment:

❯ docker run --rm -it --entrypoint=bash python:3
root@b441346070f6:/# apt update && apt install -y vim
...
root@b441346070f6:/# pip install uv
Collecting uv
  Downloading uv-0.5.1-py3-none-manylinux_2_28_aarch64.whl.metadata (11 kB)
Downloading uv-0.5.1-py3-none-manylinux_2_28_aarch64.whl (13.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 13.0/13.0 MB 17.7 MB/s eta 0:00:00
Installing collected packages: uv
Successfully installed uv-0.5.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable.It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.

[notice] A new release of pip is available: 24.2 -> 24.3.1
[notice] To update, run: pip install --upgrade pip
root@b441346070f6:/# vim my_script.py
root@b441346070f6:/# uv run my_script.py
Reading inline script metadata from `my_script.py`
Installed 4 packages in 6ms
['foo']
root@b441346070f6:/# uvx pytest my_script.py
Installed 4 packages in 5ms
========================================================================================== test session starts ===========================================================================================
platform linux -- Python 3.13.0, pytest-8.3.3, pluggy-1.5.0
rootdir: /
collected 0 items / 1 error                                                                                                                                                                              

================================================================================================= ERRORS =================================================================================================
_____________________________________________________________________________________ ERROR collecting my_script.py ______________________________________________________________________________________
ImportError while importing test module '/my_script.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
usr/local/lib/python3.13/importlib/__init__.py:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
my_script.py:7: in <module>
    import rich
E   ModuleNotFoundError: No module named 'rich'
======================================================================================== short test summary info =========================================================================================
ERROR my_script.py
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
============================================================================================ 1 error in 0.06s ============================================================================================
root@b441346070f6:/# 
dylwil3 commented 1 week ago

Ah, thank you! That was helpful - I guess I didn't try actually importing the dependency in the previous example.

If there are only a few dependencies then you could get around it for now with uvx --with rich pytest my_script.py (but that defeats the purpose of inline dependencies...)

zanieb commented 1 week ago

Related

zanieb commented 1 week ago

We've also discussed like --with-requirements <script-path>