Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
20 changes: 15 additions & 5 deletions mfd_code_quality/code_standard/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]")

Expand Down
154 changes: 124 additions & 30 deletions tests/unit/test_mfd_code_quality/test_code_standard/test_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,169 @@
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()
assert "No code standard module is available! [flake8 or ruff]" in str(excinfo.value)

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"
7 changes: 6 additions & 1 deletion tests/unit/test_mfd_code_quality/test_mfd_code_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading