Skip to content

Commit

Permalink
Merge ed92fd9 into master
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions[bot] authored May 27, 2021
2 parents 594dfd7 + ed92fd9 commit 00c8b82
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 120 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 8.0

- The `call` command now supports the `-m` option, that runs the file as a module.

# 7.1

- Fixed: `call` command sometimes received incorrect `$PYTHONPATH` values on systems
Expand Down
85 changes: 58 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,7 @@ $ vien run pip install requests lxml
$ vien call main.py
```

# Commands

## create
# "create" command

`vien create` сreates a virtual environment that will correspond the current
working directory. The **working directory** in this case is assumed to be
Expand Down Expand Up @@ -146,7 +144,7 @@ be executed in the shell as `python3.8`, you can try
$ vien create python3.8
```

## shell
# "shell" command

`vien shell` starts interactive bash session in the virtual environment.

Expand Down Expand Up @@ -184,7 +182,7 @@ command line.
$ echo 'which python3 && echo $PATH' | vien shell
```

## run
# "run" command

`vien run COMMAND` runs a shell command in the virtual environment.

Expand All @@ -211,40 +209,75 @@ $ /path/to/the/venv/bin/deactivate

</details>

## call
# "call" command

`vien call PYFILE` executes a `.py` script in the virtual environment.

``` bash
$ cd /path/to/myProject
$ vien call main.py
### "call": running file as a file

``` bash
$ cd /abc/myProject
$ vien call pkg/module.py

# runs [python pkg/module.py]
```

In fact, all arguments following the `call` command are passed directly to the
python executable.
### "call": running file as a module

If the `.py` file name is preceded by the `-m` parameter, we will run it with
`python -m MODULE`. Running in this manner often simplifies importing other modules
from the program.


``` bash
$ cd /abc/myProject
$ vien call -m /abc/myProject/pkg/module.py

# runs [python -m pkg.module]
```

- `module.py` must be located somewhere inside the `/abc/myProject`
- parent subdirectories such as `pkg` must be importable, i.e. must contain
`pkg/__init__.py`


The `call` command only accepts `.py` files, no module names.

``` bash
# ERROR: there is no file named pkg.module
$ vien call -m pkg.module
```

### "call": passing arguments to Python and to the program

Arguments following the `call` command are passed to the python executable.

``` bash
# passing arguments [-B, -OO] to Python and [arg1, arg2] to main.py
$ vien call -B -OO main.py arg1 arg2
$ vien call -B -OO -m package/main.py arg1 arg2

# runs [python -B -OO -m package.main arg1 arg2]
```

### "call": project directory

The optional `-p` parameter can be specified before the `call` word. It allows
you to set the project directory **relative** to the parent directory of the
**file** being run.

``` bash
$ cd any/where # working dir is irrelevant

# both of the following calls consider /abc/myProject
# the project directory
# both of the following calls will use
# /abc/myProject as the project directory

$ vien -p /abc/myProject call /abc/myProject/main.py
$ vien -p . call /abc/myProject/main.py
$ vien -p /abc/myProject call /abc/myProject/pkg/main.py
$ vien -p .. call /abc/myProject/pkg/main.py
```

