diff --git a/vien/_constants.py b/vien/_constants.py index 5ecab41..125d551 100644 --- a/vien/_constants.py +++ b/vien/_constants.py @@ -1,4 +1,3 @@ -__version__ = "8.0.4" +__version__ = "8.0.5" __copyright__ = "(c) 2020-2021 Artëm IG " __license__ = "BSD-3-Clause" - diff --git a/vien/_exceptions.py b/vien/_exceptions.py index db08fcc..5b35225 100644 --- a/vien/_exceptions.py +++ b/vien/_exceptions.py @@ -20,14 +20,15 @@ def __init__(self, exit_code: int): super().__init__(exit_code) -class VenvExistsExit(VienExit): # todo does it return error code? - pass +class VenvExistsExit(VienExit): + def __init__(self, path: Path): + super().__init__(f'Virtual environment "{path}" already exists.') class VenvDoesNotExistExit(VienExit): def __init__(self, path: Path): super().__init__(f'Virtual environment "{path}" does not exist.\n' - f'You can create it with "create" command.') + f'You can create it with "vien create".') class PyFileNotFoundExit(VienExit): @@ -43,12 +44,12 @@ def __init__(self): class FailedToCreateVenvExit(VienExit): def __init__(self, path: Path): - super().__init__(f"Failed to create virtualenv {path}.") + super().__init__(f"Failed to create virtual environment {path}.") class FailedToClearVenvExit(VienExit): def __init__(self, path: Path): - super().__init__(f"Failed to clear virtualenv {path}.") + super().__init__(f"Failed to clear virtual environment {path}.") class CannotFindExecutableExit(VienExit): diff --git a/vien/_main.py b/vien/_main.py index a0ffe68..dd9cbdc 100755 --- a/vien/_main.py +++ b/vien/_main.py @@ -139,7 +139,7 @@ def arg_to_python_interpreter(argument: Optional[str]) -> str: def main_create(venv_dir: Path, interpreter: Optional[str]): if venv_dir.exists(): - raise VenvExistsExit("Virtualenv already exists.") + raise VenvExistsExit(venv_dir) exe = arg_to_python_interpreter(interpreter) diff --git a/vien/_parsed_args.py b/vien/_parsed_args.py index 1c829cc..1f61038 100644 --- a/vien/_parsed_args.py +++ b/vien/_parsed_args.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause import argparse +import os import sys from enum import Enum from typing import List, Optional, Iterable @@ -65,128 +66,157 @@ class Commands(Enum): path = "path" +class TempColumns: + def __init__(self, width: int): + self.width = width + self._old_value: Optional[str] = None + + def __enter__(self): + self._old_value = os.environ.get('COLUMNS') + + new_value_int = self.width + + try: + old_value_int = int(self._old_value) + new_value_int = min(old_value_int, new_value_int) + except (ValueError, TypeError): + pass + + os.environ['COLUMNS'] = str(new_value_int) + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._old_value is not None: + os.environ['COLUMNS'] = self._old_value + + class ParsedArgs: PARAM_WINDOWS_ALL_ARGS = "--vien-secret-windows-all-args" def __init__(self, args: Optional[List[str]]): - super().__init__() - - self._call: Optional[ParsedCall] = None - - if args is None: - args = sys.argv[1:] - - # secret parameter PARAM_WINDOWS_ALL_ARGS allows to run commands that - # are not yet fully supported on Windows. - enable_windows_all_args = self.PARAM_WINDOWS_ALL_ARGS in args - if enable_windows_all_args: - # for more transparent testing, I don't want this param - # to ever affect posix behavior - assert is_windows - - parser = argparse.ArgumentParser() - - parser.add_argument("--project-dir", "-p", default=None, type=str, - help="the Python project directory " - "(default: current working directory). " - "Implicitly determines which virtual " - "environment should be used for the command") - - # the following parameter is added only to avoid parsing errors. - # Actually we use its value from `args` before running ArgumentParser - parser.add_argument(self.PARAM_WINDOWS_ALL_ARGS, action='store_true', - help=argparse.SUPPRESS) - - subparsers = parser.add_subparsers(dest='command', required=True) - - parser_init = subparsers.add_parser(Commands.create.name, - help="create new virtual environment") - parser_init.add_argument('python', type=str, default=None, - nargs='?') - - subparsers.add_parser(Commands.delete.name, - help="delete existing environment") - - parser_reinit = subparsers.add_parser( - Commands.recreate.name, - help="delete existing environment and create new") - parser_reinit.add_argument('python', type=str, default=None, - nargs='?') - - if is_posix or enable_windows_all_args: - shell_parser = subparsers.add_parser( - Commands.shell.name, - help="dive into Bash sub-shell with the environment") - shell_parser.add_argument("--input", type=str, default=None) - shell_parser.add_argument("--delay", type=float, default=None, - help=argparse.SUPPRESS) - - if is_posix or enable_windows_all_args: - parser_run = subparsers.add_parser( - Commands.run.name, - help="run a shell command in the environment") - parser_run.add_argument('otherargs', nargs=argparse.REMAINDER) - - parser_call = subparsers.add_parser( - Commands.call.name, - help="run a .py file in the environment") - # todo Remove it later. [call -p] is outdated since 2021-05 - parser_call.add_argument("--project-dir", "-p", default=None, type=str, - dest="outdated_call_project_dir", - help=argparse.SUPPRESS) - # this arg is for help only. Actually it's buggy (at least in 3.7), - # so we will never use its result, and get those args other way - parser_call.add_argument('args_to_python', nargs=argparse.REMAINDER) - - subparsers.add_parser( - Commands.path.name, - help="show the path of the environment " - "for the project") - - if not args: - print(usage_doc()) - parser.print_help() - exit(2) - - self.args = args - - # it seems, nargs.REMAINDER is buggy in 2021: - # https://bugs.python.org/issue17050 - # - # For example, when the first REMAINDER argument is an option - # such as "-d", argparse shows error instead of just remembering "-d" - # - # But "-d" actually can be the first REMAINDER arg after the CALL - # command. - # - # That's why we parse args twice. First time with `parse_known_args` - - # to get the command name. And then, if it's not CALL - we parse - # again with a stricter parse_args. - - self._ns: argparse.Namespace - unknown: List[str] - - self._ns, unknown = parser.parse_known_args(self.args) - if self._ns.command == 'call': - self.args_to_python = list(_iter_after(args, 'call')) - - # if some of the unknown args are NOT after the 'call', - # then we're failed to interpret the command - bad_unrecognized = [unk for unk in unknown if - unk not in self.args_to_python] - if bad_unrecognized: - parser.error(f"unrecognized arguments: {bad_unrecognized}") - raise AssertionError("Not expected to run this line") - - # todo Remove later. [call -p] is outdated since 2021-05 - self.args_to_python = _remove_leading_p(self.args_to_python) - self._call = ParsedCall(args) - else: - # if some args were not recognized, parsing everything stricter - if unknown: - self._ns = parser.parse_args(self.args) - - self.command = Commands(self._ns.command) + with TempColumns(80): + + self._call: Optional[ParsedCall] = None + + if args is None: + args = sys.argv[1:] + + # secret parameter PARAM_WINDOWS_ALL_ARGS allows to run commands that + # are not yet fully supported on Windows. + enable_windows_all_args = self.PARAM_WINDOWS_ALL_ARGS in args + if enable_windows_all_args: + # for more transparent testing, I don't want this param + # to ever affect posix behavior + assert is_windows + + parser = argparse.ArgumentParser() + + parser.add_argument("--project-dir", "-p", default=None, type=str, + help="the Python project directory " + "(default: current working directory). " + "Implicitly determines which virtual " + "environment should be used for the " + "command") + + # the following parameter is added only to avoid parsing errors. + # Actually we use its value from `args` before running + # ArgumentParser + parser.add_argument(self.PARAM_WINDOWS_ALL_ARGS, + action='store_true', + help=argparse.SUPPRESS) + + subparsers = parser.add_subparsers(dest='command', required=True) + + parser_init = subparsers.add_parser( + Commands.create.name, + help="create new virtual environment") + parser_init.add_argument('python', type=str, default=None, + nargs='?') + + subparsers.add_parser(Commands.delete.name, + help="delete existing environment") + + parser_reinit = subparsers.add_parser( + Commands.recreate.name, + help="delete existing environment and create new") + parser_reinit.add_argument('python', type=str, default=None, + nargs='?') + + if is_posix or enable_windows_all_args: + shell_parser = subparsers.add_parser( + Commands.shell.name, + help="dive into Bash sub-shell with the environment") + shell_parser.add_argument("--input", type=str, default=None) + shell_parser.add_argument("--delay", type=float, default=None, + help=argparse.SUPPRESS) + + if is_posix or enable_windows_all_args: + parser_run = subparsers.add_parser( + Commands.run.name, + help="run a shell command in the environment") + parser_run.add_argument('otherargs', nargs=argparse.REMAINDER) + + parser_call = subparsers.add_parser( + Commands.call.name, + help="run a .py file in the environment") + # todo Remove it later. [call -p] is outdated since 2021-05 + parser_call.add_argument("--project-dir", "-p", default=None, + type=str, + dest="outdated_call_project_dir", + help=argparse.SUPPRESS) + # this arg is for help only. Actually it's buggy (at least in 3.7), + # so we will never use its result, and get those args other way + parser_call.add_argument('args_to_python', nargs=argparse.REMAINDER) + + subparsers.add_parser( + Commands.path.name, + help="show the path of the environment " + "for the project") + + if not args: + print(usage_doc()) + parser.print_help() + exit(2) + + self.args = args + + # it seems, nargs.REMAINDER is buggy in 2021: + # https://bugs.python.org/issue17050 + # + # For example, when the first REMAINDER argument is an option + # such as "-d", argparse shows error instead of just + # remembering "-d" + # + # But "-d" actually can be the first REMAINDER arg after the CALL + # command. + # + # That's why we parse args twice. First time with + # `parse_known_args` - to get the command name. And then, if it's + # not CALL - we parse again with a stricter parse_args. + + self._ns: argparse.Namespace + unknown: List[str] + + self._ns, unknown = parser.parse_known_args(self.args) + if self._ns.command == 'call': + self.args_to_python = list(_iter_after(args, 'call')) + + # if some of the unknown args are NOT after the 'call', + # then we're failed to interpret the command + bad_unrecognized = [unk for unk in unknown if + unk not in self.args_to_python] + if bad_unrecognized: + parser.error(f"unrecognized arguments: {bad_unrecognized}") + raise AssertionError("Not expected to run this line") + + # todo Remove later. [call -p] is outdated since 2021-05 + self.args_to_python = _remove_leading_p(self.args_to_python) + self._call = ParsedCall(args) + else: + # if some args were not recognized, parsing everything stricter + if unknown: + self._ns = parser.parse_args(self.args) + + self.command = Commands(self._ns.command) # @property # def command(self) -> Commands: