Skip to content

service

dev_tool.services.coverage.service

log = logging.getLogger(__name__) module-attribute

CoverageService

Bases: BaseService

A service class for running code coverage analysis.

This class provides methods for measuring code coverage for both Django and Python projects.

The constructor for the CoverageService class.

Parameters:

  • config (dict[str, Any] | None, default: None ) –

    The configuration dictionary for coverage settings.

Source code in dev_tool/services/coverage/service.py
def __init__(self, config: dict[str, Any] | None = None) -> None:
    """
    The constructor for the CoverageService class.

    :param config: The configuration dictionary for coverage settings.
    """

    super().__init__()

    self.set_config(config or {})
    self.manager, self.fallback = PackageManagerFactory.create_package_manager()

set_config

A method that sets the configuration for the coverage service.

Parameters:

  • config (dict[str, Any]) –

    The configuration dictionary containing coverage settings.

Source code in dev_tool/services/coverage/service.py
def set_config(self, config: dict[str, Any]) -> None:
    """
    A method that sets the configuration for the coverage service.

    :param config: The configuration dictionary containing coverage settings.
    """

    self.config = config
    self.apps = config.get('apps', ['.'])
    self.settings = config.get('settings', 'system.testing.settings')
    self.nobrowser = config.get('nobrowser', False)
    self.noerase = config.get('noerase', False)
    self.nohtml = config.get('nohtml', False)
    self.failfast = config.get('failfast', False)
    self.exclude = config.get('exclude', [])

install_coverage

A method that installs the coverage package.

Source code in dev_tool/services/coverage/service.py
def install_coverage(self) -> None:
    """A method that installs the coverage package."""

    try:
        if not self.manager.install_package('coverage'):
            message = 'Failed to install coverage with preferred manager, falling back...'
            self.notification.error_banner(message)

            log.debug(message)

            if not self.fallback.install_package('coverage'):
                message = 'Failed to install coverage package with any package manager'
                log.debug(message)

                raise CoverageInstallationError(message)
    except (OSError, PermissionError):
        message = 'Unexpected error installing coverage package'
        log.exception(message)

        raise CoverageInstallationError(message) from None

run_django_coverage

A method that runs coverage analysis for Django projects.

This method requires Django settings to be properly configured.

Source code in dev_tool/services/coverage/service.py
@is_settings
def run_django_coverage(self) -> None:
    """
    A method that runs coverage analysis for Django projects.

    This method requires Django settings to be properly configured.
    """

    if not self.apps:
        message = 'No apps to test.'
        self.notification.warning_text(message)

        log.debug(message)
        return

    self._validate_directories(self.apps, CoverageRunError)

    message = 'Running Django coverage...'
    self.notification.normal_text(message)

    self.install_coverage()

    apps = list(self.apps)
    omit = ','.join(self.exclude)

    command = [
        'coverage',
        'run',
        '--branch',
        '--source=.',
        f'--omit={omit}',
        'manage.py',
        'test',
        *apps,
        f'--settings={self.settings}',
        '--noinput',
        '-v'
    ]

    if self.failfast:
        command.append('--failfast')

    try:
        subprocess.run(self.with_venv(command), check=False)
        self.generate_html_report()
        self.erase_coverage()
        self.open_browser()
    except Exception:
        message = 'Failed to run Django coverage analysis'
        log.exception(message)

        raise CoverageRunError(message) from None

run_python_coverage

A method that runs coverage analysis for Python projects.

Source code in dev_tool/services/coverage/service.py
def run_python_coverage(self) -> None:
    """A method that runs coverage analysis for Python projects."""

    if not self.apps:
        message = 'No test directory specified for Python coverage.'
        self.notification.warning_text(message)

        log.debug(message)
        return

    self._validate_directories(self.apps, CoverageRunError)

    self.install_coverage()

    apps = list(self.apps)
    omit = ','.join(self.exclude)

    command = [
        'coverage',
        'run',
        '--branch',
        '--source=.',
        f'--omit={omit}',
        '-m',
        'unittest',
        'discover',
        '-s', *apps,
        '-v'
    ]

    if self.failfast:
        command.append('--failfast')

    try:
        subprocess.run(self.with_venv(command), check=False)
        self.generate_html_report()
        self.erase_coverage()
        self.open_browser()
    except Exception:
        message = 'Failed to run Python coverage analysis'
        log.exception(message)

        raise CoverageRunError(message) from None

generate_html_report

A method that generates an HTML report of the coverage results.

Source code in dev_tool/services/coverage/service.py
def generate_html_report(self) -> None:
    """A method that generates an HTML report of the coverage results."""

    if self.nohtml:
        return

    directory = '.coverage_html_report'

    command = [
        'coverage',
        'html',
        f'--directory={directory}'
    ]

    try:
        subprocess.run(self.with_venv(command), check=True)
    except subprocess.CalledProcessError:
        message = 'Failed to generate HTML coverage report'
        log.exception(message)

        raise CoverageReportError(message) from None
    except Exception:
        message = 'Unexpected error during HTML report generation'
        log.exception(message)

        raise CoverageReportError(message) from None

erase_coverage

A method that erases previously collected coverage data.

Source code in dev_tool/services/coverage/service.py
def erase_coverage(self) -> None:
    """A method that erases previously collected coverage data."""

    if self.noerase:
        return

    command = ['coverage', 'erase']

    try:
        subprocess.run(self.with_venv(command), check=True)
    except subprocess.CalledProcessError:
        message = 'Failed to erase coverage data'
        log.debug(message)

        raise CoverageReportError(message) from None
    except Exception:
        message = 'Unexpected error during coverage data cleanup'
        log.exception(message)

        raise CoverageReportError(message) from None

open_browser

A method that opens the coverage report in a web browser.

Source code in dev_tool/services/coverage/service.py
def open_browser(self) -> None:
    """A method that opens the coverage report in a web browser."""

    if self.nobrowser:
        return

    directory = '.coverage_html_report'
    index = str(BASE / directory / 'index.html')

    command = f'start "" "{index}"'

    try:
        subprocess.run(command, check=True, shell=True)
    except subprocess.CalledProcessError:
        message = 'Failed to open coverage report in browser'
        log.debug(message)

        raise CoverageReportError(message) from None
    except Exception:
        message = 'Unexpected error opening browser'
        log.exception(message)

        raise CoverageReportError(message) from None

with_venv

A method that prepends the virtual environment Python to a command.

Parameters:

  • command (list[str]) –

    The command to modify.

Returns:

  • list[Path | str]

    The modified command with virtual environment Python.

Source code in dev_tool/services/coverage/service.py
def with_venv(self, command: list[str]) -> list[Path | str]:
    """
    A method that prepends the virtual environment Python to a command.

    :param command: The command to modify.
    :return: The modified command with virtual environment Python.
    """

    return [VENV_PYTHON, '-m', *command]