Skip to content
This repository was archived by the owner on Sep 13, 2023. It is now read-only.

Commit 0dbe116

Browse files
Fix/stderr (#410)
* implemented context manager for printing to stderr * modified cli to print errors to stderr * updated the runner to split output to stdout and stderr * updated cli tests to read from result.stdout and result.stderr as opposed to result.output * added tests to ensure exceptions and mlem errors are printed to stderr * typo correction - thrown to throws * added test for stderr_echo context manager to fix warning from codecov * fix dvc test * fix dvc test Co-authored-by: Alexander Guschin <1aguschin@gmail.com>
1 parent 51f328a commit 0dbe116

17 files changed

+243
-50
lines changed

mlem/cli/main.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@
2323
from mlem.core.metadata import load_meta
2424
from mlem.core.objects import MlemObject
2525
from mlem.telemetry import telemetry
26-
from mlem.ui import EMOJI_FAIL, EMOJI_MLEM, bold, cli_echo, color, echo
26+
from mlem.ui import (
27+
EMOJI_FAIL,
28+
EMOJI_MLEM,
29+
bold,
30+
cli_echo,
31+
color,
32+
echo,
33+
stderr_echo,
34+
)
2735

2836

2937
class MlemFormatter(HelpFormatter):
@@ -289,22 +297,22 @@ def inner(ctx, *iargs, **ikwargs):
289297
error = f"{e.__class__.__module__}.{e.__class__.__name__}"
290298
if ctx.obj["traceback"]:
291299
raise
292-
with cli_echo():
300+
with stderr_echo():
293301
echo(EMOJI_FAIL + color(str(e), col=typer.colors.RED))
294302
raise typer.Exit(1)
295303
except ValidationError as e:
296304
error = f"{e.__class__.__module__}.{e.__class__.__name__}"
297305
if ctx.obj["traceback"]:
298306
raise
299307
msgs = "\n".join(_format_validation_error(e))
300-
with cli_echo():
308+
with stderr_echo():
301309
echo(EMOJI_FAIL + color("Error:\n", "red") + msgs)
302310
raise typer.Exit(1)
303311
except Exception as e: # pylint: disable=broad-except
304312
error = f"{e.__class__.__module__}.{e.__class__.__name__}"
305313
if ctx.obj["traceback"]:
306314
raise
307-
with cli_echo():
315+
with stderr_echo():
308316
echo(
309317
EMOJI_FAIL
310318
+ color(

mlem/ui.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from mlem.config import LOCAL_CONFIG
1111

1212
console = Console()
13+
error_console = Console(stderr=True)
1314

1415
_echo_func: Optional[Callable] = None
1516
_offset: int = 0
@@ -46,6 +47,12 @@ def cli_echo():
4647
yield
4748

4849

50+
@contextlib.contextmanager
51+
def stderr_echo():
52+
with set_echo(error_console.print):
53+
yield
54+
55+
4956
@contextlib.contextmanager
5057
def no_echo():
5158
with set_echo(None):

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"pyarrow",
5151
"skl2onnx",
5252
"dvc[s3]",
53+
"importlib_metadata",
5354
]
5455

5556
extras = {

tests/cli/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class Runner:
99
def __init__(self):
10-
self._runner = CliRunner()
10+
self._runner = CliRunner(mix_stderr=False)
1111

1212
def invoke(self, *args, **kwargs) -> Result:
1313
return self._runner.invoke(app, *args, **kwargs)

tests/cli/test_apply.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ def test_apply(runner, model_path, data_path):
3838
"--no-index",
3939
],
4040
)
41-
assert result.exit_code == 0, (result.output, result.exception)
41+
assert result.exit_code == 0, (
42+
result.stdout,
43+
result.stderr,
44+
result.exception,
45+
)
4246
predictions = load(path)
4347
assert isinstance(predictions, ndarray)
4448

@@ -85,7 +89,11 @@ def test_apply_batch(runner, model_path_batch, data_path_batch):
8589
"5",
8690
],
8791
)
88-
assert result.exit_code == 0, (result.output, result.exception)
92+
assert result.exit_code == 0, (
93+
result.stdout,
94+
result.stderr,
95+
result.exception,
96+
)
8997
predictions_meta = load_meta(
9098
path, load_value=True, force_type=MlemData
9199
)
@@ -116,7 +124,11 @@ def test_apply_with_import(runner, model_meta_saved_single, tmp_path_factory):
116124
"pandas[csv]",
117125
],
118126
)
119-
assert result.exit_code == 0, (result.output, result.exception)
127+
assert result.exit_code == 0, (
128+
result.stdout,
129+
result.stderr,
130+
result.exception,
131+
)
120132
predictions = load(path)
121133
assert isinstance(predictions, ndarray)
122134

