Skip to content

Commit 36e8205

Browse files
dnicolodirgommers
authored andcommitted
MAINT: simplify logging and use of ANSI color escapes
Moved code around to have constants at the top of the module and to group similar functions together.
1 parent 1e79b26 commit 36e8205

File tree

2 files changed

+78
-122
lines changed

2 files changed

+78
-122
lines changed

mesonpy/__init__.py

Lines changed: 71 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@
5252
from mesonpy._compat import cached_property, read_binary
5353

5454

55-
_MESON_ARGS_KEYS = ['dist', 'setup', 'compile', 'install']
56-
5755
if typing.TYPE_CHECKING: # pragma: no cover
5856
from typing import Any, Callable, DefaultDict, Dict, List, Literal, Optional, Sequence, TextIO, Tuple, Type, TypeVar, Union
5957

@@ -69,49 +67,15 @@
6967
__version__ = '0.15.0.dev0'
7068

7169

72-
_COLORS = {
73-
'red': '\33[31m',
74-
'cyan': '\33[36m',
75-
'yellow': '\33[93m',
76-
'light_blue': '\33[94m',
77-
'bold': '\33[1m',
78-
'dim': '\33[2m',
79-
'underline': '\33[4m',
80-
'reset': '\33[0m',
81-
}
82-
_NO_COLORS = {color: '' for color in _COLORS}
83-
8470
_NINJA_REQUIRED_VERSION = '1.8.2'
8571
_MESON_REQUIRED_VERSION = '0.63.3' # keep in sync with the version requirement in pyproject.toml
8672

87-
88-
def _init_colors() -> Dict[str, str]:
89-
"""Detect if we should be using colors in the output. We will enable colors
90-
if running in a TTY, and no environment variable overrides it. Setting the
91-
NO_COLOR (https://no-color.org/) environment variable force-disables colors,
92-
and FORCE_COLOR forces color to be used, which is useful for thing like
93-
Github actions.
94-
"""
95-
if 'NO_COLOR' in os.environ:
96-
if 'FORCE_COLOR' in os.environ:
97-
warnings.warn(
98-
'Both NO_COLOR and FORCE_COLOR environment variables are set, disabling color',
99-
stacklevel=1,
100-
)
101-
return _NO_COLORS
102-
elif 'FORCE_COLOR' in os.environ or sys.stdout.isatty():
103-
return _COLORS
104-
return _NO_COLORS
105-
106-
107-
_STYLES = _init_colors() # holds the color values, should be _COLORS or _NO_COLORS
108-
73+
_MESON_ARGS_KEYS = ['dist', 'setup', 'compile', 'install']
10974

11075
_SUFFIXES = importlib.machinery.all_suffixes()
11176
_EXTENSION_SUFFIX_REGEX = re.compile(r'^[^.]+\.(?:(?P<abi>[^.]+)\.)?(?:so|pyd|dll)$')
11277
assert all(re.match(_EXTENSION_SUFFIX_REGEX, f'foo{x}') for x in importlib.machinery.EXTENSION_SUFFIXES)
11378

