Skip to content

Commit c681012

Browse files
authored
Merge pull request #142 from Pennycook/cli
Move MetaWarning classes into new module
2 parents 00f4a4f + 9108dce commit c681012

File tree

6 files changed

+437
-96
lines changed

6 files changed

+437
-96
lines changed

codebasin/__main__.py

Lines changed: 2 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
import argparse
99
import logging
1010
import os
11-
import re
1211
import sys
1312

1413
from codebasin import CodeBase, config, finder, report, util
14+
from codebasin._detail.logging import Formatter, WarningAggregator
1515

1616
log = logging.getLogger("codebasin")
1717
version = "1.2.0"
@@ -54,100 +54,6 @@ def _help_string(*lines: str, is_long=False, is_last=False):
5454
return result
5555

5656

57-
class Formatter(logging.Formatter):
58-
def __init__(self, *, colors: bool = False):
59-
self.colors = colors
60-
61-
def format(self, record: logging.LogRecord) -> str:
62-
msg = record.msg
63-
level = record.levelname.lower()
64-
65-
# Display info messages with no special formatting.
66-
if level == "info":
67-
return f"{msg}"
68-
69-
# Drop colors if requested.
70-
if not self.colors:
71-
return f"{level}: {msg}"
72-
73-
# Otherwise, use ASCII codes to improve readability.
74-
BOLD = "\033[1m"
75-
DEFAULT = "\033[39m"
76-
YELLOW = "\033[93m"
77-
RED = "\033[91m"
78-
RESET = "\033[0m"
79-
80-
if level == "warning":
81-
color = YELLOW
82-
elif level == "error":
83-
color = RED
84-
else:
85-
color = DEFAULT
86-
return f"{BOLD}{color}{level}{RESET}: {msg}"
87-
88-
89-
class MetaWarning:
90-
"""
91-
A MetaWarning is used to represent multiple warnings, and provide suggested
92-
actions to the user.
93-
"""
94-
95-
def __init__(self, regex: str, msg: str):
96-
self.regex = re.compile(regex)
97-
self.msg = msg
98-
self._count = 0
99-
100-
def inspect(self, record: logging.LogRecord):
101-
if self.regex.search(record.msg):
102-
self._count += 1
103-
104-
def warn(self):
105-
if self._count == 0:
106-
return
107-
log.warning(self.msg.format(self._count))
108-
109-
110-
class WarningAggregator(logging.Filter):
111-
"""
112-
Inspect warnings to generate meta-warnings and statistics.
113-
"""
114-
115-
def __init__(self):
116-
self.meta_warnings = [
117-
MetaWarning(".", "{} warnings generated during preprocessing."),
118-
MetaWarning(
119-
"user include",
120-
"{} user include files could not be found.\n"
121-
+ " These could contain important macros and includes.\n"
122-
+ " Suggested solutions:\n"
123-
+ " - Check that the file(s) exist in the code base.\n"
124-
+ " - Check the include paths in the compilation database.\n"
125-
+ " - Check if the include(s) should have used '<>'.",
126-
),
127-
MetaWarning(
128-
"system include",
129-
"{} system include files could not be found.\n"
130-
+ " These could define important feature macros.\n"
131-
+ " Suggested solutions:\n"
132-
+ " - Check that the file(s) exist on your system.\n"
133-
+ " - Use .cbi/config to define system include paths.\n"
134-
+ " - Use .cbi/config to define important macros.",
135-
),
136-
]
137-
138-
def filter(self, record: logging.LogRecord) -> bool:
139-
if record.levelno == logging.WARNING:
140-
for meta_warning in self.meta_warnings:
141-
meta_warning.inspect(record)
142-
143-
# Do not filter anything.
144-
return True
145-
146-
def warn(self):
147-
for meta_warning in self.meta_warnings:
148-
meta_warning.warn()
149-
150-
15157
def _main():
15258
# Read command-line arguments
15359
parser = argparse.ArgumentParser(
@@ -325,7 +231,7 @@ def _main():
325231
# Temporarily override log_level to ensure they are visible.
326232
stdout_handler.setLevel(logging.WARNING)
327233
print("")
328-
aggregator.warn()
234+
aggregator.warn(log)
329235
stdout_handler.setLevel(log_level)
330236

331237
# Count lines for platforms

codebasin/_detail/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright (C) 2019-2025 Intel Corporation
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
"""
4+
This package contains implementation details that are not part of the public
5+
interface of Code Base Investigator. These implementation details are not
6+
intended to be used by other scripts, and should not be relied upon.
7+
"""
8+
import codebasin._detail.logging

codebasin/_detail/logging.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) 2019-2024 Intel Corporation
3+
# SPDX-License-Identifier: BSD-3-Clause
4+
5+
import logging
6+
import re
7+
8+
9+
class Formatter(logging.Formatter):
10+
"""
11+
A Formatter that formats LogRecords using a format inspired by compilers
12+
like gcc/clang, with optional colors.
13+
"""
14+
15+
def __init__(self, *, colors: bool = False):
16+
"""
17+
Initialize this Formatter.
18+
19+
Parameters
20+
----------
21+
colors: bool, default: False
22+
Whether to colorize the output.
23+
"""
24+
self.colors = colors
25+
26+
def format(self, record: logging.LogRecord) -> str:
27+
"""
28+
Format the specified record.
29+
30+
Parameters
31+
----------
32+
record: logging.LogRecord
33+
The record to format.
34+
35+
Returns
36+
-------
37+
str
38+
The formatted output string.
39+
"""
40+
msg = record.msg
41+
level = record.levelname.lower()
42+
43+
# Display info messages with no special formatting.
44+
if level == "info":
45+
return f"{msg}"
46+
47+
# Drop colors if requested.
48+
if not self.colors:
49+
return f"{level}: {msg}"
50+
51+
# Otherwise, use ASCII codes to improve readability.
52+
BOLD = "\033[1m"
53+
DEFAULT = "\033[39m"
54+
YELLOW = "\033[93m"
55+
RED = "\033[91m"
56+
RESET = "\033[0m"
57+
58+
if level == "warning":
59+
color = YELLOW
60+
elif level == "error":
61+
color = RED
62+
else:
63+
color = DEFAULT
64+
return f"{BOLD}{color}{level}{RESET}: {msg}"
65+
66+
67+
class MetaWarning:
68+
"""
69+
A MetaWarning is used to represent multiple warnings, and to provide
70+
suggested actions to the user.
71+
"""
72+
73+
def __init__(self, regex: str, msg: str):
74+
"""
75+
Initialize a new MetaWarning.
76+
77+
Parameters
78+
----------
79+
regex: str
80+
A regular expression used to identify constituent warnings.
81+
If any warning matches `regex`, this MetaWarning will trigger.
82+
83+
msg: str
84+
The message to display when this MetaWarning is triggered.
85+
"""
86+
self.regex = re.compile(regex)
87+
self.msg = msg
88+
self._count = 0
89+
90+
def inspect(self, record: logging.LogRecord) -> bool:
91+
"""
92+
Inspect a LogRecord to determine if it matches this MetaWarning.
93+
94+
Parameters
95+
----------
96+
record: logging.LogRecord
97+
The LogRecord to inspect.
98+
99+
Returns
100+
-------
101+
bool
102+
True if `record` matches this MetaWarning and False otherwise.
103+
"""
104+
if self.regex.search(record.msg):
105+
self._count += 1
106+
return True
107+
return False
108+
109+
def warn(self, logger: logging.Logger):
110+
"""
111+
Produce the warning associated with this MetaWarning.
112+
113+
Parameters
114+
----------
115+
log: logging.Logger
116+
The Logger that should be used to generate the MetaWarning.
117+
"""
118+
if self._count == 0:
119+
return
120+
logger.warning(self.msg.format(self._count))
121+
122+
123+
class WarningAggregator(logging.Filter):
124+
"""
125+
A WarningAggregator is a logging.Filter that inspects warnings to generate
126+
meta-warnings and statistics. It does not perform any filtering, but uses
127+
the logging.Filter mechanism as a hook to automatically inspect every
128+
warning passing through a logger.
129+
"""
130+
131+
def __init__(self):
132+
self.meta_warnings = [
133+
MetaWarning(".", "{} warnings generated during preprocessing."),
134+
MetaWarning(
135+
"user include",
136+
"{} user include files could not be found.\n"
137+
+ " These could contain important macros and includes.\n"
138+
+ " Suggested solutions:\n"
139+
+ " - Check that the file(s) exist in the code base.\n"
140+
+ " - Check the include paths in the compilation database.\n"
141+
+ " - Check if the include(s) should have used '<>'.",
142+
),
143+
MetaWarning(
144+
"system include",
145+
"{} system include files could not be found.\n"
146+
+ " These could define important feature macros.\n"
147+
+ " Suggested solutions:\n"
148+
+ " - Check that the file(s) exist on your system.\n"
149+
+ " - Use .cbi/config to define system include paths.\n"
150+
+ " - Use .cbi/config to define important macros.",
151+
),
152+
]
153+
154+
def filter(self, record: logging.LogRecord) -> bool:
155+
"""
156+
Inspect the specified LogRecord, attempting to match it against each
157+
possible MetaWarning.
158+
159+
Parameters
160+
----------
161+
record: logging.LogRecord
162+
The record to inspect.
163+
164+
Returns
165+
-------
166+
bool
167+
True, to prevent any warnings from being filtered.
168+
"""
169+
if record.levelno == logging.WARNING:
170+
for meta_warning in self.meta_warnings:
171+
meta_warning.inspect(record)
172+
return True
173+
174+
def warn(self, logger: logging.Logger):
175+
"""
176+
Produce the warning associated with any MetaWarning(s) that were
177+
matched by this WarningAggregator.
178+
179+
Parameters
180+
----------
181+
logger: logging.Logger
182+
The Logger that should be used to generate the MetaWarning(s).
183+
"""
184+
for meta_warning in self.meta_warnings:
185+
meta_warning.warn(logger)

