WenjieDu/PyPOTS

View on GitHub
pypots/cli/dev.py

Summary

Maintainability
A
2 hrs
Test Coverage
"""
CLI tools to help the development team build PyPOTS.
"""

# Created by Wenjie Du <wenjay.du@gmail.com>
# License: BSD-3-Clause

import os
import shutil
from argparse import Namespace

from .base import BaseCommand
from ..utils.logging import logger

IMPORT_ERROR_MESSAGE = (
    "`pypots-cli dev` command is for PyPOTS developers to run tests easily. "
    "Therefore, you need a complete PyPOTS development environment. However, you are missing some dependencies. "
    "Please refer to https://github.com/WenjieDu/PyPOTS/blob/main/environment-dev.yml for dependency details. "
)


def dev_command_factory(args: Namespace):
    return DevCommand(
        args.build,
        args.cleanup,
        args.run_tests,
        args.k,
        args.show_coverage,
        args.lint_code,
    )


class DevCommand(BaseCommand):
    """CLI tools helping develop PyPOTS. Easy the running of tests and code linting with Black and Flake8.

    Examples
    --------
    $ pypots-cli dev --run_tests
    $ pypots-cli dev --run_tests --show_coverage  # show code coverage
    $ pypots-cli dev --run_tests -k imputation  # only run tests cases of imputation models
    $ pypots-cli dev --lint_code  # use Black to reformat the code and apply Flake8 to lint code

    """

    @staticmethod
    def register_subcommand(parser):
        sub_parser = parser.add_parser(
            "dev",
            help="CLI tools helping develop PyPOTS code",
        )
        sub_parser.add_argument(
            "--build",
            dest="build",
            action="store_true",
            help="Build PyPOTS into a wheel and package the source code into a .tar.gz file for distribution",
        )
        sub_parser.add_argument(
            "-c",
            "--cleanup",
            dest="cleanup",
            action="store_true",
            help="Delete all caches and building files",
        )
        sub_parser.add_argument(
            "--run_tests",
            "--run-tests",
            dest="run_tests",
            action="store_true",
            help="Run all test cases",
        )
        sub_parser.add_argument(
            "--show-coverage",
            "--show_coverage",
            dest="show_coverage",
            action="store_true",
            help="Show the code coverage report after running tests",
        )
        sub_parser.add_argument(
            "-k",
            type=str,
            default=None,
            help="The -k option of pytest. Description of -k option in pytest: "
            "only run tests which match the given substring expression. An expression is a python evaluatable "
            "expression where all names are substring-matched against test names and their parent classes. "
            "Example: -k 'test_method or test_other' matches all test functions and classes whose name contains "
            "'test_method' or 'test_other', while -k 'not test_method' matches those that don't contain "
            "'test_method' in their names. -k 'not test_method and not test_other' will eliminate the matches. "
            "Additionally keywords are matched to classes and functions containing extra names in their "
            "'extra_keyword_matches' set, as well as functions which have names assigned directly to them. The "
            "matching is case-insensitive.",
        )
        sub_parser.add_argument(
            "--lint-code",
            "--lint_code",
            dest="lint_code",
            action="store_true",
            help="Run Black and Flake8 to lint code",
        )
        sub_parser.set_defaults(func=dev_command_factory)

    def __init__(
        self,
        build: bool,
        cleanup: bool,
        run_tests: bool,
        k: str,
        show_coverage: bool,
        lint_code: bool,
    ):
        self._build = build
        self._cleanup = cleanup
        self._run_tests = run_tests
        self._k = k
        self._show_coverage = show_coverage
        self._lint_code = lint_code

    def checkup(self):
        """Run some checks on the arguments to avoid error usages"""
        self.check_if_under_root_dir(strict=True)

        if self._k is not None:
            assert self._run_tests, (
                "Argument `-k` should combine the use of `--run_tests`. "
                "Try `pypots-cli dev --run_tests -k your_pattern`"
            )

        if self._show_coverage:
            assert self._run_tests, (
                "Argument `--show_coverage` should combine the use of `--run_tests`. "
                "Try `pypots-cli dev --run_tests --show_coverage`"
            )

        if self._cleanup:
            assert (
                not self._run_tests and not self._lint_code
            ), "Argument `--cleanup` should be used alone. Try `pypots-cli dev --cleanup`"

    def run(self):
        """Execute the given command."""
        # run checks first
        self.checkup()

        try:
            if self._cleanup:
                shutil.rmtree("build", ignore_errors=True)
                shutil.rmtree("dist", ignore_errors=True)
                shutil.rmtree("pypots.egg-info", ignore_errors=True)
            elif self._build:
                self.execute_command("python -m build")
            elif self._run_tests:
                pytest_command = f"pytest -k {self._k}" if self._k is not None else "pytest"
                command_to_run_test = f"coverage run -m {pytest_command}" if self._show_coverage else pytest_command
                self.execute_command(command_to_run_test)
                if self._show_coverage and os.path.exists(".coverage"):
                    self.execute_command("coverage report -m")
            elif self._lint_code:
                logger.info("Reformatting with Black...")
                self.execute_command("black .")
                logger.info("Linting with Flake8...")
                self.execute_command("flake8 .")
        except ImportError:
            raise ImportError(IMPORT_ERROR_MESSAGE)
        except Exception as e:
            raise e
        finally:
            shutil.rmtree(".pytest_cache", ignore_errors=True)