Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d495627
feat: add github annotations reporter
OrenMe Feb 14, 2025
2ca9816
add docs
OrenMe Feb 15, 2025
9c66cb1
lint fix
OrenMe Feb 15, 2025
841c587
Merge branch 'main' into feat/githubAnnotations
OrenMe Feb 15, 2025
890c71e
Merge remote-tracking branch 'refs/remotes/origin/feat/githubAnnotati…
OrenMe Feb 15, 2025
239a7f2
Merge branch 'main' into feat/githubAnnotations
OrenMe Feb 15, 2025
fc9962b
lint fix
OrenMe Feb 15, 2025
6188552
Merge branch 'main' into feat/githubAnnotations
OrenMe Feb 15, 2025
a16f978
Update cli.py
OrenMe Feb 17, 2025
4ca19af
Update python/deptry/cli.py
OrenMe Feb 19, 2025
132d1f7
CR comments
OrenMe Feb 19, 2025
77bbe53
Merge remote-tracking branch 'refs/remotes/origin/feat/githubAnnotati…
OrenMe Feb 19, 2025
a33bae1
Update docs/usage.md
OrenMe Feb 19, 2025
a7f6f89
more CR fixes
OrenMe Feb 19, 2025
8950f31
Merge remote-tracking branch 'refs/remotes/origin/feat/githubAnnotati…
OrenMe Feb 19, 2025
b483f72
Merge branch 'main' into feat/githubAnnotations
OrenMe Feb 21, 2025
36bca11
Merge branch 'main' into feat/githubAnnotations
OrenMe Feb 23, 2025
54c6f05
Merge branch 'main' into feat/githubAnnotations
OrenMe Mar 16, 2025
4b457a1
Merge branch 'main' of github.com:fpgmaas/deptry into feat/githubAnno…
mkniewallner Nov 7, 2025
261aa89
feat(reporters): handle violations without line/column
mkniewallner Nov 7, 2025
c7fa470
test: add functional tests for GitHub reporter
mkniewallner Nov 7, 2025
a26e41c
docs(usage): tweak documentation
mkniewallner Nov 7, 2025
4324cff
test: use tuple for `github_warning_errors` arg
mkniewallner Nov 7, 2025
1c4bbbd
fix(cli): use tuple for `github_warning_errors` arg
mkniewallner Nov 7, 2025
e6f76d8
refactor(reporters): code/message are never `None`
mkniewallner Nov 7, 2025
7fe1324
test: windows, as usual
mkniewallner Nov 8, 2025
23031df
test: more windows
mkniewallner Nov 8, 2025
9517654
Merge branch 'main' into feat/githubAnnotations
mkniewallner Nov 8, 2025
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
30 changes: 30 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,36 @@ json_output = "deptry_report.txt"
deptry . --json-output deptry_report.txt
```

#### GitHub Reporter
When enabled with the `--github-output` flag, deptry prints GitHub Actions annotations for detected dependency issues.

Annotations follow this format:
```shell
::error file=<file>,line=<line>,col=<column>,title=<error_code>::<error message>
```
By default, violations are annotated as errors. To report specific violation codes as warnings instead, use the `--github-warning-errors` option. For example, to treat violations with code `DEP001` as warnings:
```shell
deptry . --github-output --github-warning-errors DEP001
```
When a violation's error code is included in the warning list, the annotation uses the `warning` severity.

- Type: `bool` (for `github_output`), `list[str]` (for `github_warning_errors`)
- Default: `False` (for `github_output`), `[]` (for `github_warning_errors`)
- `pyproject.toml` option names: `github_output`, `github_warning_errors`
- CLI option names: `--github-output`, `--github-warning-errors`

- `pyproject.toml` example:
```toml
[tool.deptry]
github_output = true
github_warning_errors = ["DEP001"]
```

- CLI example:
```shell
deptry . --github-output --github-warning-errors DEP001
```

#### Package module name map

Deptry will automatically detect top level modules names that belong to a
Expand Down
19 changes: 19 additions & 0 deletions python/deptry/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,21 @@ def display_deptry_version(ctx: click.Context, _param: click.Parameter, value: b
help="""If specified, a summary of the dependency issues found will be written to the output location specified. e.g. `deptry . -o deptry.json`""",
show_default=True,
)
@click.option(
"--github-output",
"-go",
is_flag=True,
help="""If specified, dependency issues found will be written in the format of github annotation. e.g. `deptry . -o deptry.json`""",
)
@click.option(
"--github-warning-errors",
"-gwe",
type=COMMA_SEPARATED_TUPLE,
help="""A comma-separated list of error codes that should be printed as warnings.
If not specified, all violations will be reported as errors.""",
default=(),
show_default=False,
)
@click.option(
"--package-module-name-map",
"-pmnm",
Expand Down Expand Up @@ -254,6 +269,8 @@ def cli(
requirements_files_dev: tuple[str, ...],
known_first_party: tuple[str, ...],
json_output: str,
github_output: bool,
github_warning_errors: tuple[str, ...],
package_module_name_map: MutableMapping[str, tuple[str, ...]],
pep621_dev_dependency_groups: tuple[str, ...],
experimental_namespace_package: bool,
Expand Down Expand Up @@ -287,6 +304,8 @@ def cli(
requirements_files_dev=requirements_files_dev,
known_first_party=known_first_party,
json_output=json_output,
github_output=github_output,
github_warning_errors=list(github_warning_errors),
package_module_name_map=package_module_name_map,
pep621_dev_dependency_groups=pep621_dev_dependency_groups,
experimental_namespace_package=experimental_namespace_package,
Expand Down
7 changes: 6 additions & 1 deletion python/deptry/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from deptry.imports.extract import get_imported_modules_from_list_of_files
from deptry.module import ModuleBuilder, ModuleLocations
from deptry.python_file_finder import get_all_python_files_in
from deptry.reporters import JSONReporter, TextReporter
from deptry.reporters import GithubReporter, JSONReporter, TextReporter
from deptry.stdlibs import STDLIBS_PYTHON
from deptry.violations.finder import find_violations

Expand Down Expand Up @@ -42,6 +42,8 @@
package_module_name_map: Mapping[str, tuple[str, ...]]
pep621_dev_dependency_groups: tuple[str, ...]
experimental_namespace_package: bool
github_output: bool
github_warning_errors: list[str]

def run(self) -> None:
self._log_config()
Expand Down Expand Up @@ -89,6 +91,9 @@
if self.json_output:
JSONReporter(violations, self.json_output).report()

if self.github_output:
GithubReporter(violations, warning_ids=self.github_warning_errors).report()

Check warning on line 95 in python/deptry/core.py

View check run for this annotation

Codecov / codecov/patch

python/deptry/core.py#L95

Added line #L95 was not covered by tests

self._exit(violations)

def _find_python_files(self) -> list[Path]:
Expand Down
3 changes: 2 additions & 1 deletion python/deptry/reporters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from deptry.reporters.github import GithubReporter
from deptry.reporters.json import JSONReporter
from deptry.reporters.text import TextReporter

__all__ = ("JSONReporter", "TextReporter")
__all__ = ("GithubReporter", "JSONReporter", "TextReporter")
72 changes: 72 additions & 0 deletions python/deptry/reporters/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from deptry.reporters.base import Reporter

if TYPE_CHECKING:
from deptry.violations import Violation


@dataclass
class GithubReporter(Reporter):
warning_ids: list[str] = field(default_factory=list) # list of error codes to print as warnings

def report(self) -> None:
self._log_and_exit()

def _log_and_exit(self) -> None:
self._log_violations(self.violations)

def _log_violations(self, violations: list[Violation]) -> None:
for violation in violations:
self._print_github_annotation(violation)

def _print_github_annotation(self, violation: Violation) -> None:
annotation_severity = "warning" if violation.error_code in self.warning_ids else "error"
file_name = violation.location.file
if violation.location.line is not None and violation.location.column is not None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message mentions that it's possible to add a message on an entire file if we don't have a specific position. For some violations, we don't have the line nor the column because they apply to a whole dependency file (e.g., pyproject.toml), where we currently are not able to find the exact line.

Maybe we could add the annotations if we only have the file? Or maybe it would be spammy, if we have multiple violations in a single dependency file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can go with adding each message on its own, and see what are the feedbacks.
I can add logic to collect all message per file that are missing position and try to output them all together but I would say it might be both an overkill and also maybe it is good that each error will get its own notification

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with adding one message per issue on a file in the case where we don't have a line/column (hoping that GitHub allows doing this and won't only take the last one in that case).

Just to make myself fully understood since reading my comment again it could be confusing, I was talking about supporting GitHub output for DEP002 basically:

$ uv run deptry . --github-output
[...]
pyproject.toml: DEP002 'click' defined as a dependency but not used in the codebase
pyproject.toml: DEP002 'colorama' defined as a dependency but not used in the codebase

Where I believe we could add:

::error file=pyproject.toml,title=DEP002::'click' defined as a dependency but not used in the codebase
::error file=pyproject.toml,title=DEP002::'colorama' defined as a dependency but not used in the codebase

Is this something you'd be willing to change in the PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up handling violations where we only have the file in 261aa89. Tested on a workflow, and for the annotation to appear, I still had to add a line that was non-0. It's not ideal since we would preferably point to the exact line from the dependency file, but since we don't have this information, I think that's still better to have the wrong line but still have the annotation on the file in the end, rather than nothing at all.

ret = _build_workflow_command(
annotation_severity,
str(file_name),
violation.location.line,
column=violation.location.column,
title=violation.error_code,
message=violation.get_error_message(),
)
print(ret) # noqa: T201


def _build_workflow_command(
command_name: str,
file: str,
line: int,
end_line: int | None = None,
column: int | None = None,
end_column: int | None = None,
title: str | None = None,
message: str | None = None,
) -> str:
"""Build a command to annotate a workflow."""
result = f"::{command_name} "

entries = [
("file", file),
("line", line),
("endLine", end_line),
("col", column),
("endColumn", end_column),
("title", title),
]

result = result + ",".join(f"{k}={v}" for k, v in entries if v is not None)

if message is not None:
result = result + "::" + _escape(message)

return result


def _escape(s: str) -> str:
return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
63 changes: 63 additions & 0 deletions tests/unit/reporters/test_github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import io
import sys
from pathlib import Path
from typing import TYPE_CHECKING

import pytest

from deptry.imports.location import Location
from deptry.module import Module
from deptry.reporters.github import GithubReporter, _build_workflow_command, _escape
from deptry.violations import DEP001MissingDependencyViolation

if TYPE_CHECKING:
from deptry.violations import Violation

# Extract violation instance as a parameter
violation_instance = DEP001MissingDependencyViolation(
Module("foo", package="foo-package"), Location(Path("foo.py"), 1, 2)
)

expected_warning = _build_workflow_command(
"warning",
"foo.py",
1,
column=2,
title="DEP001",
message="'foo' imported but missing from the dependency definitions",
)

expected_error = _build_workflow_command(
"error", "foo.py", 1, column=2, title="DEP001", message="'foo' imported but missing from the dependency definitions"
)


@pytest.mark.parametrize(
("violation", "warning_ids", "expected"),
[
(violation_instance, ["DEP001"], expected_warning),
(violation_instance, [], expected_error),
],
)
def test_github_annotation(
monkeypatch: pytest.MonkeyPatch, violation: Violation, warning_ids: list[str], expected: str
) -> None:
reporter = GithubReporter(violations=[violation], warning_ids=warning_ids)

captured_output = io.StringIO()
monkeypatch.setattr(sys, "stdout", captured_output)

reporter.report()
output = captured_output.getvalue().strip()
assert output == expected


def test_build_workflow_command_escaping() -> None:
# Directly test _build_workflow_command with characters needing escape.
message = "Error % occurred\r\nNew line"
escaped_message = _escape(message)
command = _build_workflow_command("warning", "file.py", 10, column=2, title="TEST", message=message)
assert "::warning file=file.py,line=10,col=2,title=TEST::" in command
assert escaped_message in command
2 changes: 2 additions & 0 deletions tests/unit/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def test__get_local_modules(
pep621_dev_dependency_groups=(),
using_default_requirements_files=True,
experimental_namespace_package=experimental_namespace_package,
github_output=False,
github_warning_errors=[],
)._get_local_modules()
== expected
)
Expand Down