114-
11579
# Map Meson installation path placeholders to wheel installation paths.
11680
# See https://docs.python.org/3/library/sysconfig.html#installation-paths
11781
_INSTALLATION_PATH_MAP = {
@@ -178,29 +142,40 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[T
178142
return wheel_files
179143

180144

181-
def _is_native(file: Path) -> bool:
182-
"""Check if file is a native file."""
145+
class style:
146+
ERROR = '\33[31m', # red
147+
WARNING = '\33[93m' # bright yellow
148+
INFO = '\33[36m\33[1m' # cyan, bold
149+
RESET = '\33[0m'
183150

184-
with open(file, 'rb') as f:
185-
if sys.platform == 'linux':
186-
return f.read(4) == b'\x7fELF' # ELF
187-
elif sys.platform == 'darwin':
188-
return f.read(4) in (
189-
b'\xfe\xed\xfa\xce', # 32-bit
190-
b'\xfe\xed\xfa\xcf', # 64-bit
191-
b'\xcf\xfa\xed\xfe', # arm64
192-
b'\xca\xfe\xba\xbe', # universal / fat (same as java class so beware!)
193-
)
194-
elif sys.platform == 'win32':
195-
return f.read(2) == b'MZ'
151+
@staticmethod
152+
def strip(string: str) -> str:
153+
"""Strip ANSI escape sequences from string."""
154+
return re.sub(r'\033\[[;?0-9]*[a-zA-Z]', '', string)
196155

197-
# For unknown platforms, check for file extensions.
198-
_, ext = os.path.splitext(file)
199-
if ext in ('.so', '.a', '.out', '.exe', '.dll', '.dylib', '.pyd'):
156+
157+
@functools.lru_cache()
158+
def _use_ansi_colors() -> bool:
159+
"""Determine whether logging should use ANSI color escapes."""
160+
if 'NO_COLOR' in os.environ:
161+
return False
162+
if 'FORCE_COLOR' in os.environ or sys.stdout.isatty() and os.environ.get('TERM') != 'dumb':
163+
try:
164+
import colorama
165+
except ModuleNotFoundError:
166+
pass
167+
else:
168+
colorama.init()
200169
return True
201170
return False
202171

203172

173+
def _log(string: str , **kwargs: Any) -> None:
174+
if not _use_ansi_colors():
175+
string = style.strip(string)
176+
print(string, **kwargs)
177+
178+
204179
def _showwarning(
205180
message: Union[Warning, str],
206181
category: Type[Warning],
@@ -210,21 +185,7 @@ def _showwarning(
210185
line: Optional[str] = None,
211186
) -> None: # pragma: no cover
212187
"""Callable to override the default warning handler, to have colored output."""
213-
print('{yellow}meson-python: warning:{reset} {}'.format(message, **_STYLES))
214-
215-
216-
def _setup_cli() -> None:
217-
"""Setup CLI stuff (eg. handlers, hooks, etc.). Should only be called when
218-
actually we are in control of the CLI, not on a normal import.
219-
"""
220-
warnings.showwarning = _showwarning
221-
222-
try: # pragma: no cover
223-
import colorama
224-
except ModuleNotFoundError: # pragma: no cover
225-
pass
226-
else: # pragma: no cover
227-
colorama.init() # fix colors on windows
188+
_log(f'{style.WARNING}meson-python: warning:{style.RESET} {message}')
228189

229190

230191
class Error(RuntimeError):
@@ -273,6 +234,27 @@ def _update_dynamic(self, value: Any) -> None:
273234
self.dynamic.remove('version')
274235

275236

237+
def _is_native(file: Path) -> bool:
238+
"""Check if file is a native file."""
239+
240+
with open(file, 'rb') as f:
241+
if sys.platform == 'linux':
242+
return f.read(4) == b'\x7fELF' # ELF
243+
elif sys.platform == 'darwin':
244+
return f.read(4) in (
245+
b'\xfe\xed\xfa\xce', # 32-bit
246+
b'\xfe\xed\xfa\xcf', # 64-bit
247+
b'\xcf\xfa\xed\xfe', # arm64
248+
b'\xca\xfe\xba\xbe', # universal / fat (same as java class so beware!)
249+
)
250+
elif sys.platform == 'win32':
251+
return f.read(2) == b'MZ'
252+
253+
# For unknown platforms, check for file extensions.
254+
_, ext = os.path.splitext(file)
255+
return ext in ('.so', '.a', '.out', '.exe', '.dll', '.dylib', '.pyd')
256+
257+
276258
class _WheelBuilder():
277259
"""Helper class to build wheels from projects."""
278260

@@ -729,7 +711,7 @@ def _run(self, cmd: Sequence[str]) -> None:
729711
# Flush the line to ensure that the log line with the executed
730712
# command line appears before the command output. Without it,
731713
# the lines appear in the wrong order in pip output.
732-
print('{cyan}{bold}+ {}{reset}'.format(' '.join(cmd), **_STYLES), flush=True)
714+
_log('{style.INFO}+ {cmd}{style.RESET}'.format(style=style, cmd=' '.join(cmd)), flush=True)
733715
r = subprocess.run(cmd, cwd=self._build_dir)
734716
if r.returncode != 0:
735717
raise SystemExit(r.returncode)
@@ -991,11 +973,12 @@ def _add_ignore_files(directory: pathlib.Path) -> None:
991973
def _pyproject_hook(func: Callable[P, T]) -> Callable[P, T]:
992974
@functools.wraps(func)
993975
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
976+
warnings.showwarning = _showwarning
994977
try:
995978
return func(*args, **kwargs)
996979
except (Error, pyproject_metadata.ConfigurationError) as exc:
997-
prefix = '{red}meson-python: error:{reset} '.format(**_STYLES)
998-
print('\n' + textwrap.indent(str(exc), prefix))
980+
prefix = f'{style.ERROR}meson-python: error:{style.RESET} '
981+
_log('\n' + textwrap.indent(str(exc), prefix))
999982
raise SystemExit(1) from exc
1000983
return wrapper
1001984

@@ -1010,18 +993,6 @@ def get_requires_for_build_sdist(config_settings: Optional[Dict[str, str]] = Non
1010993
return dependencies
1011994

1012995

1013-
@_pyproject_hook
1014-
def build_sdist(
1015-
sdist_directory: str,
1016-
config_settings: Optional[Dict[Any, Any]] = None,
1017-
) -> str:
1018-
_setup_cli()
1019-
1020-
out = pathlib.Path(sdist_directory)
1021-
with _project(config_settings) as project:
1022-
return project.sdist(out).name
1023-
1024-
1025996
@_pyproject_hook
1026997
def get_requires_for_build_wheel(config_settings: Optional[Dict[str, str]] = None) -> List[str]:
1027998
dependencies = []
@@ -1035,13 +1006,26 @@ def get_requires_for_build_wheel(config_settings: Optional[Dict[str, str]] = Non
10351006
return dependencies
10361007

10371008

1009+
get_requires_for_build_editable = get_requires_for_build_wheel
1010+
1011+
10381012
@_pyproject_hook
1039-
def build_wheel(
1040-
wheel_directory: str,
1013+
def build_sdist(
1014+
sdist_directory: str,
10411015
config_settings: Optional[Dict[Any, Any]] = None,
1016+
) -> str:
1017+
1018+
out = pathlib.Path(sdist_directory)
1019+
with _project(config_settings) as project:
1020+
return project.sdist(out).name
1021+
1022+
1023+
@_pyproject_hook
1024+
def build_wheel(
1025+
wheel_directory: str, config_settings:
1026+
Optional[Dict[Any, Any]] = None,
10421027
metadata_directory: Optional[str] = None,
10431028
) -> str:
1044-
_setup_cli()
10451029

10461030
out = pathlib.Path(wheel_directory)
10471031
with _project(config_settings) as project:
@@ -1054,7 +1038,6 @@ def build_editable(
10541038
config_settings: Optional[Dict[Any, Any]] = None,
10551039
metadata_directory: Optional[str] = None,
10561040
) -> str:
1057-
_setup_cli()
10581041

10591042
# Force set a permanent build directory.
10601043
if not config_settings:
@@ -1069,10 +1052,3 @@ def build_editable(
10691052
out = pathlib.Path(wheel_directory)
10701053
with _project(config_settings) as project:
10711054
return project.editable(out).name
1072-
1073-
1074-
@_pyproject_hook
1075-
def get_requires_for_build_editable(
1076-
config_settings: Optional[Dict[str, str]] = None,
1077-
) -> List[str]:
1078-
return get_requires_for_build_wheel()

tests/test_output.py

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,11 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5-
import importlib
6-
75
import pytest
86

97
import mesonpy
108

119

12-
@pytest.fixture()
13-
def reload_module():
14-
try:
15-
yield
16-
finally:
17-
importlib.reload(mesonpy)
18-
19-
2010
@pytest.mark.parametrize(
2111
('tty', 'env', 'colors'),
2212
[
@@ -26,29 +16,19 @@ def reload_module():
2616
(True, {'NO_COLOR': ''}, False),
2717
(False, {'FORCE_COLOR': ''}, True),
2818
(True, {'FORCE_COLOR': ''}, True),
19+
(True, {'FORCE_COLOR': '', 'NO_COLOR': ''}, False),
20+
(True, {'TERM': ''}, True),
21+
(True, {'TERM': 'dumb'}, False),
2922
],
3023
)
31-
def test_colors(mocker, monkeypatch, reload_module, tty, env, colors):
24+
def test_use_ansi_colors(mocker, monkeypatch, tty, env, colors):
3225
mocker.patch('sys.stdout.isatty', return_value=tty)
3326
monkeypatch.delenv('NO_COLOR', raising=False)
3427
monkeypatch.delenv('FORCE_COLOR', raising=False)
3528
for key, value in env.items():
3629
monkeypatch.setenv(key, value)
3730

38-
importlib.reload(mesonpy) # reload module to set _STYLES
39-
40-
assert mesonpy._STYLES == (mesonpy._COLORS if colors else mesonpy._NO_COLORS)
41-
42-
43-
def test_colors_conflict(monkeypatch, reload_module):
44-
with monkeypatch.context() as m:
45-
m.setenv('NO_COLOR', '')
46-
m.setenv('FORCE_COLOR', '')
47-
48-
with pytest.warns(
49-
UserWarning,
50-
match='Both NO_COLOR and FORCE_COLOR environment variables are set, disabling color',
51-
):
52-
importlib.reload(mesonpy)
31+
# Clear caching by functools.lru_cache().
32+
mesonpy._use_ansi_colors.cache_clear()
5333

54-
assert mesonpy._STYLES == mesonpy._NO_COLORS
34+
assert mesonpy._use_ansi_colors() == colors

0 commit comments

Comments
 (0)