@@ -146,19 +158,27 @@ def test_apply_batch_with_import(
146158
"2",
147159
],
148160
)
149-
assert result.exit_code == 1, (result.output, result.exception)
161+
assert result.exit_code == 1, (
162+
result.stdout,
163+
result.stderr,
164+
result.exception,
165+
)
150166
assert (
151167
"Batch data loading is currently not supported for loading data on-the-fly"
152-
in result.output
168+
in result.stderr
153169
)
154170

155171

156172
def test_apply_no_output(runner, model_path, data_path):
157173
result = runner.invoke(
158174
["apply", model_path, data_path, "-m", "predict", "--no-index"],
159175
)
160-
assert result.exit_code == 0, (result.output, result.exception)
161-
assert len(result.output) > 0
176+
assert result.exit_code == 0, (
177+
result.stdout,
178+
result.stderr,
179+
result.exception,
180+
)
181+
assert len(result.stdout) > 0
162182

163183

164184
def test_apply_fails_without_mlem_dir(runner, model_path, data_path):
@@ -176,7 +196,11 @@ def test_apply_fails_without_mlem_dir(runner, model_path, data_path):
176196
"--index",
177197
],
178198
)
179-
assert result.exit_code == 1, (result.output, result.exception)
199+
assert result.exit_code == 1, (
200+
result.stdout,
201+
result.stderr,
202+
result.exception,
203+
)
180204
assert isinstance(result.exception, MlemProjectNotFound)
181205

182206

@@ -206,7 +230,11 @@ def test_apply_from_remote(runner, current_test_branch, s3_tmp_path):
206230
"--no-index",
207231
],
208232
)
209-
assert result.exit_code == 0, (result.output, result.exception)
233+
assert result.exit_code == 0, (
234+
result.stdout,
235+
result.stderr,
236+
result.exception,
237+
)
210238
predictions = load(out)
211239
assert isinstance(predictions, ndarray)
212240

@@ -227,6 +255,10 @@ def test_apply_remote(mlem_client, runner, data_path):
227255
path,
228256
],
229257
)
230-
assert result.exit_code == 0, (result.output, result.exception)
258+
assert result.exit_code == 0, (
259+
result.stdout,
260+
result.stderr,
261+
result.exception,
262+
)
231263
predictions = load(path)
232264
assert isinstance(predictions, ndarray)

tests/cli/test_build.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ def test_build(runner: Runner, model_meta_saved_single, tmp_path):
2121
f"build {make_posix(model_meta_saved_single.loc.uri)} -c target={make_posix(path)} mock"
2222
)
2323

24-
assert result.exit_code == 0, (result.exception, result.output)
24+
assert result.exit_code == 0, (
25+
result.stdout,
26+
result.stderr,
27+
result.exception,
28+
)
2529

2630
with open(path, encoding="utf8") as f:
2731
assert f.read().strip() == model_meta_saved_single.loc.path

tests/cli/test_checkenv.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ def test_checkenv(runner, model_path_mlem_project):
77
result = runner.invoke(
88
["checkenv", model_path],
99
)
10-
assert result.exit_code == 0, (result.output, result.exception)
10+
assert result.exit_code == 0, (
11+
result.stdout,
12+
result.stderr,
13+
result.exception,
14+
)
1115

1216
meta = load_meta(model_path, load_value=False, force_type=MlemModel)
1317
meta.requirements.__root__[0].version = "asdad"
@@ -16,4 +20,8 @@ def test_checkenv(runner, model_path_mlem_project):
1620
result = runner.invoke(
1721
["checkenv", model_path],
1822
)
19-
assert result.exit_code == 1, (result.output, result.exception)
23+
assert result.exit_code == 1, (
24+
result.stdout,
25+
result.stderr,
26+
result.exception,
27+
)

