diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8769160..e222104 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -25,22 +25,19 @@ jobs: steps: - uses: actions/checkout@v7 - - uses: actions/setup-python@v7 - with: - python-version: "3.14" - # ref: https://github.com/pre-commit/action - - uses: pre-commit/action@v3.0.1 + # ref: https://github.com/j178/prek-action + - uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 - name: Help message if pre-commit fail if: ${{ failure() }} run: | - echo "You can install pre-commit hooks to automatically run formatting" - echo "on each commit with:" - echo " pre-commit install" + echo "You can install pre-commit hooks (with prek or pre-commit) to" + echo "automatically run formatting on each commit with:" + echo " prek install" echo "or you can run by hand on staged files with" - echo " pre-commit run" + echo " prek run" echo "or after-the-fact on already committed files with" - echo " pre-commit run --all-files" + echo " prek run --all-files" tests: runs-on: ubuntu-24.04 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 200a9e1..62e5ae5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,22 +11,22 @@ repos: # Autoformat: Python code - repo: https://github.com/astral-sh/ruff-pre-commit - rev: f0fe93c067104b76ffb58852abe79673a8429bd1 # frozen: v0.11.8 + rev: v0.16.7 hooks: - - id: ruff + - id: ruff-check args: ["--fix", "--show-fixes", "--exit-non-zero-on-fix"] - id: ruff-format # Autoformat: markdown, yaml - - repo: https://github.com/pre-commit/mirrors-prettier - rev: 787fb9f542b140ba0b2aced38e6a3e68021647a3 # frozen: v3.5.3 + - repo: https://github.com/rbubley/mirrors-prettier + rev: v3.9.6 hooks: - id: prettier exclude: tests/.* # Misc... - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 + rev: v6.0.0 # v6.0.0 # ref: https://github.com/pre-commit/pre-commit-hooks#hooks-available hooks: # Sanity checks diff --git a/docs/example_cache.ipynb b/docs/example_cache.ipynb index a1d9a14..4bf1001 100644 --- a/docs/example_cache.ipynb +++ b/docs/example_cache.ipynb @@ -18,8 +18,8 @@ "metadata": {}, "outputs": [], "source": [ + "from github_activity.cache import get_cache_stats, load_from_cache\n", "from github_activity.github_activity import get_activity\n", - "from github_activity.cache import load_from_cache, get_cache_stats\n", "\n", "activity = get_activity(\"jupyter/repo2docker\", \"2019\", cache=True)" ] diff --git a/docs/use.md b/docs/use.md index 68b647f..3a18192 100644 --- a/docs/use.md +++ b/docs/use.md @@ -266,7 +266,7 @@ df = get_activity( until="2023-12-31", auth="your-github-token", kind=None, - cache=None + cache=None, ) ``` @@ -277,5 +277,5 @@ There are some helper functions for this. For example, to extract nested comment from github_activity import get_activity, extract_comments df = get_activity(...) -comments_df = extract_comments(df['comments']) +comments_df = extract_comments(df["comments"]) ``` diff --git a/github_activity/__init__.py b/github_activity/__init__.py index 5fb3634..c32163f 100644 --- a/github_activity/__init__.py +++ b/github_activity/__init__.py @@ -1,4 +1,4 @@ __version__ = "1.1.7" -__all__ = ["get_activity", "generate_activity_md"] +__all__ = ["generate_activity_md", "get_activity"] -from .github_activity import get_activity, generate_activity_md +from .github_activity import generate_activity_md, get_activity diff --git a/github_activity/cache.py b/github_activity/cache.py index 963e8fc..85d27c9 100644 --- a/github_activity/cache.py +++ b/github_activity/cache.py @@ -19,7 +19,7 @@ def _cache_data(query_data, path_cache): path_repo_cache.mkdir(parents=True, exist_ok=True) # First pull issues - def _categorize_item(item): + def _categorize_item(item, repo=repo): if f"{repo}/issues" in item: out = "issue" else: diff --git a/github_activity/cli.py b/github_activity/cli.py index 67ace69..5f3c5dd 100644 --- a/github_activity/cli.py +++ b/github_activity/cli.py @@ -2,14 +2,14 @@ import json import os import sys -from subprocess import PIPE -from subprocess import run +from subprocess import PIPE, run -from .git import _git_installed_check -from .git import _git_toplevel_path -from .github_activity import _parse_target -from .github_activity import generate_activity_md -from .github_activity import generate_all_activity_md +from .git import _git_installed_check, _git_toplevel_path +from .github_activity import ( + _parse_target, + generate_activity_md, + generate_all_activity_md, +) DESCRIPTION = "Generate a markdown changelog of GitHub activity within a date window." @@ -191,7 +191,7 @@ def main(): if not args.target: err = "Could not automatically detect remote, and none was given." try: - out = run("git remote -v".split(), stdout=PIPE) + out = run(["git", "remote", "-v"], stdout=PIPE, check=False) remotes = out.stdout.decode().split("\n") remotes = [ii for ii in remotes if ii] remotes = { @@ -214,16 +214,16 @@ def main(): except Exception: raise ValueError(err) - common_kwargs = dict( - kind=args.kind, - auth=args.auth, - tags=tags, - include_issues=bool(args.include_issues), - include_opened=bool(args.include_opened), - strip_brackets=bool(args.strip_brackets), - branch=args.branch, - ignored_contributors=args.ignore_contributor, - ) + common_kwargs = { + "kind": args.kind, + "auth": args.auth, + "tags": tags, + "include_issues": bool(args.include_issues), + "include_opened": bool(args.include_opened), + "strip_brackets": bool(args.strip_brackets), + "branch": args.branch, + "ignored_contributors": args.ignore_contributor, + } # Wrap in a try/except so we don't have an ugly stack trace if there's an error try: diff --git a/github_activity/github_activity.py b/github_activity/github_activity.py index 87be206..d9edbfa 100644 --- a/github_activity/github_activity.py +++ b/github_activity/github_activity.py @@ -10,9 +10,7 @@ import sys from collections import OrderedDict from json import loads -from subprocess import CalledProcessError -from subprocess import PIPE -from subprocess import run +from subprocess import PIPE, CalledProcessError, run from tempfile import TemporaryDirectory import dateutil.parser @@ -25,7 +23,6 @@ from .cache import _cache_data from .graphql import GitHubGraphQlQuery - # The tags and description to use in creating subsets of PRs TAGS_METADATA_BASE = OrderedDict( [ @@ -175,7 +172,9 @@ def get_activity( if auth is None: # Attempt to use the gh cli if installed try: - p = run(["gh", "auth", "token"], text=True, capture_output=True) + p = run( + ["gh", "auth", "token"], text=True, capture_output=True, check=False + ) auth = p.stdout.strip() except CalledProcessError: print( @@ -270,7 +269,7 @@ def generate_all_activity_md( include_opened=False, strip_brackets=False, branch=None, - ignored_contributors: list[str] = None, + ignored_contributors: list[str] | None = None, ): """Generate a full markdown changelog of GitHub activity of a repo based on release tags. @@ -316,10 +315,12 @@ def generate_all_activity_md( # Get the sha and tag name for each tag in the target repo with TemporaryDirectory() as td: subprocess.run( - shlex.split(f"git clone https://github.com/{target} repo"), cwd=td + shlex.split(f"git clone https://github.com/{target} repo"), + cwd=td, + check=False, ) repo = os.path.join(td, "repo") - subprocess.run(shlex.split("git fetch origin --tags"), cwd=repo) + subprocess.run(shlex.split("git fetch origin --tags"), cwd=repo, check=False) cmd = 'git log --tags --simplify-by-decoration --pretty="format:%h | %D"' data = ( @@ -402,8 +403,7 @@ def add(self, contributor): def __iter__(self): if self.author: yield self.author - for item in sorted(self.other - {self.author}): - yield item + yield from sorted(self.other - {self.author}) def generate_activity_md( @@ -418,7 +418,7 @@ def generate_activity_md( strip_brackets=False, heading_level=1, branch=None, - ignored_contributors: list[str] = None, + ignored_contributors: list[str] | None = None, ): """Generate a markdown changelog of GitHub activity within a date window. @@ -536,12 +536,10 @@ def ignored_user(username): return True # Check against user-specified ignored contributors - if ignored_contributors and any( - fnmatch.fnmatch(username, user) for user in ignored_contributors - ): - return True - - return False + return bool( + ignored_contributors + and any(fnmatch.fnmatch(username, user) for user in ignored_contributors) + ) def filter_ignored(userlist): return {user for user in userlist if not ignored_user(user)} @@ -617,7 +615,7 @@ def filter_ignored(userlist): comment_contributors = comment_contributor_counts[ comment_contributor_counts >= comment_others_cutoff ].index.tolist() - all_contributors |= set(c for c in comment_contributors if isinstance(c, str)) + all_contributors |= {c for c in comment_contributors if isinstance(c, str)} closed_mask, opened_mask = _activity_window_masks( data, data.since_dt_str, data.until_dt_str, data.since_is_git_ref @@ -646,7 +644,7 @@ def filter_ignored(userlist): # Add any contributors to a merged PR to our contributors list # Filter out NaN values and non-strings pr_contributors = closed_prs["contributors"].explode().unique().tolist() - all_contributors |= set(c for c in pr_contributors if isinstance(c, str)) + all_contributors |= {c for c in pr_contributors if isinstance(c, str)} # Define categories for a few labels if tags is None: @@ -660,7 +658,7 @@ def filter_ignored(userlist): tags_metadata = {key: val for key, val in TAGS_METADATA_BASE.items() if key in tags} # Initialize our tags with empty metadata - for key, vals in tags_metadata.items(): + for vals in tags_metadata.values(): vals.update( { "mask": None, @@ -673,14 +671,18 @@ def filter_ignored(userlist): # Track which PRs have already been assigned to prevent duplicates assigned_prs = set() - for kind, kindmeta in tags_metadata.items(): + for kindmeta in tags_metadata.values(): # First find the PRs based on tag mask = closed_prs["labels"].map( - lambda a: any(ii == jj for ii in kindmeta["tags"] for jj in a) + lambda a, kindmeta=kindmeta: any( + ii == jj for ii in kindmeta["tags"] for jj in a + ) ) # Now find PRs based on prefix mask_pre = closed_prs["title"].map( - lambda title: any(f"{ipre}:" in title for ipre in kindmeta["pre"]) + lambda title, kindmeta=kindmeta: any( + f"{ipre}:" in title for ipre in kindmeta["pre"] + ) ) mask = mask | mask_pre @@ -705,31 +707,31 @@ def filter_ignored(userlist): # Add some optional kinds of PRs / issues tags_metadata.update( - dict(others={"description": other_description, "md": [], "data": others}) + {"others": {"description": other_description, "md": [], "data": others}} ) if include_issues: tags_metadata.update( - dict( - closed_issues={ + { + "closed_issues": { "description": "Closed issues", "md": [], "data": closed_issues, } - ) + } ) if include_opened: tags_metadata.update( - dict( - opened_issues={ + { + "opened_issues": { "description": "Opened issues", "md": [], "data": opened_issues, } - ) + } ) if include_opened: tags_metadata.update( - dict(opened_prs={"description": "Opened PRs", "md": [], "data": opened_prs}) + {"opened_prs": {"description": "Opened PRs", "md": [], "data": opened_prs}} ) # Generate the markdown @@ -737,7 +739,7 @@ def filter_ignored(userlist): extra_head = "#" * (heading_level - 1) - for kind, items in prs.items(): + for items in prs.values(): n_orgs = len(items["data"]["org"].unique()) for org, idata in items["data"].groupby("org"): if n_orgs > 1: @@ -794,7 +796,7 @@ def filter_ignored(userlist): "", f"([full changelog]({changelog_url}))", ] - for kind, info in prs.items(): + for info in prs.values(): if len(info["md"]) > 0: md += [""] md.append(f"{extra_head}## {info['description']}") @@ -943,9 +945,7 @@ def _get_datetime_and_type(org, repo, datetime_or_git_ref, auth): return (dt, False) except Exception: raise ValueError( - "{0} not found as a ref or valid date format".format( - datetime_or_git_ref - ) + f"{datetime_or_git_ref} not found as a ref or valid date format" ) @@ -981,7 +981,7 @@ def _get_latest_release_tag(org, repo): ] print(f"Auto-detecting latest release tag for: {org}/{repo}", file=sys.stderr) print(f"Running command: {' '.join(cmd)}", file=sys.stderr) - out = run(cmd, stdout=PIPE) + out = run(cmd, stdout=PIPE, check=False) try: json = out.stdout.decode() release_data = loads(json) @@ -998,6 +998,6 @@ def _get_latest_release_tag(org, repo): f"Error getting latest release tag for {org}/{repo}: {e}", file=sys.stderr ) print("Reverting to using latest local git tag...", file=sys.stderr) - out = run("git describe --tags".split(), stdout=PIPE) + out = run(["git", "describe", "--tags"], stdout=PIPE, check=False) tag = out.stdout.decode().rsplit("-", 2)[0] return tag diff --git a/github_activity/graphql.py b/github_activity/graphql.py index 48439e3..a3dd7f3 100644 --- a/github_activity/graphql.py +++ b/github_activity/graphql.py @@ -175,12 +175,12 @@ def request(self, n_pages=100, n_per_page=50): self.issues_and_or_prs = [] for ii in range(n_pages): github_search_query = [ - "first: %s" % n_per_page, - 'query: "%s"' % self.query, + f"first: {n_per_page}", + f'query: "{self.query}"', "type: ISSUE", ] if ii != 0: - github_search_query.append('after: "%s"' % pageInfo["endCursor"]) + github_search_query.append(f'after: "{pageInfo["endCursor"]}"') ii_gql_query = self.gql_template.format( query=", ".join(github_search_query), @@ -215,9 +215,7 @@ def request(self, n_pages=100, n_per_page=50): except (ValueError, KeyError): pass raise Exception( - "Query failed to run by returning code of {}. {}".format( - ii_request.status_code, ii_gql_query - ) + f"Query failed to run by returning code of {ii_request.status_code}. {ii_gql_query}" ) errors = ii_request.json().get("errors") if errors: @@ -230,7 +228,7 @@ def request(self, n_pages=100, n_per_page=50): "Please wait before making more requests, or use an authentication token with higher rate limits." ) raise Exception( - "Query failed to run with error {}. {}".format(errors, ii_gql_query) + f"Query failed to run with error {errors}. {ii_gql_query}" ) self.last_request = ii_request @@ -306,9 +304,12 @@ def is_bot(user_dict): commit = commit_edge["node"]["commit"] # Check committer committer = commit.get("committer") - if committer and committer.get("user"): - if is_bot(committer["user"]): - bot_users.add(committer["user"]["login"]) + if ( + committer + and committer.get("user") + and is_bot(committer["user"]) + ): + bot_users.add(committer["user"]["login"]) # Check authors authors = commit.get("authors") if authors: diff --git a/pyproject.toml b/pyproject.toml index e4f6550..1add150 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,13 @@ dependencies = { file = "requirements.txt" } fallback_version = "0.0.0" +[tool.ruff.lint] +ignore = [ + "BLE001", # blind `except Exception` + "S110", # try-except-pass + "TRY002", # raising vanilla `Exception` +] + [tool.ruff.lint.per-file-ignores] "docs/conf.py" = ["E402"] diff --git a/tests/test_activity_window.py b/tests/test_activity_window.py index a9954fb..d4ce96d 100644 --- a/tests/test_activity_window.py +++ b/tests/test_activity_window.py @@ -58,7 +58,7 @@ def __init__(self, *args, **kwargs): self.data = pd.DataFrame(rows) self.data.attrs["bot_users"] = set() - request = lambda self: None # noqa: E731 + request = lambda self: None monkeypatch.setattr( "github_activity.github_activity.GitHubGraphQlQuery", @@ -117,12 +117,12 @@ def test_empty_activity(monkeypatch, api): _mock_get_activity_dependencies( monkeypatch, [], since_dt, until_dt, since_is_git_ref=False ) - kwargs = dict( - target="jupyterhub/action-k3s-helm", - since="2022-08-21", - until="2022-08-22", - auth="test-token", - ) + kwargs = { + "target": "jupyterhub/action-k3s-helm", + "since": "2022-08-21", + "until": "2022-08-22", + "auth": "test-token", + } if api is get_activity: assert api(**kwargs).empty diff --git a/tests/test_cli.py b/tests/test_cli.py index 9549587..28a8650 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -136,9 +136,10 @@ def test_changelog_features(file_regression): def test_invalid_repository_error(): """Test that invalid repository names produce clear error messages.""" - from github_activity.github_activity import get_activity import pytest + from github_activity.github_activity import get_activity + # Test with an invalid repository name with pytest.raises(ValueError) as exc_info: get_activity(