Skip to content

feat: log stderr #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/installer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
unit-test:
name: Unit Test / Ubuntu / Python ${{ matrix.python-version }}
runs-on: ubuntu-22.04
strategy:
matrix:
# 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

- 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 }}
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 32 additions & 8 deletions install-poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,13 @@ 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()

Expand All @@ -912,6 +919,16 @@ 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()

Expand All @@ -923,15 +940,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

Expand Down
170 changes: 170 additions & 0 deletions tests/test_install_poetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import importlib
import re
import typing
import unittest

from unittest.mock import MagicMock
from unittest.mock import patch


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])
]
)