Skip to content

Commit 94a1fa0

Browse files
authored
Refactor output code (#156)
chore(output_handler): refactor output handling - Remove unnecessary empty methods from OutputHandler - Move code writing to stdout or to the specified file to OutputHandler - Move code computing the exit code to OutputHandler - Rename concrete classes from ${FORMAT}Handler to ${FORMAT}OutputHandler and their files from ${format}_output.py to ${format}_output_handler.py - Add docstrings to OutputHandler methods
1 parent 621a6bb commit 94a1fa0

13 files changed

+85
-77
lines changed

ggshield/cmd.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .hook_cmd import precommit_cmd, prepush_cmd
1414
from .ignore import ignore
1515
from .install import install
16-
from .output import JSONHandler, OutputHandler, TextHandler
16+
from .output import JSONOutputHandler, OutputHandler, TextOutputHandler
1717
from .pre_receive_cmd import prereceive_cmd
1818
from .quota import quota
1919
from .status import status
@@ -150,9 +150,9 @@ def scan(
150150
if max_commits:
151151
config.max_commits_for_hook = max_commits
152152

153-
output_handler_cls: Type[OutputHandler] = TextHandler
153+
output_handler_cls: Type[OutputHandler] = TextOutputHandler
154154
if json_output:
155-
output_handler_cls = JSONHandler
155+
output_handler_cls = JSONOutputHandler
156156

157157
ctx.obj["output_handler"] = output_handler_cls(
158158
show_secrets=config.show_secrets, verbose=config.verbose, output=output

ggshield/dev_scan.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def path_cmd(
176176
)
177177
scan = ScanCollection(id=" ".join(paths), type="path_scan", results=results)
178178

179-
return output_handler.process_scan(scan)[1]
179+
return output_handler.process_scan(scan)
180180
except Exception as error:
181181
return handle_exception(error, config.verbose)
182182

@@ -260,5 +260,5 @@ def scan_commit_range(
260260

261261
return_code = output_handler.process_scan(
262262
ScanCollection(id=scan_id, type="commit-range", scans=scans)
263-
)[1]
263+
)
264264
return return_code

ggshield/docker.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def docker_name_cmd(ctx: click.Context, name: str) -> int:
127127
banlisted_detectors=config.banlisted_detectors,
128128
)
129129

130-
return output_handler.process_scan(scan)[1]
130+
return output_handler.process_scan(scan)
131131
except Exception as error:
132132
return handle_exception(error, config.verbose)
133133

@@ -161,6 +161,6 @@ def docker_archive_cmd(
161161
banlisted_detectors=config.banlisted_detectors,
162162
)
163163

164-
return output_handler.process_scan(scan)[1]
164+
return output_handler.process_scan(scan)
165165
except Exception as error:
166166
return handle_exception(error, config.verbose)

ggshield/hook_cmd.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import click
66

77
from ggshield.dev_scan import scan_commit_range
8-
from ggshield.output import TextHandler
8+
from ggshield.output import TextOutputHandler
99
from ggshield.scan import Commit, ScanCollection
1010
from ggshield.utils import EMPTY_SHA, EMPTY_TREE, SupportedScanMode, handle_exception
1111

@@ -22,7 +22,7 @@ def precommit_cmd(
2222
scan as a pre-commit git hook.
2323
"""
2424
config = ctx.obj["config"]
25-
output_handler = TextHandler(
25+
output_handler = TextOutputHandler(
2626
show_secrets=config.show_secrets, verbose=config.verbose, output=None
2727
)
2828
try:
@@ -39,7 +39,7 @@ def precommit_cmd(
3939

4040
return output_handler.process_scan(
4141
ScanCollection(id="cached", type="pre-commit", results=results)
42-
)[1]
42+
)
4343
except Exception as error:
4444
return handle_exception(error, config.verbose)
4545

ggshield/output/__init__.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from .json import JSONHandler
1+
from .json import JSONOutputHandler
22
from .output_handler import OutputHandler
3-
from .text import TextHandler
3+
from .text import TextOutputHandler
44

55

6-
__all__ = ["TextHandler", "OutputHandler", "JSONHandler"]
6+
__all__ = ["TextOutputHandler", "OutputHandler", "JSONOutputHandler"]

ggshield/output/json/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .json_output import JSONHandler
1+
from .json_output_handler import JSONOutputHandler
22

33

4-
__all__ = ["JSONHandler"]
4+
__all__ = ["JSONOutputHandler"]

ggshield/output/json/json_output.py ggshield/output/json/json_output_handler.py

+15-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from typing import Any, Dict, List, Tuple
1+
from typing import Any, Dict, List, cast
22

3-
import click
43
from pygitguardian.client import VERSIONS
54
from pygitguardian.models import Match, PolicyBreak
65

@@ -13,26 +12,29 @@
1312
from ggshield.utils import Filemode, find_match_indices, get_lines_from_content
1413

1514

16-
class JSONHandler(OutputHandler):
17-
def process_scan(
15+
class JSONOutputHandler(OutputHandler):
16+
def _process_scan_impl(self, scan: ScanCollection) -> str:
17+
scan_dict = self.create_scan_dict(scan, top=True)
18+
text = JSONScanCollectionSchema().dumps(scan_dict)
19+
# dumps() return type is not defined, so cast `text`, otherwise mypy complains
20+
return cast(str, text)
21+
22+
def create_scan_dict(
1823
self, scan: ScanCollection, top: bool = True
19-
) -> Tuple[Dict[str, Any], int]:
24+
) -> Dict[str, Any]:
2025
scan_dict: Dict[str, Any] = {
2126
"id": scan.id,
2227
"type": scan.type,
2328
"total_incidents": 0,
2429
"total_occurrences": 0,
2530
}
26-
return_code = 0
27-
2831
if scan.extra_info:
2932
scan_dict["extra_info"] = scan.extra_info
3033

3134
if top and scan.results:
3235
scan_dict["secrets_engine_version"] = VERSIONS.secrets_engine_version
3336

3437
if scan.results:
35-
return_code = 1
3638
for result in scan.results:
3739
result_dict = self.process_result(result)
3840
scan_dict.setdefault("results", []).append(result_dict)
@@ -41,22 +43,12 @@ def process_scan(
4143

4244
if scan.scans:
4345
for inner_scan in scan.scans_with_results:
44-
inner_scan_dict, inner_return_code = self.process_scan(
45-
inner_scan, top=False
46-
)
46+
inner_scan_dict = self.create_scan_dict(inner_scan, top=False)
4747
scan_dict.setdefault("scans", []).append(inner_scan_dict)
4848
scan_dict["total_incidents"] += inner_scan_dict["total_incidents"]
4949
scan_dict["total_occurrences"] += inner_scan_dict["total_occurrences"]
50-
return_code = max(return_code, inner_return_code)
51-
52-
if top:
53-
if self.output:
54-
with open(self.output, "w+") as f:
55-
f.write(JSONScanCollectionSchema().dumps(scan_dict))
56-
else:
57-
click.echo(JSONScanCollectionSchema().dumps(scan_dict))
5850

59-
return scan_dict, return_code
51+
return scan_dict
6052

6153
def process_result(self, result: Result) -> Dict[str, Any]:
6254
result_dict: Dict[str, Any] = {
@@ -110,7 +102,9 @@ def flattened_policy_break(
110102
flattened_dict["validity"] = policy_breaks[0].validity
111103

112104
for policy_break in policy_breaks:
113-
matches = JSONHandler.make_matches(policy_break.matches, lines, is_patch)
105+
matches = JSONOutputHandler.make_matches(
106+
policy_break.matches, lines, is_patch
107+
)
114108
flattened_dict["occurrences"].extend(matches)
115109

116110
return flattened_dict

ggshield/output/output_handler.py

+38-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
from typing import Any, Optional, Tuple
1+
from abc import ABC, abstractmethod
2+
from typing import Optional
23

3-
from ggshield.scan import Result, ScanCollection
4+
import click
45

6+
from ggshield.scan import ScanCollection
57

6-
class OutputHandler:
8+
9+
class OutputHandler(ABC):
710
show_secrets: bool = False
811
verbose: bool = False
912
output: Optional[str] = None
@@ -13,15 +16,41 @@ def __init__(
1316
show_secrets: bool,
1417
verbose: bool,
1518
output: Optional[str] = None,
16-
*args: Any,
17-
**kwargs: Any,
1819
):
1920
self.show_secrets = show_secrets
2021
self.verbose = verbose
2122
self.output = output
2223

23-
def process_scan(self, scan: ScanCollection, top: bool = True) -> Tuple[Any, int]:
24-
pass
24+
def process_scan(self, scan: ScanCollection) -> int:
25+
"""Process a scan collection, write the report to :attr:`self.output`
26+
27+
:param scan: The scan collection to process
28+
:return: The exit code
29+
"""
30+
text = self._process_scan_impl(scan)
31+
if self.output:
32+
with open(self.output, "w+") as f:
33+
f.write(text)
34+
else:
35+
click.echo(text)
36+
return OutputHandler._get_exit_code(scan)
37+
38+
@abstractmethod
39+
def _process_scan_impl(self, scan: ScanCollection) -> str:
40+
"""Implementation of scan processing,
41+
called by :meth:`OutputHandler.process_scan`
42+
43+
Must return a string for the report.
44+
45+
:param scan: The scan collection to process
46+
:return: The content
47+
"""
48+
raise NotImplementedError()
2549

26-
def process_result(self, result: Result) -> Any:
27-
pass
50+
@staticmethod
51+
def _get_exit_code(scan: ScanCollection) -> int:
52+
if scan.results:
53+
return 1
54+
if scan.scans and any(x.results for x in scan.scans):
55+
return 1
56+
return 0

ggshield/output/text/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .text_output import TextHandler
1+
from .text_output_handler import TextOutputHandler
22

33

4-
__all__ = ["TextHandler"]
4+
__all__ = ["TextOutputHandler"]

ggshield/output/text/text_output.py ggshield/output/text/text_output_handler.py

+6-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from io import StringIO
2-
from typing import ClassVar, List, Tuple
2+
from typing import ClassVar, List
33

44
import click
55
from pygitguardian.models import Match
@@ -34,11 +34,10 @@ def get_offset(padding: int, is_patch: bool = False) -> int:
3434
return len(LINE_DISPLAY["file"].format("0" * padding))
3535

3636

37-
class TextHandler(OutputHandler):
37+
class TextOutputHandler(OutputHandler):
3838
nb_lines: ClassVar[int] = 3
3939

40-
def process_scan(self, scan: ScanCollection, top: bool = True) -> Tuple[str, int]:
41-
return_code = 0
40+
def _process_scan_impl(self, scan: ScanCollection, top: bool = True) -> str:
4241
scan_buf = StringIO()
4342
if scan.optional_header and (scan.results or self.verbose):
4443
scan_buf.write(scan.optional_header)
@@ -47,7 +46,6 @@ def process_scan(self, scan: ScanCollection, top: bool = True) -> Tuple[str, int
4746
scan_buf.write(secrets_engine_version())
4847

4948
if scan.results:
50-
return_code = 1
5149
for result in scan.results:
5250
scan_buf.write(self.process_result(result))
5351
else:
@@ -61,21 +59,10 @@ def process_scan(self, scan: ScanCollection, top: bool = True) -> Tuple[str, int
6159

6260
if scan.scans:
6361
for sub_scan in scan.scans:
64-
inner_scan_str, inner_return_code = self.process_scan(
65-
sub_scan, top=False
66-
)
62+
inner_scan_str = self._process_scan_impl(sub_scan, top=False)
6763
scan_buf.write(inner_scan_str)
68-
return_code = max(return_code, inner_return_code)
69-
70-
scan_str = scan_buf.getvalue()
71-
if top:
72-
if self.output:
73-
with open(self.output, "w+") as f:
74-
click.echo(scan_str, file=f)
75-
else:
76-
click.echo(scan_str)
7764

78-
return scan_str, return_code
65+
return scan_buf.getvalue()
7966

8067
def process_result(self, result: Result) -> str:
8168
"""
@@ -111,7 +98,7 @@ def process_result(self, result: Result) -> str:
11198
for issue_n, (ignore_sha, policy_breaks) in enumerate(sha_dict.items(), 1):
11299
result_buf.write(policy_break_header(issue_n, policy_breaks, ignore_sha))
113100
for policy_break in policy_breaks:
114-
policy_break.matches = TextHandler.make_matches(
101+
policy_break.matches = TextOutputHandler.make_matches(
115102
policy_break.matches, lines, is_patch
116103
)
117104

tests/output/test_json_output.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from ggshield.output import JSONHandler
5+
from ggshield.output import JSONOutputHandler, OutputHandler
66
from ggshield.output.json.schemas import JSONScanCollectionSchema
77
from ggshield.scan import Commit, ScanCollection
88
from ggshield.utils import Filemode, SupportedScanMode
@@ -53,7 +53,7 @@
5353
def test_json_output(client, cache, name, input_patch, expected, snapshot):
5454
c = Commit()
5555
c._patch = input_patch
56-
handler = JSONHandler(verbose=True, show_secrets=False)
56+
handler = JSONOutputHandler(verbose=True, show_secrets=False)
5757

5858
with my_vcr.use_cassette(name):
5959
results = c.scan(
@@ -66,10 +66,9 @@ def test_json_output(client, cache, name, input_patch, expected, snapshot):
6666
banlisted_detectors=None,
6767
)
6868

69-
flat_results, exit_code = handler.process_scan(
70-
scan=ScanCollection(id="path", type="test", results=results), top=True
71-
)
69+
scan = ScanCollection(id="path", type="test", results=results)
70+
json_flat_results = handler._process_scan_impl(scan)
71+
exit_code = OutputHandler._get_exit_code(scan)
7272

7373
assert exit_code == expected
74-
json_flat_results = JSONScanCollectionSchema().dumps(flat_results)
7574
snapshot.assert_match(JSONScanCollectionSchema().loads(json_flat_results))

tests/output/test_message.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
format_line_count_break,
88
no_leak_message,
99
)
10-
from ggshield.output.text.text_output import get_offset, get_padding
10+
from ggshield.output.text.text_output_handler import get_offset, get_padding
1111
from ggshield.text_utils import Line
1212

1313

tests/output/test_text_output.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from ggshield.output import TextHandler
5+
from ggshield.output import TextOutputHandler
66
from ggshield.scan import Result
77
from ggshield.scan.scannable import ScanCollection
88
from ggshield.utils import Filemode
@@ -90,9 +90,9 @@
9090
],
9191
)
9292
def test_leak_message(result_input, snapshot, show_secrets, verbose):
93-
output_handler = TextHandler(show_secrets=show_secrets, verbose=verbose)
93+
output_handler = TextOutputHandler(show_secrets=show_secrets, verbose=verbose)
9494
new_result = deepcopy(result_input)
95-
output, exit_code = output_handler.process_scan(
95+
output = output_handler._process_scan_impl(
9696
ScanCollection(
9797
id="scan",
9898
type="test",
@@ -101,5 +101,4 @@ def test_leak_message(result_input, snapshot, show_secrets, verbose):
101101
)
102102
)
103103

104-
assert exit_code == 1
105104
snapshot.assert_match(output)

0 commit comments

Comments
 (0)