Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add an executable property to the custom runners #45

Merged
merged 3 commits into from
Apr 4, 2024
Merged
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
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 <name>`, 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 <executable>` 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:

Expand Down
10 changes: 8 additions & 2 deletions coveo_stew/ci/any_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down
11 changes: 9 additions & 2 deletions coveo_stew/ci/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion coveo_stew/ci/poetry_runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 11 additions & 4 deletions coveo_stew/ci/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 []

Expand All @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion coveo_stew/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
2 changes: 1 addition & 1 deletion coveo_stew/metadata/poetry_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion coveo_stew/metadata/stew_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions coveo_stew/offline_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion coveo_stew/pydev.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions coveo_stew/stew.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion test_coveo_stew/test_backward_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion test_coveo_stew/test_pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
5 changes: 4 additions & 1 deletion test_coveo_stew/test_stew_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
],
Expand Down
Loading