Skip to content

Commit 4e978af

Browse files
authored
Use Rich for check output (#874)
* Use print instead of output_stream * Use logging for errors and warnings * Add color to status * Update tests * Update changelog
1 parent 0f7a68b commit 4e978af

File tree

5 files changed

+96
-64
lines changed

5 files changed

+96
-64
lines changed
File renamed without changes.

changelog/874.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use Rich to add color to ``check`` output.

tests/test_check.py

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
import io
14+
import logging
1515

1616
import pretend
1717
import pytest
@@ -45,24 +45,27 @@ def test_str_representation(self):
4545
assert str(self.stream) == "result"
4646

4747

48-
def test_check_no_distributions(monkeypatch):
49-
stream = io.StringIO()
50-
48+
def test_check_no_distributions(monkeypatch, caplog):
5149
monkeypatch.setattr(commands, "_find_dists", lambda a: [])
5250

53-
assert not check.check(["dist/*"], output_stream=stream)
54-
assert stream.getvalue() == "No files to check.\n"
51+
assert not check.check(["dist/*"])
52+
assert caplog.record_tuples == [
53+
(
54+
"twine.commands.check",
55+
logging.ERROR,
56+
"No files to check.",
57+
),
58+
]
5559

5660

57-
def test_check_passing_distribution(monkeypatch):
61+
def test_check_passing_distribution(monkeypatch, capsys):
5862
renderer = pretend.stub(render=pretend.call_recorder(lambda *a, **kw: "valid"))
5963
package = pretend.stub(
6064
metadata_dictionary=lambda: {
6165
"description": "blah",
6266
"description_content_type": "text/markdown",
6367
}
6468
)
65-
output_stream = io.StringIO()
6669
warning_stream = ""
6770

6871
monkeypatch.setattr(check, "_RENDERERS", {None: renderer})
@@ -74,13 +77,17 @@ def test_check_passing_distribution(monkeypatch):
7477
)
7578
monkeypatch.setattr(check, "_WarningStream", lambda: warning_stream)
7679

77-
assert not check.check(["dist/*"], output_stream=output_stream)
78-
assert output_stream.getvalue() == "Checking dist/dist.tar.gz: PASSED\n"
80+
assert not check.check(["dist/*"])
81+
assert capsys.readouterr().out == "Checking dist/dist.tar.gz: PASSED\n"
7982
assert renderer.render.calls == [pretend.call("blah", stream=warning_stream)]
8083

8184