tests/cli/test_formatter.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright (C) 2019-2024 Intel Corporation
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
import logging
5+
import unittest
6+
7+
from codebasin._detail.logging import Formatter
8+
9+
10+
class TestFormatter(unittest.TestCase):
11+
"""
12+
Test Formatter class.
13+
"""
14+
15+
def setUp(self):
16+
logging.disable()
17+
18+
def test_constructor(self):
19+
"""Check constructor arguments"""
20+
self.assertTrue(Formatter(colors=True).colors)
21+
self.assertFalse(Formatter(colors=False).colors)
22+
self.assertFalse(Formatter().colors)
23+
24+
def test_format(self):
25+
"""Check output format"""
26+
levels = ["DEBUG", "INFO", "WARNING", "ERROR"]
27+
colors = ["\033[39m", "\033[39m", "\033[93m", "\033[91m"]
28+
for colorize in [True, False]:
29+
for levelname, color in zip(levels, colors):
30+
formatter = Formatter(colors=colorize)
31+
with self.subTest(
32+
colorize=colorize,
33+
levelname=levelname,
34+
color=color,
35+
):
36+
record = logging.makeLogRecord(
37+
{
38+
"msg": "Testing",
39+
"levelname": levelname,
40+
},
41+
)
42+
msg = record.msg
43+
level = record.levelname.lower()
44+
output = formatter.format(record)
45+
if level == "info":
46+
expected = msg
47+
elif colorize:
48+
expected = f"\033[1m{color}{level}\033[0m: {msg}"
49+
else:
50+
expected = f"{level}: {msg}"
51+
self.assertEqual(output, expected)
52+
53+
54+
if __name__ == "__main__":
55+
unittest.main()

0 commit comments

Comments
 (0)