Skip to content

Commit

Permalink
Merge dff474a into master
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions[bot] authored May 24, 2021
2 parents e423cc3 + dff474a commit f3d562e
Show file tree
Hide file tree
Showing 12 changed files with 706 additions and 174 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 5.0

- option `--project-dir` aka `-p` is now supported by all commands
- the `vien call -p` format (with the `-p` option after the command) is deprecated but still works

# 4.4

- `call` is now faster as it launches Python directly without spawning an extra
Expand Down
51 changes: 41 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,11 @@ The optional `-p` parameter allows you to specify the project directory
``` bash
$ cd any/where # working dir is irrelevant

# absolute (using venv for /abc/myProject):
$ vien call -p /abc/myProject /abc/myProject/main.py
# both of the following calls consider /abc/myProject
# the project directory

# relative (using venv for /abc/myProject):
$ vien call -p . /abc/myProject/main.py

# error (there is no venv for any/where)
$ vien call /abc/myProject/main.py
$ vien -p /abc/myProject call /abc/myProject/main.py
$ vien -p . call /abc/myProject/main.py
```

This parameter makes things like [shebang](#Shebang) possible.
Expand Down Expand Up @@ -262,6 +259,40 @@ $ cd /path/to/myProject
$ vien recreate /usr/local/opt/python@3.10/bin/python3
```

# Options

## --project-dir, -p

This option must appear after `vien`, but before the command. For example,
``` bash
vien -p some/dir run ...
vien -p other/dir shell ...
```

If `--project-dir` is specified, it is the project directory.

If `--project-dir` is not specified, then all commands assume that the current
working directory is the project directory.

The next two calls use the same project directory and the same virtual environment. However, the working directory is different.

``` bash
cd /abc/myProject
vien run python3 /abc/myProject/main.py
```

``` bash
cd /any/where
vien -p /abc/myProject run python3 /abc/myProject/main.py
```



If `--project-dir` is specified as a **relative path**, its interpretation depends
on the command. For the `call` command, this is considered a path relative to
the parent directory of the `.py` file being run. For other commands, this is
a path relative to the current working directory.

# Virtual environments location

By default, `vien` places virtual environments in the `$HOME/.vien` directory.
Expand Down Expand Up @@ -332,9 +363,9 @@ shebang depends on the location of the file relative to the project directory.

File | Shebang line
--------------------------------|--------------------------------
`myProject/runme.py` | `#!/usr/bin/env vien call -p .`
`myProject/pkg/runme.py` | `#!/usr/bin/env vien call -p ..`
`myProject/pkg/subpkg/runme.py` | `#!/usr/bin/env vien call -p ../..`
`myProject/runme.py` | `#!/usr/bin/env vien -p . call`
`myProject/pkg/runme.py` | `#!/usr/bin/env vien -p .. call`
`myProject/pkg/subpkg/runme.py` | `#!/usr/bin/env vien -p ../.. call`

After inserting the shebang, make the file executable:

Expand Down
134 changes: 128 additions & 6 deletions tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from __future__ import annotations

import json
import os
import sys
import unittest
Expand All @@ -12,7 +13,7 @@
from timeit import default_timer as timer

from vien import main_entry_point
from vien.main import VenvExistsExit, VenvDoesNotExistExit, ChildExit, \
from vien.exceptions import ChildExit, VenvExistsExit, VenvDoesNotExistExit, \
PyFileNotFoundExit
from tests.time_limited import TimeLimited

Expand Down Expand Up @@ -59,6 +60,12 @@ def test_path(self):
main_entry_point(["path"])


# def is_in(inner: Path, outer: Path) -> bool:
# inner_str = str(inner.absolute())
# outer_str = str(outer.absolute())
# return len(inner_str) > len(outer_str) and inner_str.startswith(outer_str)


class TestsInsideTempProjectDir(unittest.TestCase):

def setUp(self):
Expand All @@ -80,6 +87,33 @@ def tearDown(self):
self._td.cleanup()
del os.environ["VIENDIR"]

def assertInVenv(self, inner: Path):
inner_str = str(inner.absolute())
outer_str = str(self.expectedVenvDir.absolute())

# macOS weirdness
inner_str = inner_str.replace("/private/var", "/var")
outer_str = outer_str.replace("/private/var", "/var")

if (os.path.commonpath([outer_str]) != os.path.commonpath(
[outer_str, inner_str])):
# if not (len(inner_str) > len(outer_str)
# and inner_str.startswith(outer_str)):
self.fail(f"{inner_str} is not in {outer_str}")

def assertProjectDirIsNotCwd(self):
basename = "marker"
in_cwd = Path.cwd() / basename
in_project = self.projectDir / basename

self.assertFalse(in_cwd.exists())
self.assertFalse(in_project.exists())

in_cwd.touch()

self.assertTrue(in_cwd.exists())
self.assertFalse(in_project.exists())

def assertVenvDoesNotExist(self):
self.assertFalse(self.expectedVenvDir.exists())
self.assertFalse(self.expectedVenvBin.exists())
Expand All @@ -96,6 +130,18 @@ def assertIsSuccessExit(self, exc: SystemExit):
self.assertIsInstance(exc, SystemExit)
self.assertTrue(exc.code is None or exc.code == 0)

def write_program(self, py_file_path: Path) -> Path:
out_file_path = py_file_path.parent / 'output.json'
code = "import pathlib, sys, json\n" \
"d={'sys.path': sys.path, \n" \
" 'sys.executable': sys.executable}\n" \
"js=json.dumps(d)\n" \
f'(pathlib.Path("{out_file_path}")).write_text(js)'
py_file_path.write_text(code)

assert not out_file_path.exists()
return out_file_path

def test_create_with_argument(self):
self.assertFalse(self.expectedVenvDir.exists())
self.assertFalse(self.expectedVenvBin.exists())
Expand Down Expand Up @@ -171,6 +217,32 @@ def test_run_needs_venv(self):
with self.assertRaises(VenvDoesNotExistExit):
main_entry_point(["run", "python", "--version"])

def test_run_p(self):
"""Checking the -p changes both venv directory and the first item
in PYTHONPATH"""
main_entry_point(["create"])
with TemporaryDirectory() as temp_cwd:
# we will run it NOT from the project dir as CWD
os.chdir(temp_cwd)

# creating .py file to run
code_py = Path(temp_cwd) / "code.py"
output_file = self.write_program(code_py)

# running the code that will create a json file
self.assertProjectDirIsNotCwd()
with self.assertRaises(ChildExit) as ce:
main_entry_point(
["-p", str(self.projectDir.absolute()),
"run", "python3",
str(code_py)])
self.assertEqual(ce.exception.code, 0)

# loading json and checking the values
d = json.loads(output_file.read_text())
self.assertIn(str(self.projectDir.absolute()), d["sys.path"])
self.assertInVenv(Path(d["sys.executable"]))

def test_run_exit_code_0(self):
"""Test that main_entry_point returns the same exit code,
as the called command"""
Expand Down Expand Up @@ -297,10 +369,21 @@ def test_call_project_dir_venv(self):
with TemporaryDirectory() as td:
os.chdir(td)

# without -p we assume that the current dir is the project dir,
# but the current is temp. So we must get an exception
with self.assertRaises(VenvDoesNotExistExit):
main_entry_point(["call", run_py_str])
# NORMAL format

# this call specifies project dir relative to run.py.
# It runs the file successfully
with self.assertRaises(ChildExit) as ce:
main_entry_point(["-p", "..", "call", run_py_str])
self.assertEqual(ce.exception.code, 5)

# this call specifies project dir relative to run.py.
# It runs the file successfully
with self.assertRaises(ChildExit) as ce:
main_entry_point(["--project-dir", "..", "call", run_py_str])
self.assertEqual(ce.exception.code, 5)

# OUTDATED format

# this call specifies project dir relative to run.py.
# It runs the file successfully
Expand All @@ -314,6 +397,13 @@ def test_call_project_dir_venv(self):
main_entry_point(["call", "--project-dir", "..", run_py_str])
self.assertEqual(ce.exception.code, 5)

# ERRORS

# without -p we assume that the current dir is the project dir,
# but the current is temp. So we must get an exception
with self.assertRaises(VenvDoesNotExistExit):
main_entry_point(["call", run_py_str])

# this call specifies *incorrect* project dir relative to run.py.
with self.assertRaises(VenvDoesNotExistExit):
main_entry_point(["call", "--project-dir", "../..", run_py_str])
Expand All @@ -336,9 +426,35 @@ def test_call_project_dir_relative_imports(self):
with TemporaryDirectory() as td:
os.chdir(td)
with self.assertRaises(ChildExit) as ce:
main_entry_point(["call", "-p", "..", run_py_str])
main_entry_point(["-p", "..", "call", run_py_str])
self.assertEqual(ce.exception.code, 55)

def test_shell_p(self):
"""Checking the -p changes both venv directory and the first item
in PYTHONPATH"""
main_entry_point(["create"])
with TemporaryDirectory() as temp_cwd:
# we will run it NOT from the project dir as CWD
os.chdir(temp_cwd)

# creating .py file to run
code_py = Path(temp_cwd) / "code.py"
output_file = self.write_program(code_py)

# running the code that will create a json file
self.assertProjectDirIsNotCwd()
with self.assertRaises(ChildExit) as ce:
main_entry_point(
["-p", str(self.projectDir.absolute()),
"shell", "--input",
f'python3 "{code_py}"'])
self.assertEqual(ce.exception.code, 0)

# loading json and checking the values
d = json.loads(output_file.read_text())
self.assertIn(str(self.projectDir.absolute()), d["sys.path"])
self.assertInVenv(Path(d["sys.executable"]))

def test_shell_ok(self):
main_entry_point(["create"])

Expand Down Expand Up @@ -391,3 +507,9 @@ def test_shell_but_no_venv(self):
with TimeLimited(10): # safety net
with self.assertRaises(VenvDoesNotExistExit) as cm:
main_entry_point(["shell"])


if __name__ == "__main__":
suite = unittest.TestSuite()
suite.addTest(TestsInsideTempProjectDir("test_shell_p"))
unittest.TextTestRunner().run(suite)
60 changes: 60 additions & 0 deletions tests/test_arg_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import unittest
from pathlib import Path

from vien.main import get_project_dir
from vien.arg_parser import Parsed


class TestProjectDir(unittest.TestCase):

def test_run_short_left(self):
pd = Parsed('-p a/b/c run python3 myfile.py'.split())
self.assertEqual(pd.project_dir_arg, 'a/b/c')

def test_run_long_left(self):
pd = Parsed('--project-dir a/b/c run python3 myfile.py'.split())
self.assertEqual(pd.project_dir_arg, 'a/b/c')

def test_call_short_right(self):
pd = Parsed('call -p a/b/c myfile.py'.split())
self.assertEqual(pd.project_dir_arg, 'a/b/c')

def test_call_long_right(self):
pd = Parsed('call --project-dir a/b/c myfile.py'.split())
self.assertEqual(pd.project_dir_arg, 'a/b/c')

def test_call_short_left(self):
pd = Parsed('-p a/b/c call myfile.py'.split())
self.assertEqual(pd.project_dir_arg, 'a/b/c')

def test_call_long_left(self):
pd = Parsed('--project-dir a/b/c call myfile.py'.split())
self.assertEqual(pd.project_dir_arg, 'a/b/c')

def test_call_short_both(self):
pd = Parsed('-p a/b/c call -p d/e/f myfile.py'.split())
self.assertEqual(pd.project_dir_arg, 'd/e/f')


class TestCallOtherArgs(unittest.TestCase):

def test_outdated_p(self):
pd = Parsed('-p a/b/c call -p d/e/f myfile.py arg1 arg2'.split())
self.assertEqual(pd.args_to_python, ['myfile.py', 'arg1', 'arg2'])

def test_p(self):
pd = Parsed('-p a/b/c call -d myfile.py a b c'.split())
self.assertEqual(pd.args_to_python, ['-d', 'myfile.py', 'a', 'b', 'c'])

def test_unrecoginzed(self):
"""Unrecognized arguments that are NOT after the 'call' word."""
with self.assertRaises(SystemExit) as ce:
Parsed('-labuda call myfile.py a b c'.split())
self.assertEqual(ce.exception.code, 2)





if __name__ == "__main__":
unittest.main()
33 changes: 33 additions & 0 deletions tests/test_call_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import unittest

from vien.call_parser import items_after, call_pyfile


class Test(unittest.TestCase):
def test_items_after(self):
self.assertEqual(list(items_after(['A', 'B', 'C'], 'A')),
['B', 'C'])
self.assertEqual(list(items_after(['A', 'B', 'C'], 'B')),
['C'])
self.assertEqual(list(items_after(['A', 'B', 'C'], 'C')),
[])
with self.assertRaises(LookupError):
list(items_after(['A', 'B', 'C'], 'X'))

def test_call_pyfile(self):
self.assertEqual(
call_pyfile("vien -p zzz call -d file.py arg1".split()),
"file.py")
self.assertEqual(
call_pyfile("vien -p zzz call -d arg1 arg2".split()),
None)
self.assertEqual(
call_pyfile("vien -p zzz call -d File.PY arg1".split()),
"File.PY")
self.assertEqual(
call_pyfile("vien aaa.py bbb.py call -d ccc.py arg1".split()),
"ccc.py")


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit f3d562e

Please sign in to comment.