Repository: koaning/uvtrick (language: python)

Issue:
Snapshot testing, type hints, refactoring
I had a go at refactoring this and making the testing a bit more robust, including the examples from the README as tests and using inline-snapshot so that the exact same results are ensured. The refactored the `run` method reads more clearly now.

I initially split it out into separate modules to understand it but then put them back into one as they're all quite closely related :-) I put `maincall` on the `Env` since it's directly related, and used OOP references to `self.temp_dir` rather than repeatedly re-creating pathlib-type `Path` objects from it.

I also changed to use `Pathlib.read_text()`/`read_bytes()` as the code often had pathlib types to hand but used `open`.

I also figured out how to get the `uv` subprocess calls to run without `shell=True`, my understanding is it's preferable to use lists of command args rather than a string.

In the test suite I used pytest's `importorskip` to simply import standalone examples from the examples subfolder (which came from the examples in the README). This 'trick' of importing modules that aren't in the package requires the repo's root dir to be on the PATH, which PDM package manager does automatically. I noticed uv doesn't do this, so I added a 2 liner to `conftest.py` which will ensure all runs of the test suite do this.

There were some trivial bugs I caught:
- the `argskwargs_to_callstring` funcdef was not used, so I put it in an `unused` dir, it can probably be deleted (presume it was from early development)
- `quiet = "--quiet" if not self.debug else ""` was set but then in the `if self.debug:` the command was used with `--quiet` (ignoring the `quiet` variable and also the opposite logic) in `Env.run()`. I figured that if you're debugging you do want verbose output, i.e. the ternary variable was right.

Plus added some missing docstrings on `maincall` and completed the comments documenting `Env.run()`

