Skip to content

Commit 4cf51a2

Browse files
lestevergommers
authored andcommitted
ENH: no output in editable verbose mode when there is no work to do
1 parent c9d1c2d commit 4cf51a2

File tree

3 files changed

+91
-15
lines changed

3 files changed

+91
-15
lines changed

mesonpy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ def build(self, directory: Path, source_dir: pathlib.Path, build_dir: pathlib.Pa
517517
f'{loader_module_name}.py',
518518
read_binary('mesonpy', '_editable.py') + textwrap.dedent(f'''
519519
install(
520+
{self._metadata.name!r},
520521
{self._top_level_modules!r},
521522
{os.fspath(build_dir)!r},
522523
{build_command!r},

mesonpy/_editable.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
import ast
910
import functools
1011
import importlib.abc
1112
import importlib.machinery
@@ -285,15 +286,16 @@ def find_spec(fullname: str, tree: Node) -> Optional[importlib.machinery.ModuleS
285286

286287

287288
class MesonpyMetaFinder(importlib.abc.MetaPathFinder):
288-
def __init__(self, names: Set[str], path: str, cmd: List[str], verbose: bool = False):
289+
def __init__(self, package: str, names: Set[str], path: str, cmd: List[str], verbose: bool = False):
290+
self._name = package
289291
self._top_level_modules = names
290292
self._build_path = path
291293
self._build_cmd = cmd
292294
self._verbose = verbose
293295
self._loaders: List[Tuple[type, str]] = []
294296

295297
def __repr__(self) -> str:
296-
return f'{self.__class__.__name__}({self._build_path!r})'
298+
return f'{self.__class__.__name__}({self._name!r}, {self._build_path!r})'
297299

298300
def find_spec(
299301
self,
@@ -308,6 +310,21 @@ def find_spec(
308310
tree = self._rebuild()
309311
return find_spec(fullname, tree)
310312

313+
def _work_to_do(self, env: dict[str, str]) -> bool:
314+
if sys.platform == 'win32':
315+
# On Windows the build command is 'meson compile' eventually with a --ninja-args= option.
316+
if self._build_cmd[-1].startswith('--ninja-args='):
317+
ninja_args = ast.literal_eval(self._build_cmd[-1].split('=', 1)[1]) + ['-n']
318+
dry_run_build_cmd = self._build_cmd[:-1] + [f'--ninja-args={ninja_args!r}']
319+
else:
320+
dry_run_build_cmd = self._build_cmd + ['--ninja-args=-n']
321+
else:
322+
dry_run_build_cmd = self._build_cmd + ['-n']
323+
# Check adapted from
324+
# https://github.com/mesonbuild/meson/blob/a35d4d368a21f4b70afa3195da4d6292a649cb4c/mesonbuild/mtest.py#L1635-L1636
325+
p = subprocess.run(dry_run_build_cmd, cwd=self._build_path, env=env, capture_output=True)
326+
return b'ninja: no work to do.' not in p.stdout and b'samu: nothing to do' not in p.stdout
327+
311328
@functools.lru_cache(maxsize=1)
312329
def _rebuild(self) -> Node:
313330
# skip editable wheel lookup during rebuild: during the build
@@ -317,12 +334,13 @@ def _rebuild(self) -> Node:
317334
env[MARKER] = os.pathsep.join((env.get(MARKER, ''), self._build_path))
318335

319336
if self._verbose or bool(env.get(VERBOSE, '')):
320-
print('+ ' + ' '.join(self._build_cmd))
321-
stdout = None
337+
# We want to show some output only if there is some work to do
338+
if self._work_to_do(env):
339+
build_command = ' '.join(self._build_cmd)
340+
print(f'meson-python: building {self._name}: {build_command}', flush=True)
341+
subprocess.run(self._build_cmd, cwd=self._build_path, env=env)
322342
else:
323-
stdout = subprocess.DEVNULL
324-
325-
subprocess.run(self._build_cmd, cwd=self._build_path, env=env, stdout=stdout, check=True)
343+
subprocess.run(self._build_cmd, cwd=self._build_path, env=env, stdout=subprocess.DEVNULL)
326344

327345
install_plan_path = os.path.join(self._build_path, 'meson-info', 'intro-install_plan.json')
328346
with open(install_plan_path, 'r', encoding='utf8') as f:
@@ -366,7 +384,7 @@ def iter_modules(self, prefix: str) -> Iterator[Tuple[str, bool]]:
366384
yield prefix + modname, False
367385

368386

369-
def install(names: Set[str], path: str, cmd: List[str], verbose: bool) -> None:
370-
finder = MesonpyMetaFinder(names, path, cmd, verbose)
387+
def install(package: str, names: Set[str], path: str, cmd: List[str], verbose: bool) -> None:
388+
finder = MesonpyMetaFinder(package, names, path, cmd, verbose)
371389
sys.meta_path.insert(0, finder)
372390
sys.path_hooks.insert(0, finder._path_hook)

tests/test_editable.py

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

5+
import io
56
import os
67
import pathlib
78
import pkgutil
89
import sys
910

11+
from contextlib import redirect_stdout
12+
1013
import pytest
1114

1215
import mesonpy
@@ -66,10 +69,10 @@ def test_mesonpy_meta_finder(package_complex, tmp_path):
6669
mesonpy.Project(package_complex, tmp_path)
6770

6871
# point the meta finder to the build directory
69-
finder = _editable.MesonpyMetaFinder({'complex'}, os.fspath(tmp_path), ['ninja'])
72+
finder = _editable.MesonpyMetaFinder('complex', {'complex'}, os.fspath(tmp_path), ['ninja'])
7073

7174
# check repr
72-
assert repr(finder) == f'MesonpyMetaFinder({str(tmp_path)!r})'
75+
assert repr(finder) == f'MesonpyMetaFinder(\'complex\', {str(tmp_path)!r})'
7376

7477
# verify that we can look up a pure module in the source directory
7578
spec = finder.find_spec('complex')
@@ -130,7 +133,7 @@ def test_resources(tmp_path):
130133
mesonpy.Project(package_path, tmp_path)
131134

132135
# point the meta finder to the build directory
133-
finder = _editable.MesonpyMetaFinder({'simple'}, os.fspath(tmp_path), ['ninja'])
136+
finder = _editable.MesonpyMetaFinder('simple', {'simple'}, os.fspath(tmp_path), ['ninja'])
134137

135138
# verify that we can look up resources
136139
spec = finder.find_spec('simple')
@@ -149,7 +152,7 @@ def test_importlib_resources(tmp_path):
149152
mesonpy.Project(package_path, tmp_path)
150153

151154
# point the meta finder to the build directory
152-
finder = _editable.MesonpyMetaFinder({'simple'}, os.fspath(tmp_path), ['ninja'])
155+
finder = _editable.MesonpyMetaFinder('simple', {'simple'}, os.fspath(tmp_path), ['ninja'])
153156

154157
try:
155158
# install the finder in the meta path
@@ -198,7 +201,7 @@ def test_editable_pkgutils_walk_packages(package_complex, tmp_path):
198201
# build a package in a temporary directory
199202
mesonpy.Project(package_complex, tmp_path)
200203

201-
finder = _editable.MesonpyMetaFinder({'complex'}, os.fspath(tmp_path), ['ninja'])
204+
finder = _editable.MesonpyMetaFinder('complex', {'complex'}, os.fspath(tmp_path), ['ninja'])
202205

203206
try:
204207
# install editable hooks
@@ -230,10 +233,64 @@ def test_editable_pkgutils_walk_packages(package_complex, tmp_path):
230233

231234
def test_custom_target_install_dir(package_custom_target_dir, tmp_path):
232235
mesonpy.Project(package_custom_target_dir, tmp_path)
233-
finder = _editable.MesonpyMetaFinder({'package'}, os.fspath(tmp_path), ['ninja'])
236+
finder = _editable.MesonpyMetaFinder('package', {'package'}, os.fspath(tmp_path), ['ninja'])
234237
try:
235238
sys.meta_path.insert(0, finder)
236239
import package.generated.one
237240
import package.generated.two # noqa: F401
238241
finally:
239242
del sys.meta_path[0]
243+
244+
245+
@pytest.mark.parametrize('verbose', [False, True], ids=('', 'verbose'))
246+
@pytest.mark.parametrize('args', [[], ['-j1']], ids=('', '-Ccompile-args=-j1'))
247+
def test_editable_rebuild(package_purelib_and_platlib, tmp_path, verbose, args):
248+
with mesonpy._project({'builddir': os.fspath(tmp_path), 'compile-args': args}) as project:
249+
250+
finder = _editable.MesonpyMetaFinder(
251+
project._metadata.name, {'plat', 'pure'},
252+
os.fspath(tmp_path), project._build_command,
253+
verbose=verbose,
254+
)
255+
256+
try:
257+
# Install editable hooks
258+
sys.meta_path.insert(0, finder)
259+
260+
# Import module and trigger rebuild. Importing any module in the
261+
# Python package triggers the build. Use the the pure Python one as
262+
# Cygwin is not happy when reloading an extension module.
263+
stdout = io.StringIO()
264+
with redirect_stdout(stdout):
265+
import pure
266+
assert not verbose or stdout.getvalue().startswith('meson-python: building ')
267+
268+
# Reset state.
269+
del sys.modules['pure']
270+
finder._rebuild.cache_clear()
271+
272+
# Importing again should result in no output.
273+
stdout = io.StringIO()
274+
with redirect_stdout(stdout):
275+
import pure # noqa: F401, F811
276+
assert stdout.getvalue() == ''
277+
278+
finally:
279+
del sys.meta_path[0]
280+
sys.modules.pop('pure', None)
281+
282+
283+
def test_editable_verbose(venv, package_complex, editable_complex, monkeypatch):
284+
monkeypatch.setenv('MESONPY_EDITABLE_VERBOSE', '1')
285+
venv.pip('install', os.fspath(editable_complex))
286+
287+
# Importing the module should not result in any output since the project has already been built
288+
assert venv.python('-c', 'import complex').strip() == ''
289+
290+
# Touch a compiled source file and make sure that the build info is output on import
291+
package_complex.joinpath('test.pyx').touch()
292+
output = venv.python('-c', 'import complex').strip()
293+
assert output.startswith('meson-python: building complex: ')
294+
295+
# Another import without file changes should not show any output
296+
assert venv.python('-c', 'import complex') == ''

0 commit comments

Comments
 (0)