diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 90a6b12c3a..a6147613c0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,6 +23,20 @@ To ease the process of reviewing your PR, do make sure to complete the following | ✓ | :hammer: Refactoring | | ✓ | :scroll: Docs | +## Changelog + + + + + + ## Description diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000000..48f7e4375b --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,24 @@ +name: changelog + +on: + pull_request: + types: [opened, synchronize, labeled, unlabeled, reopened, edited] + +permissions: + contents: read + +jobs: + build: + name: Changelog Entry Check + runs-on: ubuntu-latest + steps: + - name: Emit warning if changelog is missing + if: + contains(github.event.pull_request.labels.*.name, 'skip news :mute:') != true + && (contains(github.event.pull_request.body, '') != + true || contains(github.event.pull_request.body, '') != + true) + run: | + echo "Please add \n\n\nYOUR CHANGELOG ENTRY\n\n\n to the description of the PR \ + (or if appropriate, ask a maintainer to add the 'skip news' label)" && \ + exit 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7ce2c0cd3..55eb625070 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -89,7 +89,7 @@ repos: rev: "v6.0.0.post1" hooks: - id: rstcheck - args: ["--report-level=warning"] + args: ["--report-level=warning", "--ignore-directives=changelog"] files: ^(doc/(.*/)*.*\.rst) additional_dependencies: [Sphinx==5.0.1] - repo: https://github.com/pre-commit/mirrors-mypy @@ -102,7 +102,13 @@ repos: types: [python] args: [] require_serial: true - additional_dependencies: ["platformdirs==2.2.0", "types-pkg_resources==0.1.3"] + additional_dependencies: + [ + "platformdirs==2.2.0", + "types-pkg_resources==0.1.3", + "types-docutils==0.18.3", + "github3.py~=3.2", + ] exclude: tests(/\w*)*/functional/|tests/input|tests(/.*)+/conftest.py|doc/data/messages|tests(/\w*)*data/ - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.6.2 diff --git a/doc/conf.py b/doc/conf.py index 3f0c1177b4..6e285ebe51 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -40,6 +40,7 @@ "pylint_extensions", "pylint_messages", "pylint_options", + "pylint_changelog", "sphinx.ext.autosectionlabel", "sphinx.ext.intersphinx", "sphinx_reredirects", @@ -297,3 +298,13 @@ autosectionlabel_prefix_document = True linkcheck_ignore = ["https://github.com/PyCQA/pylint/blob/main/pylint/extensions/.*"] + + +# -- Options for pylint_changelog extension ------------------------------------ + +pylint_changelog_user = "PyCQA" +pylint_changelog_project = "pylint" +pylint_changelog_token = os.getenv("GITHUB_TOKEN") +pylint_changelog_exclude_labels = [ + "Documentation 📖", +] diff --git a/doc/exts/pylint_changelog.py b/doc/exts/pylint_changelog.py new file mode 100644 index 0000000000..e2d1bba495 --- /dev/null +++ b/doc/exts/pylint_changelog.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python + +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Custom extension to automatically generate changelog sections +from GitHub issues. +""" +from __future__ import annotations + +import re +from collections.abc import Iterable + +from docutils import frontend, nodes +from docutils.parsers.rst import directives +from docutils.utils import new_document +from github3 import login # type: ignore[import] +from github3.search.issue import IssueSearchResult # type: ignore[import] +from myst_parser.docutils_ import Parser # type: ignore[import] +from sphinx.application import Sphinx +from sphinx.util import logging +from sphinx.util.docutils import SphinxDirective + +logger = logging.getLogger(__name__) + + +class ChangelogDirective(SphinxDirective): + option_spec = { + "query": directives.unchanged_required, + "caption": directives.unchanged, + "hide_if_empty": directives.flag, + } + has_content = True + parser = Parser() + changelog_pattern = re.compile( + r"\n(?P(.*\n)*)", + flags=re.MULTILINE, + ) + + def run(self): + if not self.config.pylint_changelog_token: + logger.info( + "No Github token provided. Changelog generation will be skipped." + ) + return [] + result = [] + caption = self.options.get("caption") + if caption: + result.append(nodes.title(text=caption)) + list_node = nodes.bullet_list( + "", + *( + self._build_changelog_entry(issue) + for issue in self._get_relevant_issues() + ), + ) + result.append(list_node) + logger.info("Found %d issues for this query.", len(list_node)) + if not list_node and self.options.get("hide_if_empty", False): + logger.info("Flag 'hide_if_empty' is set, hiding this section.") + return [] + return result + + def _get_relevant_issues(self) -> Iterable[IssueSearchResult]: + full_query = self._build_query() + gh = login(token=self.config.pylint_changelog_token) + logger.info("Searching for issues/pull requests matching query %s", full_query) + return gh.search_issues(query=full_query) + + def _build_query(self) -> str: + user = self.config.pylint_changelog_user + project = self.config.pylint_changelog_project + query = self.options.get("query") + full_query = f"repo:{user}/{project} {query}" + for excluded_label in self.config.pylint_changelog_exclude_labels: + full_query += f' -label:"{excluded_label}"' + return full_query + + def _build_changelog_entry(self, issue: IssueSearchResult) -> nodes.list_item: + match = self.changelog_pattern.search(issue.body) + if match: + text = match.group("entry").strip() + else: + logger.warning( + "PR #%d is missing the changelog section. " + "Using the PR title as substitute.", + issue.number, + ) + text = issue.title + text += f"\n\nPR: [#{issue.number}](https://github.com/PyCQA/pylint/pull/{issue.number})" + return nodes.list_item("", *self._parse_markdown(text).children) + + def _parse_markdown(self, text: str) -> nodes.document: + parser = Parser() + components = (Parser,) + settings = frontend.OptionParser(components=components).get_default_values() + document = new_document("", settings=settings) + parser.parse(text, document) + return document + + +def setup(app: Sphinx) -> dict[str, str | bool]: + app.add_config_value("pylint_changelog_user", None, "html") + app.add_config_value("pylint_changelog_project", None, "html") + app.add_config_value("pylint_changelog_token", None, "html") + app.add_config_value("pylint_changelog_exclude_labels", [], "html") + app.add_directive("changelog", ChangelogDirective) + return {"version": "0.1", "parallel_read_safe": True} diff --git a/doc/requirements.txt b/doc/requirements.txt index a631b6ced7..28e93b19bc 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,6 @@ Sphinx==5.0.1 sphinx-reredirects<1 +github3.py~=3.2 myst-parser~=0.18 furo==2022.6.4.1 -e . diff --git a/doc/whatsnew/2/2.15/index.rst b/doc/whatsnew/2/2.15/index.rst index 0b6ed8b1d0..8c5d77167a 100644 --- a/doc/whatsnew/2/2.15/index.rst +++ b/doc/whatsnew/2/2.15/index.rst @@ -11,59 +11,34 @@ Summary -- Release highlights ============================= +.. changelog:: + :caption: New checkers + :query: is:closed is:pr milestone:2.15.0 label:"New checker ✨" -New checkers -============ +.. changelog:: + :caption: Removed checkers + :query: is:closed is:pr milestone:2.15.0 label:"Removed checker ❌" +.. changelog:: + :caption: Extensions + :query: is:closed is:pr milestone:2.15.0 label:"Extension" -Removed checkers -================ +.. changelog:: + :caption: False positives fixed + :query: is:closed is:pr milestone:2.15.0 label:"False Positive 🦟" +.. changelog:: + :caption: False negatives fixed + :query: is:closed is:pr milestone:2.15.0 label:"False Negative 🦋" -Extensions -========== +.. changelog:: + :caption: Other bug fixes + :query: is:closed is:pr milestone:2.15.0 label:"Bug 🪳" -label:"False Negative 🦋" -label:"False Positive 🦟" +.. changelog:: + :caption: Other Changes + :query: is:closed is:pr milestone:2.15.0 -label:"False Negative 🦋" -label:"False Positive 🦟" -label:"Bug 🪳" -label:"New checker ✨" -label:"Removed checker ❌" -False positives fixed -===================== - - -False negatives fixed -===================== - -* Emit ``modified-iterating-list`` and analogous messages for dicts and sets when iterating - literals, or when using the ``del`` keyword. - - Closes #6648 - -* Emit ``using-constant-test`` when testing the truth value of a variable or call result - holding a generator. - - Closes #6909 - -* Emit ``used-before-assignment`` for self-referencing named expressions (``:=``) lacking - prior assignments. - - Closes #5653 - - -Other bug fixes -=============== - - -Other Changes -============= - - -Internal changes -================ - -* ``pylint.testutils.primer`` is now a private API. - - Refs #6905 - -* Fixed an issue where it was impossible to update functional tests output when the existing - output was impossible to parse. Instead of raising an error we raise a warning message and - let the functional test fail with a default value. - - Refs #6891 +.. changelog:: + :caption: Internal changes + :query: is:closed is:pr milestone:2.15.0 label:"Maintenance"