I think that's about it, happy to explain anything else I forgot to mention :)
Tidy test suite
This PR tidies up the test suite, using a separate subfolder for non-test modules and standard pytest module naming conventions (broken out of previous PR #9 as requested)
Tidy test suite
This PR tidies up the test suite, using a separate subfolder for non-test modules and standard pytest module naming conventions (broken out of previous PR #9 as requested)


Discussion / hints:
For future work it would be better to first open an issue to confirm the work that you're planning on doing. That makes it much easier for me to confirm if I agree with the direction and it might also be nice to be able to make smaller PRs. 21 files changes is a tad bit intimidating.

Will figure out some time to have a look.  
It also seems like CI is not green. 

I think there are some valid ideas in this PR, but would it be possible to make smaller PRs? Mainly to make the review work easier on my side? 
Yeah oh no prob at all, this was initially just to get my head around it and then got carried away :) 

**Edit** CI was failing due to `demo.py` being refactored into individual examples, I put it back as was and expect CI should pass now, tests all passing on my end
To be clear, if you are willing to split up some of these PRs I will gladly accept them. But yeah ... 21 file changes are intimidating 😅 



Fix patch:
diff --git a/README.md b/README.md
index 70c27f1..40ceef5 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,10 @@ from uvtrick import Env
 # Also note that all the imports happen inside of this function. 
 def uses_rich():
     from rich import print
-    print("hello")
+    from importlib import metadata
+
+    version = metadata.version("rich")
+    print(f"hello from rich=={version}")
 
 # This runs the function `uses_rich` in a new environment with the `rich` package installed.
 # Just like the `load` function before, the result is returned via pickle. 
@@ -59,7 +62,10 @@ from uvtrick import Env
 
 def uses_rich(a, b):
     from rich import print
-    print("hello")
+    from importlib import metadata
+
+    version = metadata.version("rich")
+    print(f"hello from rich=={version}")
     return a + b
 
 for version in (10, 11, 12, 13):
diff --git a/demo.py b/demo.py
index 6865364..e7b7dab 100644
--- a/demo.py
+++ b/demo.py
@@ -11,10 +11,13 @@
 
 def uses_rich(a, b):
     from rich import print
-    print("hello")
+    from importlib import metadata
+
+    version = metadata.version("rich")
+    print(f"hello from rich=={version}")
     return a + b
 
 # This runs the function `uses_rich` in a new environment with the `rich` package installed.
 # Just like the `load` function, the result is returned via pickle. 
 for version in (10, 11, 12, 13):
-    Env(f"rich=={version}", python="3.12").run(uses_rich, a=1, b=2)
\ No newline at end of file
+    Env(f"rich=={version}", python="3.12").run(uses_rich, a=1, b=2)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..4dc9b78
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,6 @@
+import sys
+from pathlib import Path
+
+# Put root dir on the path so `pytest.importorskip` can run them
+root_dir = Path(__file__).parents[1]
+sys.path.append(str(root_dir))
diff --git a/tests/rich-fail.py b/tests/rich-fail.py
index 47af02f..a9d0926 100644
--- a/tests/rich-fail.py
+++ b/tests/rich-fail.py
@@ -1,4 +1,3 @@
-
 import rich
 
 
diff --git a/tests/some_script.py b/tests/some_script.py
new file mode 100644
index 0000000..7098a69
--- /dev/null
+++ b/tests/some_script.py
@@ -0,0 +1,8 @@
+# /// script
+# dependencies = [
+# ]
+# ///
+
+
+def add(a: int, b: int):
+    return a + b
diff --git a/tests/test_basics.py b/tests/test_basics.py
index 50003e4..53b94be 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -1,6 +1,7 @@
 """When I hacks this bad, I write tests."""
 
 import pytest
+
 from uvtrick import Env, load
 
 hello = load("tests/rich-script.py", "hello")
@@ -15,16 +16,19 @@ def test_args():
     assert add(1, 2) == 3
     assert add(a=1, b=4) == 5
 
+
 def test_no_exist():
     with pytest.raises(ValueError):
         func = load("tests/rich-script.py", "no_exist")
         func()
 
+
 def test_no_metadata():
     with pytest.raises(ValueError):
         func = load("tests/rich-fail.py", "add")
         func()
 
+
 def test_env_works1():
     def uses_rich(a, b):
         from rich import print
@@ -35,10 +39,20 @@ def uses_rich(a, b):
     for version in ["13", "12"]:
         assert Env(f"rich=={version}").run(uses_rich, a=1, b=2) == 3
 
+
 def test_env_works2():
     def handles_all_types(arr, dictionary, string):
         return {"arr": arr, "dictionary": dictionary, "string": string}
 
     for version in ["13", "12"]:
-        out = Env(f"rich=={version}").run(handles_all_types, arr=[1, 2, 3], dictionary={"a": 1, "b": 2}, string="hello") 
-        assert out == {"arr": [1, 2, 3], "dictionary": {"a": 1, "b": 2}, "string": "hello"}
+        out = Env(f"rich=={version}").run(
+            handles_all_types,
+            arr=[1, 2, 3],
+            dictionary={"a": 1, "b": 2},
+            string="hello",
+        )
+        assert out == {
+            "arr": [1, 2, 3],
+            "dictionary": {"a": 1, "b": 2},
+            "string": "hello",
+        }
diff --git a/tests/test_examples.py b/tests/test_examples.py
new file mode 100644
index 0000000..a743d70
--- /dev/null
+++ b/tests/test_examples.py
@@ -0,0 +1,65 @@
+from pathlib import Path
+
+test_assets_dir = Path(__file__).parent
+
+def test_demo():
+    import demo
+
+def test_add():
+    from uvtrick import load
+
+    script = test_assets_dir / "rich-script.py"
+    add = load(script, "add")
+
+    print(f"{add(1, 2)=}")
+    print(f"{add(a=1, b=2)=}")
+
+
+def test_hello():
+    from uvtrick import load
+
+    script = test_assets_dir / "rich-script.py"
+    hello = load(script, "hello")
+    print(f"{hello()=}")
+
+
+def test_rich_versions():
+    from uvtrick import Env
+
+    rich_versions = (10, 11, 12, 13)
+    print(f"Now iterating through {rich_versions=}:")
+
+
+    def uses_rich(a, b):
+        from importlib import metadata
+
+        from rich import print
+
+        version = metadata.version("rich")
+
+        print(f"hello from rich=={version}")
+        return a + b
+
+
+    # This runs the function `uses_rich` in a new environment with the `rich` package installed.
+    # Just like the `load` function, the result is returned via pickle.
+    for version in rich_versions:
+        result = Env(f"rich=={version}", python="3.12").run(uses_rich, a=1, b=2)
+        print(f" --> add(1, 2) = {result}")
+
+
+def test_simple():
+    from uvtrick import load
+
+    # Load the function `add` from the file `some_script.py`
+    # It runs in another virtualenv, but you get back the response via pickle.
+    # Be aware of the limitations, please only consider base Python objects.
+    script = test_assets_dir / "some_script.py"
+    add = load(script, "add")
+
+    # This result is from the `some_script.py` file, running in another virtualenv
+    # with `uv`. A pickle in a temporary file is used to communicate the result.
+    result = add(1, 2)  # 3
+
+    assert result == 3
+    print(result)