This parameter makes things like [shebang](#Shebang) possible.
In the second case `..` means that the project directory is
`/abc/myProject/pkg/..`, which resolves to `/abc/myProject`.

## delete
# "delete" command

`vien delete` deletes the virtual environment.

Expand All @@ -253,7 +286,7 @@ $ cd /path/to/myProject
$ vien delete
```

## recreate
# "recreate" command

`vien recreate` old and creates new virtual environment.

Expand All @@ -271,9 +304,7 @@ $ cd /path/to/myProject
$ vien recreate /usr/local/opt/python@3.10/bin/python3
```

# Options

## --project-dir, -p
# --project-dir, -p

This option must appear after `vien`, but before the command.

Expand Down Expand Up @@ -341,10 +372,10 @@ Insert the shebang line to the top of the file you want to run. The value of the
shebang depends on the location of the file relative to the project directory.

File | Shebang line
--------------------------------|--------------------------------
`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`
--------------------------------|--------------------------------------
`myProject/runme.py` | `#!/usr/bin/env vien -p . call -m`
`myProject/pkg/runme.py` | `#!/usr/bin/env vien -p .. call -m`
`myProject/pkg/subpkg/runme.py` | `#!/usr/bin/env vien -p ../.. call -m`

After inserting the shebang, make the file executable:

Expand Down
162 changes: 135 additions & 27 deletions tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from pathlib import Path
from tempfile import TemporaryDirectory
from timeit import default_timer as timer
from typing import List
from typing import List, Optional

from tests.test_arg_parser import windows_too
from vien.arg_parser import Parsed
Expand Down Expand Up @@ -72,6 +72,24 @@ def test_path(self):
main_entry_point(["path"])


class TempCwd:
"""Context manager that creates temp directory and makes it the current
working directory until exit."""

def __init__(self):
self.prev_cwd: Optional[str] = None
self.temp_dir: Optional[str] = None

def __enter__(self):
self.prev_cwd = os.getcwd()
self.temp_dir = tempfile.mkdtemp()
os.chdir(self.temp_dir)

def __exit__(self, exc_type, exc_val, exc_tb):
os.chdir(self.prev_cwd)
shutil.rmtree(self.temp_dir)


class TestsInsideTempProjectDir(unittest.TestCase):

def setUp(self):
Expand All @@ -87,6 +105,15 @@ def setUp(self):
self.projectDir.mkdir()
self.svetDir.mkdir()

# creating two empty packages: pkg and pkg.sub
self.project_pkg = self.projectDir / "pkg"
self.project_pkg_sub = self.project_pkg / "sub"
self.project_pkg_sub.mkdir(parents=True)
(self.project_pkg_sub / "__init__.py").touch()
(self.project_pkg / "__init__.py").touch()

self.json_report_file: Optional[Path] = None

os.chdir(self.projectDir)

self.expectedVenvDir = self.svetDir / "project_venv"
Expand Down Expand Up @@ -162,19 +189,44 @@ 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'
out_file_path_str = repr(str(out_file_path))
def write_reporting_program(self, py_file_path: Path) -> Path:
self.json_report_file = py_file_path.parent / 'output.json'
out_file_path_str = repr(str(self.json_report_file))
code = "import pathlib, sys, json\n" \
"d={'sys.path': sys.path, \n" \
" 'sys.executable': sys.executable}\n" \
" 'sys.executable': sys.executable, \n" \
" 'sys.argv': sys.argv\n" \
"}\n" \
"js=json.dumps(d)\n" \
f"file=pathlib.Path({out_file_path_str})\n" \
'file.write_text(js, encoding="utf-8")'
py_file_path.write_text(code, encoding='utf-8')

assert not out_file_path.exists()
return out_file_path
assert not self.json_report_file.exists()
return self.json_report_file

@property
def report_exists(self) -> bool:
return self.json_report_file is not None and \
self.json_report_file.exists()

@property
def reported_syspath(self) -> List[str]:
assert self.json_report_file is not None
d = json.loads(self.json_report_file.read_text(encoding="utf-8"))
return d["sys.path"]

@property
def reported_executable(self) -> Path:
assert self.json_report_file is not None
d = json.loads(self.json_report_file.read_text(encoding="utf-8"))
return Path(d["sys.executable"])

@property
def reported_argv(self) -> List[str]:
assert self.json_report_file is not None
d = json.loads(self.json_report_file.read_text(encoding="utf-8"))
return d["sys.argv"]

############################################################################

Expand Down Expand Up @@ -320,6 +372,11 @@ def test_run_python_version(self):
# (was failing with nargs='*', ok with nargs=argparse.REMAINDER)
main_entry_point(windows_too(["run", "python3", "--version"]))

def _run_and_check(self, args: List[str], expected_exit_code=0):
with self.assertRaises(ChildExit) as ce:
main_entry_point(args)
self.assertEqual(ce.exception.code, expected_exit_code)

@unittest.skipUnless(is_posix, "not POSIX")
def test_run_p(self):
"""Checking the -p changes both venv directory and the first item
Expand All @@ -331,21 +388,17 @@ def test_run_p(self):

# creating .py file to run
code_py = Path(temp_cwd) / "code.py"
output_file = self.write_program(code_py)
self.write_reporting_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)
self._run_and_check(
["-p", str(self.projectDir.absolute()),
"run", "python3", str(code_py)])

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

@unittest.skipUnless(is_posix, "not POSIX")
def test_run_python_code(self):
Expand Down Expand Up @@ -414,20 +467,75 @@ def test_call_23(self):
Testing whether it runs and whether we get correct exit code."""
self._call_for_exit_code(23)

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

# creating pkg/sub/module.py
file_py = self.project_pkg_sub / "module.py"
self.write_reporting_program(file_py)

self.assertFalse(self.report_exists)

# running from random CWD
with TempCwd():
self.assertProjectDirIsNotCwd()
self._run_and_check(
["-p", str(self.projectDir.absolute()),
"call", "-m", str(file_py.absolute())])

self.assertTrue(self.report_exists)
self.assertInVenv(self.reported_executable)
self.assertIn(str(self.projectDir.absolute()),
self.reported_syspath)

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

# creating pkg/sub/module.py
file_py = self.project_pkg_sub / "module.py"
self.write_reporting_program(file_py)

self.assertFalse(self.report_exists)

# running from random CWD
with TempCwd():
self.assertProjectDirIsNotCwd()
self._run_and_check(
["-p", str(self.projectDir.absolute()),
"call", str(file_py.absolute())]) # no -m

self.assertTrue(self.report_exists)
self.assertInVenv(self.reported_executable)
self.assertIn(str(self.projectDir.absolute()),
self.reported_syspath)

def test_call_parameters(self):
"""Testing that call really passes parameters to child."""

main_entry_point(["create"])
(self.projectDir / "main.py").write_text(
f"import sys; exit(len(sys.argv))")

with self.assertRaises(ChildExit) as ce:
main_entry_point(["call", "main.py"])
self.assertEqual(ce.exception.code, 1) # received len(argv)
self.write_reporting_program(self.projectDir/"file.py")

with self.assertRaises(ChildExit) as ce:
main_entry_point(["call", "main.py", "aaa", "bbb", "ccc"])
self.assertEqual(ce.exception.code, 4) # received len(argv)
self.assertFalse(self.report_exists)
self._run_and_check(["call", "file.py", "hello", "program"])
self.assertTrue(self.report_exists)

self.assertEqual(self.reported_argv[-2], "hello")
self.assertEqual(self.reported_argv[-1], "program")



# main_entry_point(["create"])
# (self.projectDir / "main.py").write_text(
# f"import sys; exit(len(sys.argv))")
#
# with self.assertRaises(ChildExit) as ce:
# main_entry_point(["call", "main.py"])
# self.assertEqual(ce.exception.code, 1) # received len(argv)
#
# with self.assertRaises(ChildExit) as ce:
# main_entry_point(["call", "main.py", "aaa", "bbb", "ccc"])
# self.assertEqual(ce.exception.code, 4) # received len(argv)

def test_call_project_dir_venv(self):
"""Tests that the -p parameter actually changes the project directory,
Expand Down Expand Up @@ -532,7 +640,7 @@ def test_shell_p(self):

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

# running the code that will create a json file
self.assertProjectDirIsNotCwd()
Expand Down
Loading

0 comments on commit 00c8b82

Please sign in to comment.