8285
@pytest.mark.parametrize("content_type", ["text/plain", "text/markdown"])
83-
def test_check_passing_distribution_with_none_renderer(content_type, monkeypatch):
86+
def test_check_passing_distribution_with_none_renderer(
87+
content_type,
88+
monkeypatch,
89+
capsys,
90+
):
8491
"""Pass when rendering a content type can't fail."""
8592
package = pretend.stub(
8693
metadata_dictionary=lambda: {
@@ -96,12 +103,11 @@ def test_check_passing_distribution_with_none_renderer(content_type, monkeypatch
96103
pretend.stub(from_filename=lambda *a, **kw: package),
97104
)
98105

99-
output_stream = io.StringIO()
100-
assert not check.check(["dist/*"], output_stream=output_stream)
101-
assert output_stream.getvalue() == "Checking dist/dist.tar.gz: PASSED\n"
106+
assert not check.check(["dist/*"])
107+
assert capsys.readouterr().out == "Checking dist/dist.tar.gz: PASSED\n"
102108

103109

104-
def test_check_no_description(monkeypatch, capsys):
110+
def test_check_no_description(monkeypatch, capsys, caplog):
105111
package = pretend.stub(
106112
metadata_dictionary=lambda: {
107113
"description": None,
@@ -116,18 +122,26 @@ def test_check_no_description(monkeypatch, capsys):
116122
pretend.stub(from_filename=lambda *a, **kw: package),
117123
)
118124

119-
# used to crash with `AttributeError`
120-
output_stream = io.StringIO()
121-
assert not check.check(["dist/*"], output_stream=output_stream)
122-
assert output_stream.getvalue() == (
123-
"Checking dist/dist.tar.gz: PASSED, with warnings\n"
124-
" warning: `long_description_content_type` missing. "
125-
"defaulting to `text/x-rst`.\n"
126-
" warning: `long_description` missing.\n"
127-
)
128-
125+
assert not check.check(["dist/*"])
129126

130-
def test_strict_fails_on_warnings(monkeypatch, capsys):
127+
assert capsys.readouterr().out == (
128+
"Checking dist/dist.tar.gz: PASSED with warnings\n"
129+
)
130+
assert caplog.record_tuples == [
131+
(
132+
"twine.commands.check",
133+
logging.WARNING,
134+
"`long_description_content_type` missing. defaulting to `text/x-rst`.",
135+
),
136+
(
137+
"twine.commands.check",
138+
logging.WARNING,
139+
"`long_description` missing.",
140+
),
141+
]
142+
143+
144+
def test_strict_fails_on_warnings(monkeypatch, capsys, caplog):
131145
package = pretend.stub(
132146
metadata_dictionary=lambda: {
133147
"description": None,
@@ -142,27 +156,34 @@ def test_strict_fails_on_warnings(monkeypatch, capsys):
142156
pretend.stub(from_filename=lambda *a, **kw: package),
143157
)
144158

145-
# used to crash with `AttributeError`
146-
output_stream = io.StringIO()
147-
assert check.check(["dist/*"], output_stream=output_stream, strict=True)
148-
assert output_stream.getvalue() == (
149-
"Checking dist/dist.tar.gz: FAILED, due to warnings\n"
150-
" warning: `long_description_content_type` missing. "
151-
"defaulting to `text/x-rst`.\n"
152-
" warning: `long_description` missing.\n"
153-
)
154-
159+
assert check.check(["dist/*"], strict=True)
155160

156-
def test_check_failing_distribution(monkeypatch):
161+
assert capsys.readouterr().out == (
162+
"Checking dist/dist.tar.gz: FAILED due to warnings\n"
163+
)
164+
assert caplog.record_tuples == [
165+
(
166+
"twine.commands.check",
167+
logging.WARNING,
168+
"`long_description_content_type` missing. defaulting to `text/x-rst`.",
169+
),
170+
(
171+
"twine.commands.check",
172+
logging.WARNING,
173+
"`long_description` missing.",
174+
),
175+
]
176+
177+
178+
def test_check_failing_distribution(monkeypatch, capsys, caplog):
157179
renderer = pretend.stub(render=pretend.call_recorder(lambda *a, **kw: None))
158180
package = pretend.stub(
159181
metadata_dictionary=lambda: {
160182
"description": "blah",
161183
"description_content_type": "text/markdown",
162184
}
163185
)
164-
output_stream = io.StringIO()
165-
warning_stream = "WARNING"
186+
warning_stream = "Syntax error"
166187

167188
monkeypatch.setattr(check, "_RENDERERS", {None: renderer})
168189
monkeypatch.setattr(commands, "_find_dists", lambda a: ["dist/dist.tar.gz"])
@@ -173,13 +194,17 @@ def test_check_failing_distribution(monkeypatch):
173194
)
174195
monkeypatch.setattr(check, "_WarningStream", lambda: warning_stream)
175196

176-
assert check.check(["dist/*"], output_stream=output_stream)
177-
assert output_stream.getvalue() == (
178-
"Checking dist/dist.tar.gz: FAILED\n"
179-
" `long_description` has syntax errors in markup and would not be "
180-
"rendered on PyPI.\n"
181-
" WARNING"
182-
)
197+
assert check.check(["dist/*"])
198+
199+
assert capsys.readouterr().out == "Checking dist/dist.tar.gz: FAILED\n"
200+
assert caplog.record_tuples == [
201+
(
202+
"twine.commands.check",
203+
logging.ERROR,
204+
"`long_description` has syntax errors in markup and would not be rendered "
205+
"on PyPI.\nSyntax error",
206+
),
207+
]
183208
assert renderer.render.calls == [pretend.call("blah", stream=warning_stream)]
184209

185210

@@ -190,3 +215,8 @@ def test_main(monkeypatch):
190215

191216
assert check.main(["dist/*"]) == check_result
192217
assert check_stub.calls == [pretend.call(["dist/*"], strict=False)]
218+
219+
220+
# TODO: Test print() color output
221+
222+
# TODO: Test log formatting

twine/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def configure_output() -> None:
3737
# doesn't support that (https://github.com/Textualize/rich/issues/343).
3838
force_terminal=True,
3939
no_color=getattr(args, "no_color", False),
40+
highlight=False,
4041
theme=rich.theme.Theme(
4142
{
4243
"logging.level.debug": "green",

twine/commands/check.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@
1515
import argparse
1616
import cgi
1717
import io
18+
import logging
1819
import re
19-
import sys
20-
import textwrap
21-
from typing import IO, List, Optional, Tuple, cast
20+
from typing import List, Optional, Tuple, cast
2221

2322
import readme_renderer.rst
23+
from rich import print
2424

2525
from twine import commands
2626
from twine import package as package_file
2727

28+
logger = logging.getLogger(__name__)
29+
30+
2831
_RENDERERS = {
2932
None: readme_renderer.rst, # Default if description_content_type is None
3033
"text/plain": None, # Rendering cannot fail
@@ -65,7 +68,7 @@ def write(self, text: str) -> None:
6568
)
6669

6770
def __str__(self) -> str:
68-
return self.output.getvalue()
71+
return self.output.getvalue().strip()
6972

7073

7174
def _check_file(
@@ -104,7 +107,6 @@ def _check_file(
104107

105108
def check(
106109
dists: List[str],
107-
output_stream: IO[str] = sys.stdout,
108110
strict: bool = False,
109111
) -> bool:
110112
"""Check that a distribution will render correctly on PyPI and display the results.
@@ -124,39 +126,37 @@ def check(
124126
"""
125127
uploads = [i for i in commands._find_dists(dists) if not i.endswith(".asc")]
126128
if not uploads: # Return early, if there are no files to check.
127-
output_stream.write("No files to check.\n")
129+
logger.error("No files to check.")
128130
return False
129131

130132
failure = False
131133

132134
for filename in uploads:
133-
output_stream.write("Checking %s: " % filename)
135+
print(f"Checking {filename}: ", end="")
134136
render_warning_stream = _WarningStream()
135137
warnings, is_ok = _check_file(filename, render_warning_stream)
136138

137139
# Print the status and/or error
138140
if not is_ok:
139141
failure = True
140-
output_stream.write("FAILED\n")
141-
142-
error_text = (
143-
"`long_description` has syntax errors in markup and "
144-
"would not be rendered on PyPI.\n"
142+
print("[red]FAILED[/red]")
143+
logger.error(
144+
"`long_description` has syntax errors in markup"
145+
" and would not be rendered on PyPI."
146+
f"\n{render_warning_stream}"
145147
)
146-
output_stream.write(textwrap.indent(error_text, " "))
147-
output_stream.write(textwrap.indent(str(render_warning_stream), " "))
148148
elif warnings:
149149
if strict:
150150
failure = True
151-
output_stream.write("FAILED, due to warnings\n")
151+
print("[red]FAILED due to warnings[/red]")
152152
else:
153-
output_stream.write("PASSED, with warnings\n")
153+
print("[yellow]PASSED with warnings[/yellow]")
154154
else:
155-
output_stream.write("PASSED\n")
155+
print("[green]PASSED[/green]")
156156

157157
# Print warnings after the status and/or error
158158
for message in warnings:
159-
output_stream.write(" warning: " + message + "\n")
159+
logger.warning(message)
160160

161161
return failure
162162

0 commit comments

Comments
 (0)