From 8db565cd0f5613c7288d0fd330c93113cdf5abce Mon Sep 17 00:00:00 2001 From: Brian Lee Date: Fri, 4 Nov 2022 17:57:21 -0700 Subject: [PATCH 1/5] feat: log stderr --- .github/workflows/installer.yml | 21 +++++ README.md | 10 +++ install-poetry.py | 41 +++++++-- tests/test_install_poetry.py | 148 ++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 tests/test_install_poetry.py diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index 76282ea..7806416 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -19,6 +19,27 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + unit-test: + name: ${{ matrix.python-version }} + runs-on: Ubuntu + strategy: + matrix: + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] + fail-fast: false + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install pytest + run: python -m pip install pytest + + - name: Run PyTest + run: python -m pytest ./tests + default: name: ${{ matrix.os }} / ${{ matrix.python-version }} / install-poetry.py ${{ matrix.args }} runs-on: ${{ matrix.image }} diff --git a/README.md b/README.md index 6c2c00c..5881475 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,16 @@ You can also install Poetry for a `git` repository by using the `--git` option: curl -sSL https://install.python-poetry.org | python3 - --git https://github.com/python-poetry/poetry.git@master ```` +If you need Poetry to write errors to stderr, you can use `--stderr` option or the `$POETRY_LOG_STDERR` +environment variable: + +> _Note: In CI environments, this will be enabled automatically._ + +```bash +curl -sSL https://install.python-poetry.org | python3 - --stderr +curl -sSL https://install.python-poetry.org | POETRY_LOG_STDERR=1 python3 - +```` + > **Note**: The installer does not support Python < 3.6. ## Known Issues diff --git a/install-poetry.py b/install-poetry.py index b6d32d4..0d2e0e3 100644 --- a/install-poetry.py +++ b/install-poetry.py @@ -898,6 +898,15 @@ def main(): "of Poetry available online." ), ) + parser.add_argument( + "--stderr", + dest="stderr", + action="store_true", + help=( + "Log installation errors to stderr instead of a log file." + ), + default=False + ) args = parser.parse_args() @@ -912,6 +921,15 @@ def main(): git=args.git, ) + disable_log_file = args.stderr or string_to_bool(os.getenv("POETRY_LOG_STDERR", "0")) + + if not disable_log_file and string_to_bool(os.getenv("CI", "0")): + installer._write( + colorize("info", "CI environment detected. Writing logs to stderr.") + ) + disable_log_file = True + + if args.uninstall or string_to_bool(os.getenv("POETRY_UNINSTALL", "0")): return installer.uninstall() @@ -923,15 +941,22 @@ def main(): if e.log is not None: import traceback - _, path = tempfile.mkstemp( - suffix=".log", - prefix="poetry-installer-error-", - dir=str(Path.cwd()), - text=True, + error = ( + f"{e.log}\n" + f"Traceback:\n\n{''.join(traceback.format_tb(e.__traceback__))}" ) - installer._write(colorize("error", f"See {path} for error logs.")) - text = f"{e.log}\nTraceback:\n\n{''.join(traceback.format_tb(e.__traceback__))}" - Path(path).write_text(text) + + if disable_log_file: + installer._write(colorize("error", error)) + else: + _, path = tempfile.mkstemp( + suffix=".log", + prefix="poetry-installer-error-", + dir=str(Path.cwd()), + text=True, + ) + installer._write(colorize("error", f"See {path} for error logs.")) + Path(path).write_text(error) return e.return_code diff --git a/tests/test_install_poetry.py b/tests/test_install_poetry.py new file mode 100644 index 0000000..b5deaee --- /dev/null +++ b/tests/test_install_poetry.py @@ -0,0 +1,148 @@ +from unittest.mock import MagicMock, patch +import re +import typing +import importlib +import unittest + + +module = importlib.import_module("install-poetry") + + +class InstallPoetryTestCase(unittest.TestCase): + def setUp(self): + self.__patchers = [] + + self.__patchers.append(patch("install-poetry.Installer")) + self.__mock_installer_cls = self.__patchers[-1].start() + self.__installer = MagicMock() + self.__mock_installer_cls.return_value = self.__installer + + self.__patchers.append(patch("install-poetry.argparse.ArgumentParser")) + self.__mock_argument_parser_cls = self.__patchers[-1].start() + self.__parser = MagicMock() + self.__args = MagicMock() + self.__args.version = None + self.__args.preview = False + self.__args.force = False + self.__args.accept_all = False + self.__args.path = None + self.__args.git = None + self.__args.stderr = False + self.__args.uninstall = False + self.__mock_argument_parser_cls.return_value = self.__parser + self.__parser.parse_args.return_value = self.__args + + self.__patchers.append(patch("install-poetry.os")) + self.__mock_os = self.__patchers[-1].start() + self.__mock_os.getenv.side_effect = self.__getenv + self.__env = {} + + self.__patchers.append(patch("install-poetry.colorize")) + self.__mock_colorize = self.__patchers[-1].start() + self.__mock_colorize.side_effect = self.__colorize + self.__messages = [] + + self.__patchers.append(patch("install-poetry.tempfile")) + self.__mock_tempfile = self.__patchers[-1].start() + self.__mock_tempfile.mkstemp.side_effect = self.__mkstemp + self.__tmp_file = None + + self.__patchers.append(patch("install-poetry.Path")) + self.__mock_path_cls = self.__patchers[-1].start() + + def tearDown(self): + for patcher in self.__patchers: + patcher.stop() + + def test_install_poetry_main__happy(self): + self.__installer.run.return_value = 0 + + return_code = module.main() + + self.__mock_installer_cls.assert_called_with( + version=None, + preview=False, + force=False, + accept_all=True, + path=None, + git=None + ) + + self.__installer.uninstall.assert_not_called() + + self.assertEqual(return_code, 0) + + def test_install_poetry_main__default_install_error(self): + self.__installer.run.side_effect = [ + module.PoetryInstallationError(1, "a fake poetry installation error") + ] + + return_code = module.main() + + self.__assert_no_matching_message("error", re.compile("a fake poetry installation error")) + self.__assert_any_matching_message("error", re.compile(f"See {self.__tmp_file} for error logs")) + + self.assertEqual(return_code, 1) + self.__mock_path_cls(self.__tmp_file).write_text.assert_called_once() + + def test_install_poetry_main__stderr_arg(self): + self.__args.stderr = True + self.__installer.run.side_effect = [ + module.PoetryInstallationError(1, "a fake poetry installation error") + ] + + return_code = module.main() + + self.__assert_no_matching_message("info", re.compile("CI environment detected")) + self.__assert_any_matching_message("error", re.compile("a fake poetry installation error")) + + self.assertEqual(return_code, 1) + self.__mock_path_cls.assert_not_called() + + def test_install_poetry_main__log_stderr_var(self): + self.__env["POETRY_LOG_STDERR"] = "1" + self.__installer.run.side_effect = [ + module.PoetryInstallationError(1, "a fake poetry installation error") + ] + + return_code = module.main() + + self.__assert_no_matching_message("info", re.compile("CI environment detected")) + self.__assert_any_matching_message("error", re.compile("a fake poetry installation error")) + + self.assertEqual(return_code, 1) + self.__mock_path_cls.assert_not_called() + + def test_install_poetry_main__ci(self): + self.__env["CI"] = "1" + self.__installer.run.side_effect = [ + module.PoetryInstallationError(1, "a fake poetry installation error") + ] + + return_code = module.main() + + self.__assert_any_matching_message("info", re.compile("CI environment detected")) + self.__assert_any_matching_message("error", re.compile("a fake poetry installation error")) + + self.assertEqual(return_code, 1) + self.__mock_path_cls.assert_not_called() + + def __colorize(self, severity: str, message: str) -> str: + self.__messages.append((severity, message)) + return f"{severity}:{message}" + + def __getenv(self, key: str, default: typing.Optional[str] = None) -> typing.Optional[str]: + return self.__env.get(key, default) + + def __mkstemp(self, suffix="suffix", prefix="prefix", dir=None, text=None): + self.__tmp_file = f"{prefix}unittest{suffix}" + return None, self.__tmp_file + + def __assert_any_matching_message(self, severity: str, pattern: re.Pattern): + self.assertGreater(self.__count_matching_message(severity, pattern), 0) + + def __assert_no_matching_message(self, severity: str, pattern: re.Pattern): + self.assertEqual(self.__count_matching_message(severity, pattern), 0) + + def __count_matching_message(self, severity: str, pattern: re.Pattern): + return len([message for message in self.__messages if severity == message[0] and pattern.search(message[1])]) From 3745592173d660544d938ec11a1522f8aec0f623 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 5 Nov 2022 00:59:00 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- install-poetry.py | 11 ++++----- tests/test_install_poetry.py | 44 +++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/install-poetry.py b/install-poetry.py index 0d2e0e3..4e1215a 100644 --- a/install-poetry.py +++ b/install-poetry.py @@ -902,10 +902,8 @@ def main(): "--stderr", dest="stderr", action="store_true", - help=( - "Log installation errors to stderr instead of a log file." - ), - default=False + help=("Log installation errors to stderr instead of a log file."), + default=False, ) args = parser.parse_args() @@ -921,7 +919,9 @@ def main(): git=args.git, ) - disable_log_file = args.stderr or string_to_bool(os.getenv("POETRY_LOG_STDERR", "0")) + disable_log_file = args.stderr or string_to_bool( + os.getenv("POETRY_LOG_STDERR", "0") + ) if not disable_log_file and string_to_bool(os.getenv("CI", "0")): installer._write( @@ -929,7 +929,6 @@ def main(): ) disable_log_file = True - if args.uninstall or string_to_bool(os.getenv("POETRY_UNINSTALL", "0")): return installer.uninstall() diff --git a/tests/test_install_poetry.py b/tests/test_install_poetry.py index b5deaee..15c4e78 100644 --- a/tests/test_install_poetry.py +++ b/tests/test_install_poetry.py @@ -1,9 +1,11 @@ -from unittest.mock import MagicMock, patch +import importlib import re import typing -import importlib import unittest +from unittest.mock import MagicMock +from unittest.mock import patch + module = importlib.import_module("install-poetry") @@ -65,7 +67,7 @@ def test_install_poetry_main__happy(self): force=False, accept_all=True, path=None, - git=None + git=None, ) self.__installer.uninstall.assert_not_called() @@ -79,8 +81,12 @@ def test_install_poetry_main__default_install_error(self): return_code = module.main() - self.__assert_no_matching_message("error", re.compile("a fake poetry installation error")) - self.__assert_any_matching_message("error", re.compile(f"See {self.__tmp_file} for error logs")) + self.__assert_no_matching_message( + "error", re.compile("a fake poetry installation error") + ) + self.__assert_any_matching_message( + "error", re.compile(f"See {self.__tmp_file} for error logs") + ) self.assertEqual(return_code, 1) self.__mock_path_cls(self.__tmp_file).write_text.assert_called_once() @@ -94,7 +100,9 @@ def test_install_poetry_main__stderr_arg(self): return_code = module.main() self.__assert_no_matching_message("info", re.compile("CI environment detected")) - self.__assert_any_matching_message("error", re.compile("a fake poetry installation error")) + self.__assert_any_matching_message( + "error", re.compile("a fake poetry installation error") + ) self.assertEqual(return_code, 1) self.__mock_path_cls.assert_not_called() @@ -108,7 +116,9 @@ def test_install_poetry_main__log_stderr_var(self): return_code = module.main() self.__assert_no_matching_message("info", re.compile("CI environment detected")) - self.__assert_any_matching_message("error", re.compile("a fake poetry installation error")) + self.__assert_any_matching_message( + "error", re.compile("a fake poetry installation error") + ) self.assertEqual(return_code, 1) self.__mock_path_cls.assert_not_called() @@ -121,8 +131,12 @@ def test_install_poetry_main__ci(self): return_code = module.main() - self.__assert_any_matching_message("info", re.compile("CI environment detected")) - self.__assert_any_matching_message("error", re.compile("a fake poetry installation error")) + self.__assert_any_matching_message( + "info", re.compile("CI environment detected") + ) + self.__assert_any_matching_message( + "error", re.compile("a fake poetry installation error") + ) self.assertEqual(return_code, 1) self.__mock_path_cls.assert_not_called() @@ -131,7 +145,9 @@ def __colorize(self, severity: str, message: str) -> str: self.__messages.append((severity, message)) return f"{severity}:{message}" - def __getenv(self, key: str, default: typing.Optional[str] = None) -> typing.Optional[str]: + def __getenv( + self, key: str, default: typing.Optional[str] = None + ) -> typing.Optional[str]: return self.__env.get(key, default) def __mkstemp(self, suffix="suffix", prefix="prefix", dir=None, text=None): @@ -145,4 +161,10 @@ def __assert_no_matching_message(self, severity: str, pattern: re.Pattern): self.assertEqual(self.__count_matching_message(severity, pattern), 0) def __count_matching_message(self, severity: str, pattern: re.Pattern): - return len([message for message in self.__messages if severity == message[0] and pattern.search(message[1])]) + return len( + [ + message + for message in self.__messages + if severity == message[0] and pattern.search(message[1]) + ] + ) From 1c9f9a29ce424b296c9f4593192abf458f34d1aa Mon Sep 17 00:00:00 2001 From: Brian Lee Date: Fri, 4 Nov 2022 18:05:20 -0700 Subject: [PATCH 3/5] ci: adjust unit test step name --- .github/workflows/installer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index 7806416..9c7f390 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -20,7 +20,7 @@ concurrency: jobs: unit-test: - name: ${{ matrix.python-version }} + name: Unit Test / Ubuntu / Python ${{ matrix.python-version }} runs-on: Ubuntu strategy: matrix: From 9cce60389550281e2982f084f9099843d5701966 Mon Sep 17 00:00:00 2001 From: Brian Lee Date: Fri, 4 Nov 2022 18:21:01 -0700 Subject: [PATCH 4/5] ci: fix unit-test image --- .github/workflows/installer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index 9c7f390..d356dbb 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -21,7 +21,7 @@ concurrency: jobs: unit-test: name: Unit Test / Ubuntu / Python ${{ matrix.python-version }} - runs-on: Ubuntu + runs-on: ubuntu-22.04 strategy: matrix: python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] From 652fa1efc0f1b5fa8d85abe9634dec2432b65544 Mon Sep 17 00:00:00 2001 From: Brian Lee Date: Fri, 4 Nov 2022 18:59:32 -0700 Subject: [PATCH 5/5] ci: remove 3.11 from unit test matrix --- .github/workflows/installer.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index d356dbb..0273068 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -24,7 +24,8 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] + # using importlib to import hyphenated modules doesn't seem to work in Python 3.11 right now + python-version: [ "3.7", "3.8", "3.9", "3.10" ] fail-fast: false steps: - uses: actions/checkout@v3