Skip to content

Add built-in JUnit XML reporter#10985

Open
hamza-mobeen wants to merge 4 commits intopylint-dev:mainfrom
hamza-mobeen:feature/issue-9143-junit-reporter
Open

Add built-in JUnit XML reporter#10985
hamza-mobeen wants to merge 4 commits intopylint-dev:mainfrom
hamza-mobeen:feature/issue-9143-junit-reporter

Conversation

@hamza-mobeen
Copy link
Copy Markdown

Fixes #9143

Type of Changes

Type
🐛 Bug fix
✨ New feature
🔨 Refactoring
📜 Docs

Description

Closes #9143

Problem

Third-party JUnit reporter packages (pylint2junit, pylint-junit) have not been working reliably since pylint 2.15 due to internal API changes. Users who need JUnit XML output for their CI/CD pipelines (e.g. Azure DevOps) are left without a working solution.

Solution

This PR adds a built-in JUnitReporter class so users can run:

pylint --output-format=junit mypackage/

and get valid JUnit-compatible XML output directly from pylint, with no external dependencies required.

What was changed

4 files changed, 434 insertions(+)


pylint/reporters/junit_reporter.py (new file)

New JUnitReporter class extending BaseReporter, following the same pattern used by JSONReporter. Key details:

  • Messages are grouped by module. Each module with violations becomes a <testcase>, and each violation becomes a <failure> child element.
  • Uses only xml.etree.ElementTree from the standard library — zero new dependencies.
  • The <failure> element's type attribute maps directly to the pylint message category (error, warning, convention, refactor, etc.).
  • The <failure> element's message attribute follows the format: {msg_id}({symbol}): {message} (line {line}).
  • The <failure> element's text body includes the full absolute file path for CI tool linking.
  • display_reports and _display are no-ops, same as JSONReporter.
  • A module-level register() function is provided for auto-discovery by utils.register_plugins().
class JUnitReporter(BaseReporter):
    name = "junit"
    extension = "xml"

    def display_messages(self, layout: Section | None) -> None:
        """Build and write JUnit XML from collected messages."""
        messages_by_module: dict[str, list[Message]] = defaultdict(list)
        for msg in self.messages:
            messages_by_module[msg.module].append(msg)

        testsuites_el = ET.Element("testsuites")
        testsuite_el = ET.SubElement(testsuites_el, "testsuite", name="pylint")

        total_failures = 0
        total_tests = 0

        for module_name in sorted(messages_by_module):
            module_messages = messages_by_module[module_name]
            testcase_el = ET.SubElement(
                testsuite_el, "testcase",
                name=module_name, classname=module_name,
            )
            total_tests += 1

            for msg in module_messages:
                failure_msg = (
                    f"{msg.msg_id}({msg.symbol}): {msg.msg or ''} "
                    f"(line {msg.line})"
                )
                failure_el = ET.SubElement(
                    testcase_el, "failure",
                    type=msg.category, message=failure_msg,
                )
                failure_el.text = (
                    f"{msg.abspath}:{msg.line}: [{msg.msg_id}({msg.symbol})] "
                    f"{msg.msg or ''}"
                )
                total_failures += 1

        testsuite_el.set("tests", str(total_tests))
        testsuite_el.set("failures", str(total_failures))

        tree = ET.ElementTree(testsuites_el)
        ET.indent(tree, space="  ")
        tree.write(self.out, encoding="unicode", xml_declaration=True)
        print(file=self.out)

pylint/reporters/__init__.py (modified)

Added JUnitReporter import and __all__ export:

 from pylint.reporters.json_reporter import JSON2Reporter, JSONReporter
+from pylint.reporters.junit_reporter import JUnitReporter
 from pylint.reporters.multi_reporter import MultiReporter

 __all__ = [
     "BaseReporter",
     "CollectingReporter",
     "JSON2Reporter",
     "JSONReporter",
+    "JUnitReporter",
     "MultiReporter",
     "ReportsHandlerMixIn",
 ]

Closes #9143