tests/cli/test_clone.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,9 @@ def test_model_cloning(runner: Runner, model_path):
99
with tempfile.TemporaryDirectory() as path:
1010
path = posixpath.join(path, "cloned")
1111
result = runner.invoke(["clone", model_path, path, "--no-index"])
12-
assert result.exit_code == 0, (result.exception, result.output)
12+
assert result.exit_code == 0, (
13+
result.stdout,
14+
result.stderr,
15+
result.exception,
16+
)
1317
load_meta(path, load_value=False)

tests/cli/test_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_set_get_validation(runner: Runner, mlem_project):
5050
assert result.exit_code == 1
5151
assert (
5252
isinstance(result.exception, ValidationError)
53-
or "extra fields not permitted" in result.output
53+
or "extra fields not permitted" in result.stderr
5454
), traceback.format_exception(
5555
type(result.exception),
5656
result.exception,
@@ -64,7 +64,7 @@ def test_set_get_validation(runner: Runner, mlem_project):
6464
assert result.exit_code == 1
6565
assert (
6666
isinstance(result.exception, MlemError)
67-
or "[name] should contain at least one dot" in result.output
67+
or "[name] should contain at least one dot" in result.stderr
6868
), traceback.format_exception(
6969
type(result.exception),
7070
result.exception,

tests/cli/test_deployment.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ def test_deploy_create_new(
8484
result = runner.invoke(
8585
f"deploy run {path} -m {model_meta_saved_single.loc.uri} -t {mock_env_path} -c param=aaa".split()
8686
)
87-
assert result.exit_code == 0, result.output
87+
assert result.exit_code == 0, (
88+
result.stdout,
89+
result.stderr,
90+
result.exception,
91+
)
8892
assert os.path.isfile(path + MLEM_EXT)
8993
meta = load_meta(path)
9094
assert isinstance(meta, MlemDeploymentMock)
@@ -94,7 +98,11 @@ def test_deploy_create_new(
9498

9599
def test_deploy_create_existing(runner: Runner, mock_deploy_path):
96100
result = runner.invoke(f"deploy run {mock_deploy_path}".split())
97-
assert result.exit_code == 0, result.output
101+
assert result.exit_code == 0, (
102+
result.stdout,
103+
result.stderr,
104+
result.exception,
105+
)
98106
meta = load_meta(mock_deploy_path)
99107
assert isinstance(meta, MlemDeploymentMock)
100108
assert meta.param == "bbb"
@@ -103,13 +111,21 @@ def test_deploy_create_existing(runner: Runner, mock_deploy_path):
103111

104112
def test_deploy_status(runner: Runner, mock_deploy_path):
105113
result = runner.invoke(f"deploy status {mock_deploy_path}".split())
106-
assert result.exit_code == 0, result.output
107-
assert result.output.strip() == DeployStatus.NOT_DEPLOYED.value
114+
assert result.exit_code == 0, (
115+
result.stdout,
116+
result.stderr,
117+
result.exception,
118+
)
119+
assert result.stdout.strip() == DeployStatus.NOT_DEPLOYED.value
108120

109121

110122
def test_deploy_remove(runner: Runner, mock_deploy_path):
111123
result = runner.invoke(f"deploy remove {mock_deploy_path}".split())
112-
assert result.exit_code == 0, result.output
124+
assert result.exit_code == 0, (
125+
result.stdout,
126+
result.stderr,
127+
result.exception,
128+
)
113129
meta = load_meta(mock_deploy_path)
114130
assert isinstance(meta, MlemDeploymentMock)
115131
assert meta.status == DeployStatus.STOPPED
@@ -126,7 +142,11 @@ def test_deploy_apply(
126142
result = runner.invoke(
127143
f"deploy apply {mock_deploy_path} {data_path} -o {path}".split()
128144
)
129-
assert result.exit_code == 0, result.output
145+
assert result.exit_code == 0, (
146+
result.stdout,
147+
result.stderr,
148+
result.exception,
149+
)
130150
meta = load_meta(mock_deploy_path)
131151
assert isinstance(meta, MlemDeploymentMock)
132152
assert meta.status == DeployStatus.NOT_DEPLOYED

0 commit comments

Comments
 (0)