diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b397d20 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +neatest \ No newline at end of file diff --git a/setup.py b/setup.py index e4df4c2..10efe95 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,26 @@ -from importlib.machinery import SourceFileLoader from pathlib import Path -from setuptools import setup, find_packages +from setuptools import setup -constants = SourceFileLoader('constants', 'vien/constants.py').load_module() -# def readDocText(): -# """Reads README.md and returns the text after first empty line. -# It helps to skip the header with badges intended only for GitHub. -# """ -# doctext = (Path(__file__).parent / 'README.md').read_text() -# -# # skipping until first empty line -# lines = [l.strip() for l in doctext.splitlines()] -# first_empty_line_index = lines.index("") -# lines = lines[first_empty_line_index + 1:] -# -# return "\n".join(lines) +def load_module_dict(filename: str) -> dict: + import importlib.util as ilu + filename = Path(__file__).parent / filename + spec = ilu.spec_from_file_location('', filename) + module = ilu.module_from_spec(spec) + spec.loader.exec_module(module) + return module.__dict__ + + +name = "vien" +constants = load_module_dict(f'{name}/constants.py') readme = (Path(__file__).parent / 'README.md').read_text() readme = "# " + readme.partition("\n#")[-1] setup( - name="vien", - version=constants.__version__, + name=name, + version=constants['__version__'], author="Artëm IG", author_email="ortemeo@gmail.com", @@ -37,7 +34,7 @@ long_description=readme, long_description_content_type='text/markdown', - license=constants.__license__, + license=constants['__license__'], entry_points={ 'console_scripts': [ @@ -47,11 +44,7 @@ keywords="virtual-environment venv virtualenv python command-line shell " "terminal bash run create delete execute".split(), - # https://pypi.org/classifiers/ classifiers=[ - - # "Development Status :: 2 - Pre-Alpha", - # "Development Status :: 3 - Alpha", "Development Status :: 4 - Beta", "Intended Audience :: Developers", 'License :: OSI Approved :: BSD License', diff --git a/test_unit.py b/test_unit.py index 5c22237..f0fe55b 100755 --- a/test_unit.py +++ b/test_unit.py @@ -1,29 +1,35 @@ #!/usr/bin/env python3 -import unittest -from pathlib import Path - - -def suite(): - """Can be imported into `setup.py` as `test_suite="test_unit.suite"`.""" - - parent_dir = Path(__file__).parent - init_py, = parent_dir.glob("*/__init__.py") - - return unittest.TestLoader().discover( - top_level_dir=str(parent_dir), - start_dir=str(init_py.parent), - pattern="*.py") - - -def run_tests(): - """Discovers and runs unit tests for the module.""" - - result = unittest.TextTestRunner(buffer=True).run(suite()) - - if result.failures or result.errors: - exit(1) - +import neatest if __name__ == "__main__": - run_tests() + neatest.run() + +# +# import unittest +# from pathlib import Path +# +# +# def suite(): +# """Can be imported into `setup.py` as `test_suite="test_unit.suite"`.""" +# +# parent_dir = Path(__file__).parent +# init_py, = parent_dir.glob("*/__init__.py") +# +# return unittest.TestLoader().discover( +# top_level_dir=str(parent_dir), +# start_dir=str(init_py.parent), +# pattern="*.py") +# +# +# def run_tests(): +# """Discovers and runs unit tests for the module.""" + +# result = unittest.TextTestRunner(buffer=True).run(suite()) +# +# if result.failures or result.errors: +# exit(1) +# +# +# if __name__ == "__main__": +# run_tests() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vien/main_test.py b/tests/main_test.py similarity index 89% rename from vien/main_test.py rename to tests/main_test.py index 249be08..5cfa8a2 100644 --- a/vien/main_test.py +++ b/tests/main_test.py @@ -155,6 +155,30 @@ def test_shell_fails_if_not_exist(self): with self.assertRaises(VenvDoesNotExistError) as cm: main_entry_point(["shell"]) + # TODO test 'shell' exit codes too? + + def test_run_exit_code_0(self): + """Test that main_entry_point returns the same exit code, + as the called command""" + with self.assertRaises(SystemExit) as ce: + main_entry_point(["run", "python3", "-c", "exit(0)"]) + self.assertEqual(ce.exception.code, 0) + + def test_run_exit_code_1(self): + """Test that main_entry_point returns the same exit code, + as the called command""" + with self.assertRaises(SystemExit) as ce: + main_entry_point(["run", "python3", "-c", "exit(1)"]) + self.assertEqual(ce.exception.code, 1) + + def test_run_exit_code_2(self): + """Test that main_entry_point returns the same exit code, + as the called command""" + with self.assertRaises(SystemExit) as ce: + main_entry_point(["run", "python3", "-c", "exit(2)"]) + self.assertEqual(ce.exception.code, 2) + + def test_run(self): main_entry_point(["create"]) diff --git a/vien/constants.py b/vien/constants.py index 536af0b..a25ef10 100644 --- a/vien/constants.py +++ b/vien/constants.py @@ -1,3 +1,3 @@ -__version__ = "0.3.5" +__version__ = "0.3.6" __copyright__ = "(c) 2020-2021 Artëm IG " __license__ = "BSD-3-Clause" diff --git a/vien/main.py b/vien/main.py index 72d1d50..ac740a8 100755 --- a/vien/main.py +++ b/vien/main.py @@ -32,7 +32,7 @@ class VenvExistsError(VienError): class VenvDoesNotExistError(VienError): def __init__(self, path: Path): - super().__init__(f"Virtualenv {path} does not exist.") + super().__init__(f"Virtual environment {path} does not exist.") class FailedToCreateVenvError(VienError): @@ -142,13 +142,7 @@ def test_if_not_n(self): self.assertGreater(len(p), len("/.vien")) -# def run(args: List[str]): -# if verbose: -# print(f"Running {args}") -# subprocess.run(args, shell=True) -# - -def runseq(commands: List[str]): +def run_bash_sequence(commands: List[str]) -> int: bash_lines = [ "#!/bin/bash" "set -e", # fail on first error @@ -160,7 +154,9 @@ def runseq(commands: List[str]): # Otherwise the command is executed in /bin/sh, ignoring the hashbang, # but SH fails to execute commands like 'source' - subprocess.call("\n".join(bash_lines), shell=True, executable='/bin/bash') + return subprocess.call("\n".join(bash_lines), + shell=True, + executable='/bin/bash') def quote(arg: str) -> str: @@ -258,7 +254,8 @@ def guess_bash_ps1(): return r"\h:\W \u\$" # default for MacOS up to Catalina # hope for the best in other systems - return subprocess.check_output(['/bin/bash', '-i', '-c', 'echo $PS1']).decode().rstrip() + return subprocess.check_output( + ['/bin/bash', '-i', '-c', 'echo $PS1']).decode().rstrip() def main_shell(venv_dir: Path, venv_name: str, input: str, input_delay: float): @@ -283,8 +280,9 @@ def main_shell(venv_dir: Path, venv_name: str, input: str, input_delay: float): if bashrc_file.exists(): # Ubuntu - commands.append(f"exec bash --rcfile <(cat {json.dumps(str(bashrc_file))} " - f"&& echo 'PS1={json.dumps(new_ps1)}')") + commands.append( + f"exec bash --rcfile <(cat {json.dumps(str(bashrc_file))} " + f"&& echo 'PS1={json.dumps(new_ps1)}')") else: # MacOS commands.append(f"PS1={json.dumps(new_ps1)} exec bash") @@ -299,7 +297,8 @@ def main_shell(venv_dir: Path, venv_name: str, input: str, input_delay: float): # It seems it closes immediately after the subprocess.Popen closes the stdin. # So it will not wait for "exit". But it serves the task well - run_as_bash_script("\n".join(commands), input=input.encode() if input else None, + run_as_bash_script("\n".join(commands), + input=input.encode() if input else None, input_delay=input_delay) @@ -310,7 +309,7 @@ def main_run(venv_dir: Path, otherargs): " ".join(quote(a) for a in otherargs) ] - exit(runseq(commands)) + exit(run_bash_sequence(commands)) def main_entry_point(args: Optional[List[str]] = None): @@ -333,9 +332,11 @@ def main_entry_point(args: Optional[List[str]] = None): shell_parser = subparsers.add_parser('shell', help="dive into Bash sub-shell using the virtualenv") shell_parser.add_argument("--input", type=str, default=None) - shell_parser.add_argument("--delay", type=float, default=None, help=argparse.SUPPRESS) + shell_parser.add_argument("--delay", type=float, default=None, + help=argparse.SUPPRESS) - parser_run = subparsers.add_parser('run', help="run a command inside the virtualenv") + parser_run = subparsers.add_parser('run', + help="run a command inside the virtualenv") parser_run.add_argument('otherargs', nargs=argparse.REMAINDER) subparsers.add_parser('path',