From d99aa3243eef6adfff1f70ba9573696d9730a2d2 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 4 Dec 2025 08:43:31 +0530 Subject: [PATCH 1/6] add `ignored_user_logins` param to `ChangeNote.from_pull_requests` --- src/changelist/_cli.py | 1 + src/changelist/_objects.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/changelist/_cli.py b/src/changelist/_cli.py index 46c0bdb..afe9271 100644 --- a/src/changelist/_cli.py +++ b/src/changelist/_cli.py @@ -157,6 +157,7 @@ def main( pull_requests, pr_summary_regex=config["pr_summary_regex"], pr_summary_label_regex=config["pr_summary_label_regex"], + ignored_user_logins=tuple(config["ignored_user_logins"]), ) Formatter = {"md": MdFormatter, "rst": RstFormatter}[format] diff --git a/src/changelist/_objects.py b/src/changelist/_objects.py index eb88cfa..78fe946 100644 --- a/src/changelist/_objects.py +++ b/src/changelist/_objects.py @@ -27,6 +27,7 @@ def from_pull_requests( *, pr_summary_regex: str, pr_summary_label_regex: str, + ignored_user_logins: tuple[str, ...] = (), ) -> "set[ChangeNote]": """Create a set of notes from pull requests. @@ -39,12 +40,22 @@ def from_pull_requests( requests and notes somewhat. While ideally, a pull request introduces a change that would be described in a single note, this is often not the case. + + `ignored_user_logins` is a list of user logins whose pull requests + should be excluded from the changelog entirely. """ pr_summary_regex = re.compile(pr_summary_regex, flags=re.MULTILINE) pr_summary_label_regex = re.compile(pr_summary_label_regex) notes = set() for pr in pull_requests: + if pr.user and pr.user.login in ignored_user_logins: + logger.debug( + "skipping PR %s from ignored user %s", + pr.html_url, + pr.user.login, + ) + continue pr_labels = tuple(label.name for label in pr.labels) if not pr.body or not ( From e4a162c43ce7fb1a7895f05eb6137c132399d138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Thu, 4 Dec 2025 14:51:52 +0100 Subject: [PATCH 2/6] Add `ignore_prs_by_username` field with include option This allows users to configure ignored contributors and which PRs are ingnored by users independently. At the same time most users for which these two fields may look the same don't have to repeat themselves. --- README.md | 12 ++++++++++-- src/changelist/_config.py | 24 ++++++++++++++++++++++++ src/changelist/_objects.py | 9 ++++++--- src/changelist/default_config.toml | 10 +++++++++- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 668a622..0b24724 100644 --- a/README.md +++ b/README.md @@ -102,11 +102,19 @@ _These lists are automatically generated, and may not be complete or may contain duplicates._ """ -# Profiles that are excluded from the contributor list. +# Usernames that are excluded from the contributor list. This may also ignore +# pull request of these users (see field `ignore_prs_by_username`). ignored_user_logins = [ "web-flow", ] +# Ignore pull requests authored by these users. +# If `{include = "ignored_user_logins"}` is included in this list (the default), +# usernames from the field `ignored_user_logins` are also included. +ignore_prs_by_username = [ + {include = "ignored_user_logins"}, +] + # If this regex matches a pull requests description, the captured content # is included instead of the pull request title. E.g. the # default regex below is matched by @@ -228,7 +236,7 @@ jobs: name: attach to PR runs-on: ubuntu-latest steps: - - uses: scientific-python/attach-next-milestone-action@bc07be829f693829263e57d5e8489f4e57d3d420 + - uses: scientific-python/attach-next-milestone-action@c9cfab10ad0c67fed91b01103db26b7f16634639 with: token: ${{ secrets.MILESTONE_LABELER_TOKEN }} force: true diff --git a/src/changelist/_config.py b/src/changelist/_config.py index 223cd5c..8390dfb 100644 --- a/src/changelist/_config.py +++ b/src/changelist/_config.py @@ -13,6 +13,28 @@ DEFAULT_CONFIG_PATH = Path(__file__).parent / "default_config.toml" +def _dereference_ignore_prs_by_username(config: dict) -> dict: + """Dereference "include" in `ignore_prs_by_username` field. + + Example: + >>> config = { + ... "ignored_user_logins": ["bot"], + ... "ignore_prs_by_username": [{"include": "ignored_user_logins"}, "web-flow"] + ... } + >>> _dereference_ignore_prs_by_username(config) + {'ignored_user_logins': ['bot'], 'ignore_prs_by_username': ['web-flow', 'bot']} + """ + if "ignore_prs_by_username" not in config: + return config + + include_sentinel = {"include": "ignored_user_logins"} + if include_sentinel in config["ignore_prs_by_username"]: + config["ignore_prs_by_username"].remove(include_sentinel) + config["ignore_prs_by_username"] += config.get("ignored_user_logins", []) + + return config + + def remote_config(gh: Github, org_repo: str, *, rev: str): """Return configuration options in remote pyproject.toml if they exist.""" repo = gh.get_repo(org_repo) @@ -24,6 +46,7 @@ def remote_config(gh: Github, org_repo: str, *, rev: str): content = "" config = tomllib.loads(content) config = config.get("tool", {}).get("changelist", {}) + config = _dereference_ignore_prs_by_username(config) return config @@ -32,6 +55,7 @@ def local_config(path: Path) -> dict: with path.open("rb") as fp: config = tomllib.load(fp) config = config.get("tool", {}).get("changelist", {}) + config = _dereference_ignore_prs_by_username(config) return config diff --git a/src/changelist/_objects.py b/src/changelist/_objects.py index 78fe946..de2ad7b 100644 --- a/src/changelist/_objects.py +++ b/src/changelist/_objects.py @@ -27,7 +27,7 @@ def from_pull_requests( *, pr_summary_regex: str, pr_summary_label_regex: str, - ignored_user_logins: tuple[str, ...] = (), + ignore_prs_by_username: list[str] | None = None, ) -> "set[ChangeNote]": """Create a set of notes from pull requests. @@ -41,15 +41,18 @@ def from_pull_requests( a change that would be described in a single note, this is often not the case. - `ignored_user_logins` is a list of user logins whose pull requests + `ignore_prs_by_username` is a list of user logins whose pull requests should be excluded from the changelog entirely. """ + if ignore_prs_by_username is None: + ignore_prs_by_username = [] + pr_summary_regex = re.compile(pr_summary_regex, flags=re.MULTILINE) pr_summary_label_regex = re.compile(pr_summary_label_regex) notes = set() for pr in pull_requests: - if pr.user and pr.user.login in ignored_user_logins: + if pr.user and pr.user.login in ignore_prs_by_username: logger.debug( "skipping PR %s from ignored user %s", pr.html_url, diff --git a/src/changelist/default_config.toml b/src/changelist/default_config.toml index 67e0a77..643e6e4 100644 --- a/src/changelist/default_config.toml +++ b/src/changelist/default_config.toml @@ -21,11 +21,19 @@ _These lists are automatically generated, and may not be complete or may contain duplicates._ """ -# Profiles that are excluded from the contributor list. +# Usernames that are excluded from the contributor list. This may also ignore +# pull request of these users (see field `ignore_prs_by_username`). ignored_user_logins = [ "web-flow", ] +# Ignore pull requests authored by these users. +# If `{include = "ignored_user_logins"}` is included in this list (the default), +# usernames from the field `ignored_user_logins` are also included. +ignore_prs_by_username = [ + {include = "ignored_user_logins"}, +] + # If this regex matches a pull requests description, the captured content # is included instead of the pull request title. E.g. the # default regex below is matched by From 7c0f65ec20581184316e0e10289dc46e05d281a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Thu, 4 Dec 2025 14:52:21 +0100 Subject: [PATCH 3/6] Test new `ignore_prs_by_username` option --- test/test_objects.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/test/test_objects.py b/test/test_objects.py index 18caf5a..320b38b 100644 --- a/test/test_objects.py +++ b/test/test_objects.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Union @@ -12,6 +12,13 @@ DEFAULT_CONFIG = local_config(DEFAULT_CONFIG_PATH) +@dataclass +class _MockUser: + """Mocks github.User partially.""" + + login: str + + @dataclass class _MockLabel: """Mocks github.Label.Label partially.""" @@ -26,6 +33,7 @@ class _MockPullRequest: title: str body: Union[str, None] labels: list[_MockLabel] + user: _MockUser = field(default_factory=lambda: _MockUser(login="friendlyDev")) number: int = (42,) html_url: str = "https://github.com/scientific-python/changelist/pull/53" merged_at: datetime = datetime(2024, 1, 1) @@ -73,6 +81,40 @@ def test_from_pull_requests_multiple(self, caplog): assert caplog.records[0].levelname == "DEBUG" assert "falling back to PR labels for summary" in caplog.records[0].msg + def test_from_pull_requests_ignore_by_username(self, caplog): + caplog.set_level("DEBUG") + pull_requests = [ + _MockPullRequest( + title="PR from user", + body=None, + labels=[_MockLabel("Documentation")], + user=_MockUser("someDev"), + ), + _MockPullRequest( + title="PR from bot", + body=None, + labels=[_MockLabel("Documentation")], + user=_MockUser("bot"), + ), + ] + notes = ChangeNote.from_pull_requests( + pull_requests, + pr_summary_regex=DEFAULT_CONFIG["pr_summary_regex"], + pr_summary_label_regex=DEFAULT_CONFIG["pr_summary_label_regex"], + ignore_prs_by_username=["bot"], + ) + assert len(notes) == 1 + notes = list(notes) + assert notes[0].content == "PR from user" + assert notes[0].labels == ("Documentation",) + + assert len(caplog.records) == 2 + assert caplog.records[0].levelname == "DEBUG" + assert "falling back to title" in caplog.records[0].msg + + assert caplog.records[1].levelname == "DEBUG" + assert "skipping PR %s from ignored user %s" in caplog.records[1].msg + def test_from_pull_requests_fallback_title(self, caplog): caplog.set_level("DEBUG") pull_request = _MockPullRequest( From c9abcb9df770a84005d388056bad72fd4d5528b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Thu, 4 Dec 2025 14:55:09 +0100 Subject: [PATCH 4/6] Use `Optional` instead of `| None` to support Python 3.9 --- src/changelist/_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/changelist/_objects.py b/src/changelist/_objects.py index de2ad7b..76ff3e0 100644 --- a/src/changelist/_objects.py +++ b/src/changelist/_objects.py @@ -2,7 +2,7 @@ import re from dataclasses import dataclass from datetime import datetime -from typing import Union +from typing import Optional, Union from github.NamedUser import NamedUser from github.PullRequest import PullRequest @@ -27,7 +27,7 @@ def from_pull_requests( *, pr_summary_regex: str, pr_summary_label_regex: str, - ignore_prs_by_username: list[str] | None = None, + ignore_prs_by_username: Optional[list[str]] = None, ) -> "set[ChangeNote]": """Create a set of notes from pull requests. From 59c2fe443dd5e82e59076197377b4ddbec737568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Thu, 4 Dec 2025 15:11:45 +0100 Subject: [PATCH 5/6] Fix erroneous refactor --- src/changelist/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/changelist/_cli.py b/src/changelist/_cli.py index afe9271..6dbfc10 100644 --- a/src/changelist/_cli.py +++ b/src/changelist/_cli.py @@ -157,7 +157,7 @@ def main( pull_requests, pr_summary_regex=config["pr_summary_regex"], pr_summary_label_regex=config["pr_summary_label_regex"], - ignored_user_logins=tuple(config["ignored_user_logins"]), + ignore_prs_by_username=config["ignore_prs_by_username"], ) Formatter = {"md": MdFormatter, "rst": RstFormatter}[format] From d7f5df19f4c810a7a5e3b82b4ef36ab2b539a76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Thu, 4 Dec 2025 15:13:35 +0100 Subject: [PATCH 6/6] Apply dereferencing last when adding defaults Dereferencing can only be done once both fields have been loaded which may happen only at this stage when adding defaults. --- src/changelist/_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/changelist/_config.py b/src/changelist/_config.py index 8390dfb..5fd3e5a 100644 --- a/src/changelist/_config.py +++ b/src/changelist/_config.py @@ -46,7 +46,6 @@ def remote_config(gh: Github, org_repo: str, *, rev: str): content = "" config = tomllib.loads(content) config = config.get("tool", {}).get("changelist", {}) - config = _dereference_ignore_prs_by_username(config) return config @@ -55,7 +54,6 @@ def local_config(path: Path) -> dict: with path.open("rb") as fp: config = tomllib.load(fp) config = config.get("tool", {}).get("changelist", {}) - config = _dereference_ignore_prs_by_username(config) return config @@ -74,4 +72,5 @@ def add_config_defaults( if key not in config: config[key] = value logger.debug("using default config value for %s", key) + config = _dereference_ignore_prs_by_username(config) return config