From a0d296676d3828c86f244b962cf55db0a351fd6c Mon Sep 17 00:00:00 2001 From: Mateusz Chrominski Date: Tue, 18 Nov 2025 14:44:09 +0100 Subject: [PATCH] feat: UV venv support. Signed-off-by: Mateusz Chrominski --- README.md | 5 +- mfd_code_quality/code_standard/checks.py | 20 ++- .../test_code_standard/test_checks.py | 154 ++++++++++++++---- .../test_mfd_code_quality.py | 7 +- 4 files changed, 148 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index ccbd131..7a5e160 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ # MFD Code Quality -This module provides a set of methods for checking code quality.\ +This module provides a set of methods for checking code quality. + **Along with the module installation, additional CLI tools appear in your environment.** ## Usage @@ -56,7 +57,7 @@ If command requires configuration, the config file will be automatically created ### Configuration files -We are using 2 configuration files (created/modified/removed automatically): +We are using two configuration files (created/modified/removed automatically): * `ruff.toml` - for ruff configuration diff --git a/mfd_code_quality/code_standard/checks.py b/mfd_code_quality/code_standard/checks.py index 53bfd8b..efd605a 100644 --- a/mfd_code_quality/code_standard/checks.py +++ b/mfd_code_quality/code_standard/checks.py @@ -58,11 +58,21 @@ def _get_available_code_standard_module() -> str: :raises Exception: When no code standard module is available """ code_standard_modules = ["ruff", "flake8"] - pip_list = run((sys.executable, "-m", "pip", "list"), capture_output=True, text=True, cwd=get_root_dir()) - for code_standard_module in code_standard_modules: - if f"{code_standard_module} " in pip_list.stdout: - logger.info(f"{code_standard_module.capitalize()} will be used for code standard check.") - return code_standard_module + commands = [("uv", "pip", "list"), (sys.executable, "-m", "pip", "list")] + for cmd in commands: + try: + pip_list = run(cmd, capture_output=True, text=True, cwd=get_root_dir()) + except Exception as e: # noqa + logger.debug(f"Error occurred while running {cmd}:\n{e}") + continue + + if pip_list.returncode != 0: + continue + + for code_standard_module in code_standard_modules: + if f"{code_standard_module} " in pip_list.stdout: + logger.info(f"{code_standard_module.capitalize()} will be used for code standard check.") + return code_standard_module raise Exception("No code standard module is available! [flake8 or ruff]") diff --git a/tests/unit/test_mfd_code_quality/test_code_standard/test_checks.py b/tests/unit/test_mfd_code_quality/test_code_standard/test_checks.py index 9a5b222..57a1475 100644 --- a/tests/unit/test_mfd_code_quality/test_code_standard/test_checks.py +++ b/tests/unit/test_mfd_code_quality/test_code_standard/test_checks.py @@ -3,38 +3,64 @@ from textwrap import dedent import logging +import sys + import pytest -from mfd_code_quality.code_standard.checks import _get_available_code_standard_module, _test_ruff_check, run_checks +from mfd_code_quality.code_standard.checks import ( + _get_available_code_standard_module, + _run_code_standard_tests, + _test_ruff_check, +) class TestChecks: def test_get_available_code_standard_module_flake8(self, mocker): - output = dedent( - """\ - flake8 7.1.1 - flake8-annotations 3.0.1 - flake8-black 0.2.4 - """ + """flake8 is chosen when ruff is not present but flake8 is.""" + # First command (uv pip list) returns no ruff / flake8 + mocker.patch( + "mfd_code_quality.code_standard.checks.run", + side_effect=[ + mocker.Mock(stdout="", returncode=0), + mocker.Mock( + stdout=dedent( + """\ + flake8 7.1.1 + flake8-annotations 3.0.1 + flake8-black 0.2.4 + """ + ), + returncode=0, + ), + ], ) - mocker.patch("mfd_code_quality.code_standard.checks.run", return_value=mocker.Mock(stdout=output)) mocker.patch("mfd_code_quality.code_standard.checks.get_root_dir", return_value="/path/to/root") + assert _get_available_code_standard_module() == "flake8" - def test_get_available_code_standard_module_ruff(self, mocker): + def test_get_available_code_standard_module_ruff_preferred(self, mocker): + """Ruff is preferred when both ruff and flake8 are installed.""" output = dedent( """\ ruff 0.6.4 + flake8 7.1.1 flake8-annotations 3.0.1 flake8-black 0.2.4 """ ) - mocker.patch("mfd_code_quality.code_standard.checks.run", return_value=mocker.Mock(stdout=output)) + mocker.patch( + "mfd_code_quality.code_standard.checks.run", + return_value=mocker.Mock(stdout=output, returncode=0), + ) mocker.patch("mfd_code_quality.code_standard.checks.get_root_dir", return_value="/path/to/root") + assert _get_available_code_standard_module() == "ruff" def test_get_available_code_standard_module_none(self, mocker): - mocker.patch("mfd_code_quality.code_standard.checks.run", return_value=mocker.Mock(stdout="")) + mocker.patch( + "mfd_code_quality.code_standard.checks.run", + return_value=mocker.Mock(stdout="", returncode=0), + ) mocker.patch("mfd_code_quality.code_standard.checks.get_root_dir", return_value="/path/to/root") with pytest.raises(Exception) as excinfo: _get_available_code_standard_module() @@ -42,36 +68,104 @@ def test_get_available_code_standard_module_none(self, mocker): def test__test_ruff_check_call(self, mocker, caplog): caplog.set_level(logging.INFO) - mocker.patch("mfd_code_quality.code_standard.checks.run", return_value=mocker.Mock(returncode=1)) + mocker.patch( + "mfd_code_quality.code_standard.checks.run", + return_value=mocker.Mock(returncode=1, stdout=""), + ) mocker.patch("mfd_code_quality.code_standard.checks.get_root_dir", return_value="/path/to/root") _test_ruff_check() assert "Checking 'ruff check'..." in caplog.text - def test_run_checks_failure(self, mocker, caplog): - output = dedent( - """\ - ruff 0.6.4 - """ - ) - mocker.patch("mfd_code_quality.code_standard.checks.run", return_value=mocker.Mock(stdout=output)) - mocker.patch("mfd_code_quality.code_standard.checks.get_root_dir", return_value="/path/to/root") - mocker.patch("mfd_code_quality.code_standard.checks.set_cwd") - mocker.patch("mfd_code_quality.code_standard.checks.sys.exit", return_value=mocker.Mock()) + def test_run_code_standard_tests_failure_logs_and_returns_false(self, mocker, caplog): + """When ruff checks fail, helper returns False and logs failure message.""" caplog.set_level(logging.INFO) - run_checks(with_configs=False) + mocker.patch("mfd_code_quality.code_standard.checks.set_up_logging") + mocker.patch("mfd_code_quality.code_standard.checks.set_cwd") + mocker.patch( + "mfd_code_quality.code_standard.checks._get_available_code_standard_module", + return_value="ruff", + ) + mocker.patch( + "mfd_code_quality.code_standard.checks._test_ruff_format", + return_value=False, + ) + mocker.patch( + "mfd_code_quality.code_standard.checks._test_ruff_check", + return_value=False, + ) + delete_mock = mocker.patch("mfd_code_quality.code_standard.checks.delete_config_files") + create_mock = mocker.patch("mfd_code_quality.code_standard.checks.create_config_files") + + result = _run_code_standard_tests(with_configs=False) + + assert result is False assert "Code standard check FAILED." in caplog.text + # with_configs=False -> no config files should be touched + create_mock.assert_not_called() + delete_mock.assert_not_called() - def test_run_checks_with_configs(self, mocker): - mocker.patch("mfd_code_quality.code_standard.checks.sys.exit", return_value=mocker.Mock()) - create_mock = mocker.patch("mfd_code_quality.code_standard.checks.create_config_files") - delete_mock = mocker.patch("mfd_code_quality.code_standard.checks.delete_config_files") + def test_run_code_standard_tests_with_configs_calls_create_and_delete(self, mocker, caplog): + """With configs enabled and ruff selected, config files are created and deleted in finally block.""" + caplog.set_level(logging.INFO) + mocker.patch("mfd_code_quality.code_standard.checks.set_up_logging") mocker.patch("mfd_code_quality.code_standard.checks.set_cwd") mocker.patch( "mfd_code_quality.code_standard.checks._get_available_code_standard_module", return_value="ruff", ) - mocker.patch("mfd_code_quality.code_standard.checks._test_ruff_format", return_value=True) - mocker.patch("mfd_code_quality.code_standard.checks._test_ruff_check", return_value=True) - run_checks() + mocker.patch( + "mfd_code_quality.code_standard.checks._test_ruff_format", + return_value=True, + ) + mocker.patch( + "mfd_code_quality.code_standard.checks._test_ruff_check", + return_value=True, + ) + create_mock = mocker.patch("mfd_code_quality.code_standard.checks.create_config_files") + delete_mock = mocker.patch("mfd_code_quality.code_standard.checks.delete_config_files") + + result = _run_code_standard_tests(with_configs=True) + + assert result is True create_mock.assert_called_once() delete_mock.assert_called_once() + + def test_run_checks_exits_with_correct_code(self, mocker): + """High-level run_checks exits with 0/1 depending on helper result.""" + from mfd_code_quality.code_standard import checks + + mocker.patch.object(checks, "_run_code_standard_tests", return_value=True) + exit_mock = mocker.patch.object(sys, "exit") + + checks.run_checks(with_configs=True) + + exit_mock.assert_called_once_with(0) + + # Now simulate failure + exit_mock.reset_mock() + mocker.patch.object(checks, "_run_code_standard_tests", return_value=False) + + checks.run_checks(with_configs=False) + + exit_mock.assert_called_once_with(1) + + def test_get_available_code_standard_module_uv_raises_then_fallback(self, mocker): + """If the first command raises (e.g. uv missing), fallback command is used.""" + mocker.patch( + "mfd_code_quality.code_standard.checks.run", + side_effect=[ + FileNotFoundError(), # simulates missing 'uv' + mocker.Mock( + stdout=dedent( + """\ + ruff 0.6.4 + flake8 7.1.1 + """ + ), + returncode=0, + ), + ], + ) + mocker.patch("mfd_code_quality.code_standard.checks.get_root_dir", return_value="/path/to/root") + + assert _get_available_code_standard_module() == "ruff" diff --git a/tests/unit/test_mfd_code_quality/test_mfd_code_quality.py b/tests/unit/test_mfd_code_quality/test_mfd_code_quality.py index 1897d49..dcf8215 100644 --- a/tests/unit/test_mfd_code_quality/test_mfd_code_quality.py +++ b/tests/unit/test_mfd_code_quality/test_mfd_code_quality.py @@ -71,7 +71,12 @@ def test_run_all_checks_with_flake8(mock_dependencies): ) -def test_log_help_info_logs_commands(caplog): +def test_log_help_info_logs_commands(caplog, mocker): + """log_help_info should log available commands without parsing real CLI args.""" caplog.set_level("INFO") + # Prevent argparse in set_up_logging/get_parsed_args from seeing pytest/IDE args. + mocker.patch("mfd_code_quality.utils.get_parsed_args", return_value=mocker.Mock(verbose=False)) + log_help_info() + assert "Available commands:" in caplog.text