diff --git a/README.md b/README.md index 7552587..ddc7e8b 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,19 @@ autofix-args = [".", "--profile black"] [tool.stew.ci.custom-runners.pytest] check-args = ["--tb=long", "--junitxml=.ci/pytest-results.xml"] + +# using `executable`, you can create multiple custom runners with the same executable: +[tool.stew.ci.custom-runners.ruff-check] +executable = "ruff" +working-directory = "project" +check-args = ["check", "."] +autofix-args = [ "check", "--fix", "."] + +[tool.stew.ci.custom-runners.ruff-format] +executable = "ruff" +working-directory = "project" +check-args = ["format", "--check", "."] +autofix-args = ["format", "."] ``` When a builtin runner such as pytest is redefined as a custom runner, you must provide all the arguments. @@ -338,15 +351,16 @@ In this case, not passing `--junitxml` would mean that we lose the report that u ### Options -The following options are supported for custom runners: - -- name: You can specify the module name if it differs from the name of the tool. - - Important: Runners are called through `python -m `, not through the shell! +- executable: You can specify the executable name if it's different from the tool's name. + - Runners are called through `python -m ` first to see if it's installed in the virtual environment, else through the shell. + - Using `executable`, you can create multiple custom runners with the same executable (e.g.: `ruff check` vs `ruff format`) - check-args: The arguments to invoke the check. - autofix-args: The arguments to invoke the autofix. Provide the empty string "" in order to run without arguments. - check-failed-exit-codes: A list of ints denoting the exit codes to consider "failed" (anything else will be "error"). 0 is always a success. default is `[1]`. - create-generic-report: Whether to create a generic pass/fail JUnit report for this check. - working-directory: The default is "project" which corresponds to the project's `pyproject.toml` file. You can change it to "repository" in order to run from the root. +- name: You can specify the module name if it differs from the name of the tool. + - Deprecated: name must be unique. This has been replaced by `executable`. The `args` and `check-args` can be: diff --git a/coveo_stew/ci/any_runner.py b/coveo_stew/ci/any_runner.py index 880dc29..a512c7d 100644 --- a/coveo_stew/ci/any_runner.py +++ b/coveo_stew/ci/any_runner.py @@ -34,6 +34,7 @@ def __init__( working_directory: str = "project", check_args: Optional[Union[str, List[str]]] = None, autofix_args: Optional[Union[str, List[str]]] = None, + executable: Optional[str] = None, _pyproject: PythonProject, ) -> None: if args and check_args: @@ -47,6 +48,7 @@ def __init__( super().__init__(_pyproject=_pyproject) self._name = name + self._executable = executable self.check_failed_exit_codes = check_failed_exit_codes self.outputs_own_report = not create_generic_report self.check_args = [] if check_args is None else check_args @@ -68,7 +70,7 @@ def __init__( async def _launch(self, environment: PythonEnvironment, *extra_args: str) -> RunnerStatus: args = [self.check_args] if isinstance(self.check_args, str) else self.check_args - command = environment.build_command(self.name, *args) + command = environment.build_command(self.executable, *args) working_directory = self._pyproject.project_path if self.working_directory is WorkingDirectoryKind.Repository: @@ -91,9 +93,13 @@ async def _launch(self, environment: PythonEnvironment, *extra_args: str) -> Run def name(self) -> str: return self._name + @property + def executable(self) -> str: + return self._executable or self.name + async def _custom_autofix(self, environment: PythonEnvironment) -> None: args = [self.autofix_args] if isinstance(self.autofix_args, str) else self.autofix_args - command = environment.build_command(self.name, *args) + command = environment.build_command(self.executable, *args) working_directory = self._pyproject.project_path if self.working_directory is WorkingDirectoryKind.Repository: diff --git a/coveo_stew/ci/config.py b/coveo_stew/ci/config.py index ea5c315..2b10cb9 100644 --- a/coveo_stew/ci/config.py +++ b/coveo_stew/ci/config.py @@ -90,7 +90,10 @@ def get_runner(self, runner_name: str) -> Optional[ContinuousIntegrationRunner]: return self._runners.get(runner_name) def _generate_ci_plans( - self, checks: Optional[List[str]], skips: Optional[List[str]], parallel: bool = True + self, + checks: Optional[List[str]], + skips: Optional[List[str]], + parallel: bool = True, ) -> Generator[CIPlan, None, None]: """Generates one test plan per environment.""" checks = [check.lower() for check in checks] if checks else [] @@ -130,7 +133,11 @@ async def launch_continuous_integration( generate_github_step_report(ci_plans) statuses = set(check.status for plan in ci_plans for check in plan.checks) - for status in (RunnerStatus.Error, RunnerStatus.CheckFailed, RunnerStatus.Success): + for status in ( + RunnerStatus.Error, + RunnerStatus.CheckFailed, + RunnerStatus.Success, + ): if status in statuses: return status diff --git a/coveo_stew/ci/poetry_runners.py b/coveo_stew/ci/poetry_runners.py index a9396a5..e4a022c 100644 --- a/coveo_stew/ci/poetry_runners.py +++ b/coveo_stew/ci/poetry_runners.py @@ -12,6 +12,6 @@ class PoetryCheckRunner(ContinuousIntegrationRunner): async def _launch(self, environment: PythonEnvironment, *extra_args: str) -> RunnerStatus: await async_check_output( *environment.build_command(PythonTool.Poetry, "check"), - working_directory=self._pyproject.project_path + working_directory=self._pyproject.project_path, ) return RunnerStatus.Success diff --git a/coveo_stew/ci/runner.py b/coveo_stew/ci/runner.py index 9655027..052ea70 100644 --- a/coveo_stew/ci/runner.py +++ b/coveo_stew/ci/runner.py @@ -35,7 +35,10 @@ def project(self) -> PythonProject: return self._pyproject async def launch( - self, environment: PythonEnvironment = None, *extra_args: str, auto_fix: bool = False + self, + environment: PythonEnvironment = None, + *extra_args: str, + auto_fix: bool = False, ) -> "ContinuousIntegrationRunner": """ Launch the runner's checks. @@ -118,7 +121,8 @@ def _output_generic_report(self, environment: PythonEnvironment) -> None: test_case = TestCase(self.name, classname=f"ci.{self._pyproject.package.name}") if self.status is RunnerStatus.Error: test_case.add_error_info( - "An error occurred, the test was unable to complete.", self.last_output() + "An error occurred, the test was unable to complete.", + self.last_output(), ) elif self.status is RunnerStatus.CheckFailed: test_case.add_failure_info("The test completed; errors were found.", self.last_output()) @@ -197,7 +201,9 @@ class Run: checks: Sequence[ContinuousIntegrationRunner] @cached_property - def exceptions(self) -> List[Tuple[ContinuousIntegrationRunner, DetailedCalledProcessError]]: + def exceptions( + self, + ) -> List[Tuple[ContinuousIntegrationRunner, DetailedCalledProcessError]]: """Exceptions are stored here after the run. Exceptions are cleared when `run_and_report` is called.""" return [] @@ -224,7 +230,8 @@ async def run_and_report( else: for runner in self.checks: self._report( - await runner.launch(self.environment, auto_fix=auto_fix), feedback=feedback + await runner.launch(self.environment, auto_fix=auto_fix), + feedback=feedback, ) if self.exceptions: diff --git a/coveo_stew/discovery.py b/coveo_stew/discovery.py index c6906f2..dd2d6cd 100644 --- a/coveo_stew/discovery.py +++ b/coveo_stew/discovery.py @@ -14,7 +14,8 @@ def find_pyproject(project_name: str, path: Path = None, *, verbose: bool = False) -> PythonProject: """Find a python project in path using the exact project name""" project = next( - discover_pyprojects(path, query=project_name, exact_match=True, verbose=verbose), None + discover_pyprojects(path, query=project_name, exact_match=True, verbose=verbose), + None, ) if not project: raise PythonProjectNotFound(f"{project_name} cannot be found in {path}") diff --git a/coveo_stew/metadata/poetry_api.py b/coveo_stew/metadata/poetry_api.py index ea60ca4..cf935f3 100644 --- a/coveo_stew/metadata/poetry_api.py +++ b/coveo_stew/metadata/poetry_api.py @@ -89,7 +89,7 @@ def all_dependencies(self) -> Mapping[str, Dependency]: def dependencies_factory( - dependencies: Mapping[str, Union[str, dict]] = None + dependencies: Mapping[str, Union[str, dict]] = None, ) -> Dict[str, Dependency]: """Transforms a poetry dependency section (such as tool.poetry.dev-dependencies) into Dependency instances.""" return ( diff --git a/coveo_stew/metadata/stew_api.py b/coveo_stew/metadata/stew_api.py index 6be5357..4ebca69 100644 --- a/coveo_stew/metadata/stew_api.py +++ b/coveo_stew/metadata/stew_api.py @@ -12,7 +12,7 @@ def __init__( build: bool = False, build_without_hashes: bool = False, pydev: bool = False, - build_dependencies: Mapping[str, Any] = None + build_dependencies: Mapping[str, Any] = None, ) -> None: self.build = build # we won't build a project unless this is specified. # poetry sometimes fail at getting hashes, in which case the export cannot work because pip will complain diff --git a/coveo_stew/offline_publish.py b/coveo_stew/offline_publish.py index 2b300d6..8b0f0a5 100644 --- a/coveo_stew/offline_publish.py +++ b/coveo_stew/offline_publish.py @@ -113,8 +113,9 @@ def _store_dependencies_in_wheelhouse(self, project: Optional[PythonProject] = N lines: List[str] = [] for requirement in project.export().splitlines(): if match := LOCAL_REQUIREMENT_PATTERN.match(requirement): - dependency_name, dependency_location = match["library_name"].strip(), Path( - match["path"].strip() + dependency_name, dependency_location = ( + match["library_name"].strip(), + Path(match["path"].strip()), ) # this is a local dependency. Since poetry locks all transitive dependencies, # we're only interested in the setup dependencies and the local dependency. diff --git a/coveo_stew/pydev.py b/coveo_stew/pydev.py index f37a0a6..181a853 100644 --- a/coveo_stew/pydev.py +++ b/coveo_stew/pydev.py @@ -101,7 +101,8 @@ def _dev_dependencies_of_dependencies( if dev_dependency.is_local: value: Any = tomlkit.inline_table() value.append( - "path", str(dev_dependency.path.relative_to(local_project.project_path)) + "path", + str(dev_dependency.path.relative_to(local_project.project_path)), ) else: value = dev_dependency.version diff --git a/coveo_stew/stew.py b/coveo_stew/stew.py index ef3cec4..95168ae 100644 --- a/coveo_stew/stew.py +++ b/coveo_stew/stew.py @@ -162,7 +162,9 @@ def activated_environment(self) -> Optional[PythonEnvironment]: ) def virtual_environments( - self, *, create_default_if_missing: Union[bool, EnvironmentCreationBehavior] = False + self, + *, + create_default_if_missing: Union[bool, EnvironmentCreationBehavior] = False, ) -> Iterator[PythonEnvironment]: """The project's virtual environments. These are cached for performance. @@ -211,8 +213,9 @@ def _get_virtual_environment_paths(self) -> Iterator[Tuple[Path, bool]]: if (stripped := str_path.strip()) and ( match := re.fullmatch(ENVIRONMENT_PATH_PATTERN, stripped) ): - yield Path(match.groupdict()["path"].strip()), bool( - match.groupdict().get("activated") + yield ( + Path(match.groupdict()["path"].strip()), + bool(match.groupdict().get("activated")), ) def current_environment_belongs_to_project(self) -> bool: diff --git a/test_coveo_stew/test_backward_compatibility.py b/test_coveo_stew/test_backward_compatibility.py index b37fbbd..9c2a303 100644 --- a/test_coveo_stew/test_backward_compatibility.py +++ b/test_coveo_stew/test_backward_compatibility.py @@ -27,7 +27,8 @@ ) def test_get_verb(poetry_version: str, verb: str, expected_verb: str) -> None: with mock.patch( - *ref(find_poetry_version, context=get_verb), return_value=Version(poetry_version) + *ref(find_poetry_version, context=get_verb), + return_value=Version(poetry_version), ): get_verb.cache_clear() assert get_verb(verb, None) == expected_verb diff --git a/test_coveo_stew/test_pyproject.py b/test_coveo_stew/test_pyproject.py index 5cb453b..edf75a2 100644 --- a/test_coveo_stew/test_pyproject.py +++ b/test_coveo_stew/test_pyproject.py @@ -26,7 +26,9 @@ def test_pyproject_mock_initial_state(pyproject_mock: PythonProject) -> None: @Integration -def test_pyproject_mock_initial_state_integration(pyproject_mock: PythonProject) -> None: +def test_pyproject_mock_initial_state_integration( + pyproject_mock: PythonProject, +) -> None: assert not pyproject_mock.lock_is_outdated() diff --git a/test_coveo_stew/test_stew_ci.py b/test_coveo_stew/test_stew_ci.py index d9b9b26..166042b 100644 --- a/test_coveo_stew/test_stew_ci.py +++ b/test_coveo_stew/test_stew_ci.py @@ -322,7 +322,10 @@ def write_code(project: PythonProject, code: str) -> Generator[None, None, None] @parametrize( ("check", "failure_text"), [ - ("mypy", 'error: Argument 1 to "fn" has incompatible type "int"; expected "str"'), + ( + "mypy", + 'error: Argument 1 to "fn" has incompatible type "int"; expected "str"', + ), ("isort", "Imports are incorrectly sorted and/or formatted."), ("black", f"would reformat mock_linter_errors{os.sep}code.py"), ],