Checklist

  • Document your change, if it is a non-trivial one.
    • A maintainer might label the issue skip-news if the change does not need to be in the changelog.
    • Otherwise, create a news fragment with towncrier create <IssueNumber>.<type> which will be
      included in the changelog. <type> can be one of the types defined in ./towncrier.toml.
      If necessary you can write details or offer examples on how the new change is supposed to work.
    • Generating the doc is done with tox -e docs
  • Relate your change to an issue in the tracker if such an issue exists (Refs incorrect analysing of etree #1234, Closes incorrect analysing of etree #1234)
  • Write comprehensive commit messages and/or a good description of what the PR does.
  • Keep the change small, separate the consensual changes from the opinionated one.
    Don't hesitate to open multiple PRs if the change requires it. If your review is so
    big it requires to actually plan and allocate time to review, it's more likely
    that it's going to go stale.
  • If you used multiple emails or multiple names when contributing, add your mails
    and preferred name in script/.contributors_aliases.json

@hamza-mobeen
Copy link
Copy Markdown
Author

Hi @Pierre-Sassoulas hope you doing good ,
This PR is ready for review. Please I would like you take a look when you have a moment, and let me know if you have any feedback or if further adjustments are needed.
Thanks!

@Pierre-Sassoulas Pierre-Sassoulas added the Enhancement ✨ Improvement to a component label Apr 26, 2026
@Pierre-Sassoulas Pierre-Sassoulas added this to the 4.1.0 milestone Apr 26, 2026
@Pierre-Sassoulas
Copy link
Copy Markdown
Member

Hey @hamza-mobeen, thank you for working on this. It seems another tests was not independant, do you mind fixing it ? (You can check tests locally with pytest, as you're a first time contributor I must approve all the pipeline).

@github-actions

This comment has been minimized.

@hamza-mobeen
Copy link
Copy Markdown
Author

Hey @hamza-mobeen, thank you for working on this. It seems another tests was not independant, do you mind fixing it ? (You can check tests locally with pytest, as you're a first time contributor I must approve all the pipeline).

alright sure

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.20%. Comparing base (b080a21) to head (ff43fad).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main   #10985   +/-   ##
=======================================
  Coverage   96.19%   96.20%           
=======================================
  Files         178      179    +1     
  Lines       19683    19720   +37     
=======================================
+ Hits        18934    18971   +37     
  Misses        749      749           
Files with missing lines Coverage Δ
pylint/reporters/__init__.py 100.00% <100.00%> (ø)
pylint/reporters/junit_reporter.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hamza-mobeen
Copy link
Copy Markdown
Author

hello @Pierre-Sassoulas I made a recent update this fixes the ci errors we were having please check it out when you have a moment. thanks

@github-actions
Copy link
Copy Markdown
Contributor

🤖 According to the primer, this change has no effect on the checked open source code. 🤖🎉

This comment was generated for commit ff43fad

@hamza-mobeen
Copy link
Copy Markdown
Author

Hello @Pierre-Sassoulas still waiting for your feedback. thanks

Copy link
Copy Markdown
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

Hello @hamza-mobeen thank you for your work on this.

You should add junit in the format list of doc/user_guide/usage/output.rst, and junit to the --output-format help string in pylint/lint/base_options.py and regenerate the documezntation with tox -e docs.

Also, there's existing junit solution https://pypi.org/project/pylint-junit/ (730k download a month), or https://github.com/aroden-crowdstrike/pylint2junit (5k download a month), for example. Did you consider those ? Guaranteeing retro-compatibility with the big one would be nice.

Comment thread pylint/reporters/junit_reporter.py Outdated
Comment on lines +66 to +80
for msg in module_messages:
failure_msg = (
f"{msg.msg_id}({msg.symbol}): {msg.msg or ''} " f"(line {msg.line})"
)
failure_el = ET.SubElement(
testcase_el,
"failure",
type=msg.category,
message=failure_msg,
)
failure_el.text = (
f"{msg.abspath}:{msg.line}: [{msg.msg_id}({msg.symbol})] "
f"{msg.msg or ''}"
)
total_failures += 1
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The fact that a message is a failure or not should depend on pylint options like fail-on, or error-only (some message can be raised without pylint exiting in error). Is there only one level of message in the junit spec ? Because error message or fatal message are very different thing in pylint.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@Pierre-Sassoulas I've resolved this with my current commit please check it out when you available

@hamza-mobeen
Copy link
Copy Markdown
Author

@Pierre-Sassoulas thanks for the feedback, I just made an updated regarding your feedback,
The now tracks errors and failures counts separately.
Regarding --fail-on and --error-only those options filter which messages get emitted before they reach the reporter, so the reporter only sees messages that already passed through those filters. The XML element type is then determined by the message's category, which I think is the cleanest separation of concerns.
I also added junit to the format list in doc/user_guide/usage/output.rst and to the --output-format help string in base_options.py.
Regarding pylint-junit the output follows the same standard JUnit XML schema (testsuites → testsuite → testcase → error/failure),

please check it out when you free, thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement ✨ Improvement to a component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add pylint-junit reporter class incorrect analysing of etree

2 participants