Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add ruff to QA checks #239

Merged
merged 1 commit into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
# * The current GitHub integration expects to *only* be run in a PR context
# * The `phylum-ci` action will already be run for pull request triggers
SKIP: phylum-ci
# Add annotations to the PR for any findings
RUFF_FORMAT: github
run: poetry run tox run -e qa

test-matrix:
Expand Down
7 changes: 3 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ repos:
hooks:
- id: black

- repo: https://github.com/asottile/pyupgrade
rev: 7c551d39dfa69851607d85392ffa296568e92436 # frozen: v3.3.2
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: aad2f97dee13b682f514df846844b374cdd06e7c # frozen: v0.0.264
hooks:
- id: pyupgrade
args: [--py38-plus]
- id: ruff

- repo: https://github.com/dosisod/refurb
rev: 7fb404137a6dbb8c7b346ffd904db5c17b1c24ed # frozen: v1.16.0
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ The pull request should work for Python 3.8, 3.9, 3.10, and 3.11.
Check <https://github.com/phylum-dev/phylum-ci/actions> and make sure that the tests
pass for all supported Python versions.

To ensure quality assurance (QA), a series of checks are performed in a `qa` test.
This test essentially runs the `pre-commit` hooks to ensure proper formatting and linting over the repo.
Sometimes it is necessary to bypass these checks (e.g., via `# noqa` directives). These exceptions should
be rare and include a reason in the comment for the exclusion. Create a new issue and reference that, when
more detail is needed or the exclusion is meant to be temporary.

The [Semantic Pull Requests](https://github.com/apps/semantic-pull-requests) GitHub app is in use for this repository
as a means to ensure each PR that gets merged back to the `main` branch adheres to the
[conventional commit](https://www.conventionalcommits.org) strategy. This means that either the PR title (when
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
[![GitHub Workflow Status (branch)][workflow_shield]][workflow_test]
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)][CoC]
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)][pre-commit]
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)][black]
[![Downloads](https://pepy.tech/badge/phylum/month)][downloads]
[![Discord](https://img.shields.io/discord/1070071012353376387?logo=discord)][discord_invite]

Expand All @@ -19,6 +20,7 @@ Utilities for integrating Phylum into CI pipelines (and beyond)
[workflow_test]: https://github.com/phylum-dev/phylum-ci/actions/workflows/test.yml
[CoC]: https://github.com/phylum-dev/phylum-ci/blob/main/CODE_OF_CONDUCT.md
[pre-commit]: https://github.com/pre-commit/pre-commit
[black]: https://github.com/psf/black
[downloads]: https://pepy.tech/project/phylum
[discord_invite]: https://discord.gg/Fe6pr5eW6p

Expand Down
47 changes: 47 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,53 @@ paths = ["src", "tests"]
python_version = "3.8"
format = "github"

[tool.ruff]
# Reference: https://beta.ruff.rs/docs/settings
line-length = 120
target-version = "py38"
force-exclude = true
src = ["src", "tests"]
select = [
# Using `ALL` has the risk that new rules may be enabled when `ruff` is updated but updates are
# automated, with a PR that includes enforced QA checks, to weekly dependency and pre-commit hook bumps.
"ALL"
]
ignore = [
# Reference: https://beta.ruff.rs/docs/rules/
#
# `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Prefer D211.
"D203", # one-blank-line-before-class
# `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Prefer D212.
"D213", # multi-line-summary-second-line
# Cached instance methods are okay in this project b/c instances are short lived and won't lead to memory leaks.
"B019", # cached-instance-method
# Assigning to a variable before a return statement is more readable and useful for debugging
"RET504", # unnecessary-assign
# These ignores will be removed during https://github.com/phylum-dev/phylum-ci/issues/238
"ANN", # flake8-annotations
"TCH", # flake8-type-checking
# These ignores will be removed during https://github.com/phylum-dev/phylum-ci/issues/47
"EM", # flake8-errmsg
"TRY", # tryceratops
# This ignore will be removed during https://github.com/phylum-dev/phylum-ci/issues/23
"T20", # flake8-print
]

[tool.ruff.per-file-ignores]
"test_*.py" = [
# It is expected to use `assert` statements in `pytest` test code
"S101", # assert
# `subprocess` input is controlled in test code
"S603", # subprocess-without-shell-equals-true
]

[tool.ruff.isort]
force-sort-within-sections = true

[tool.ruff.pydocstyle]
# Use PEP-257 style docstrings
convention = "pep257"

[tool.semantic_release]
# Reference: https://python-semantic-release.readthedocs.io/en/latest/configuration.html
branch = "main"
Expand Down
40 changes: 23 additions & 17 deletions src/phylum/ci/ci_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
* https://learn.microsoft.com/rest/api/azure/devops/git/pull-request-threads/list
* https://learn.microsoft.com/rest/api/azure/devops/git/pull-request-threads/create
"""
from argparse import Namespace
import base64
from functools import cached_property, lru_cache
import os
import re
import shlex
import subprocess
from typing import Optional, Tuple
import urllib.parse
from argparse import Namespace
from functools import cached_property, lru_cache
from typing import Optional

import requests

Expand Down Expand Up @@ -84,7 +84,7 @@ def is_in_pr() -> bool:
class CIAzure(CIBase):
"""Provide methods for an Azure Pipelines CI environment."""

def __init__(self, args: Namespace) -> None:
def __init__(self, args: Namespace) -> None: # noqa: D107 ; the base __init__ docstring is better here
super().__init__(args)
self.ci_platform_name = "Azure Pipelines"
if is_in_pr():
Expand Down Expand Up @@ -164,17 +164,7 @@ def common_ancestor_commit(self) -> Optional[str]:
remote = git_remote()

if is_in_pr():
# There is no single predefined variable available to provide the PR base SHA.
# Instead, it can be determined with a `git merge-base` command, like is done for the CINone implementation.
# Reference: https://learn.microsoft.com/azure/devops/pipelines/build/variables
src_branch = os.getenv("SYSTEM_PULLREQUEST_SOURCEBRANCH", "")
tgt_branch = os.getenv("SYSTEM_PULLREQUEST_TARGETBRANCH", "")
if not src_branch:
raise SystemExit(" [!] The SYSTEM_PULLREQUEST_SOURCEBRANCH environment variable must exist and be set")
if not tgt_branch:
raise SystemExit(" [!] The SYSTEM_PULLREQUEST_TARGETBRANCH environment variable must exist and be set")
print(f" [+] SYSTEM_PULLREQUEST_SOURCEBRANCH: {src_branch}")
print(f" [+] SYSTEM_PULLREQUEST_TARGETBRANCH: {tgt_branch}")
src_branch, tgt_branch = get_pr_branches()
maxrake marked this conversation as resolved.
Show resolved Hide resolved
else:
# Assume the working context is within a CI triggered build environment when not in a PR.

Expand Down Expand Up @@ -221,7 +211,7 @@ def common_ancestor_commit(self) -> Optional[str]:
cmd = ["git", "merge-base", src_branch, tgt_branch]
print(f" [*] Finding common ancestor commit with command: {shlex.join(cmd)}")
try:
common_commit = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout.strip()
common_commit = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout.strip() # noqa: S603
except subprocess.CalledProcessError as err:
ref_url = "https://learn.microsoft.com/azure/devops/pipelines/yaml-schema/steps-checkout#shallow-fetch"
print(f" [!] The common ancestor commit could not be found: {err}")
Expand Down Expand Up @@ -282,6 +272,22 @@ def post_output(self) -> None:
post_github_comment(comments_url, self.github_token, self.analysis_report)


def get_pr_branches() -> Tuple[str, str]:
"""Get the source and destination branches when in a PR context and return them as a tuple."""
# There is no single predefined variable available to provide the PR base SHA.
# Instead, it can be determined with a `git merge-base` command, like is done for the CINone implementation.
# Reference: https://learn.microsoft.com/azure/devops/pipelines/build/variables
src_branch = os.getenv("SYSTEM_PULLREQUEST_SOURCEBRANCH", "")
tgt_branch = os.getenv("SYSTEM_PULLREQUEST_TARGETBRANCH", "")
if not src_branch:
raise SystemExit(" [!] The SYSTEM_PULLREQUEST_SOURCEBRANCH environment variable must exist and be set")
if not tgt_branch:
raise SystemExit(" [!] The SYSTEM_PULLREQUEST_TARGETBRANCH environment variable must exist and be set")
print(f" [+] SYSTEM_PULLREQUEST_SOURCEBRANCH: {src_branch}")
print(f" [+] SYSTEM_PULLREQUEST_TARGETBRANCH: {tgt_branch}")
return src_branch, tgt_branch


def post_azure_comment(azure_token: str, comment: str) -> None:
"""Post a comment on an Azure Repos Pull Request (PR).

Expand Down Expand Up @@ -366,7 +372,7 @@ def post_azure_comment(azure_token: str, comment: str) -> None:
"parentCommentId": 0,
"content": comment,
"commentType": "text",
}
},
],
"status": "active",
}
Expand Down
45 changes: 23 additions & 22 deletions src/phylum/ci/ci_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@
integration (CI) environment. Common functionality is provided where possible and CI specific features are
designated as abstract methods to be defined in specific CI environments.
"""
from abc import ABC, abstractmethod
from argparse import Namespace
from collections import OrderedDict
from functools import cached_property
import json
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import tempfile
import textwrap
from abc import ABC, abstractmethod
from argparse import Namespace
from collections import OrderedDict
from functools import cached_property
from pathlib import Path
from typing import List, Optional

import pathspec
from packaging.version import Version
import pathspec
from rich.markdown import Markdown
from ruamel.yaml import YAML

Expand Down Expand Up @@ -131,7 +131,7 @@ def filter_lockfiles(self, provided_lockfiles: List[Path]) -> Lockfiles:
cmd = [str(self.cli_path), "parse", str(provided_lockfile)]
# stdout and stderr are piped to DEVNULL because we only care about the return code.
# pylint: disable-next=subprocess-run-check ; we want return code here and don't want to raise when non-zero
if bool(subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode):
if bool(subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode): # noqa: S603
print(f" [!] Provided lockfile failed to parse as a known lockfile type: {provided_lockfile}")
continue
lockfiles.append(Lockfile(provided_lockfile, self.cli_path, self.common_ancestor_commit))
Expand Down Expand Up @@ -235,7 +235,7 @@ def cli_path(self) -> Path:
# Ensure stdout is piped to DEVNULL, to keep the token from being printed in (CI log) output.
cmd = [str(cli_path), "auth", "token"]
# pylint: disable-next=subprocess-run-check ; we want the return code here and don't want to raise when non-zero
if bool(subprocess.run(cmd, stdout=subprocess.DEVNULL).returncode):
if bool(subprocess.run(cmd, stdout=subprocess.DEVNULL).returncode): # noqa: S603
raise SystemExit(" [!] A Phylum API key is required to continue.")

print(f" [+] Using Phylum CLI instance: {cli_version} at {cli_path}")
Expand All @@ -256,7 +256,7 @@ def phylum_label(self) -> str:
* Start the label with the `self.ci_platform_name`
* Replace all runs of whitespace characters with a single `-` character
"""
raise NotImplementedError()
raise NotImplementedError

@property
def lockfile_hash_object(self) -> str:
Expand Down Expand Up @@ -285,7 +285,7 @@ def common_ancestor_commit(self) -> Optional[str]:
When found, it should be returned as a string of the SHA1 sum representing the commit.
When it can't be found (or there is an error), `None` should be returned.
"""
raise NotImplementedError()
raise NotImplementedError

@property
@abstractmethod
Expand All @@ -294,7 +294,7 @@ def is_any_lockfile_changed(self) -> bool:

Implementations should return `True` if any lockfile has changed and `False` otherwise.
"""
raise NotImplementedError()
raise NotImplementedError

def update_lockfiles_change_status(self, commit: str, err_msg: Optional[str] = None) -> None:
"""Update each lockfile's change status.
Expand All @@ -308,7 +308,7 @@ def update_lockfiles_change_status(self, commit: str, err_msg: Optional[str] = N
# `--exit-code` will make git exit with 1 if there were differences while 0 means no differences.
# Any other exit code is an error and a reason to re-raise.
cmd = ["git", "diff", "--exit-code", "--quiet", commit, "--", str(lockfile.path)]
ret = subprocess.run(cmd, check=False)
ret = subprocess.run(cmd, check=False) # noqa: S603
if ret.returncode == 0:
print(f" [-] The lockfile `{lockfile!r}` has not changed")
lockfile.is_lockfile_changed = False
Expand Down Expand Up @@ -349,15 +349,16 @@ def ensure_project_exists(self) -> None:
if self.phylum_group:
print(f" [-] Using Phylum group: {self.phylum_group}")
cmd = [str(self.cli_path), "project", "create", "--group", self.phylum_group, self.phylum_project]
ret = subprocess.run(cmd, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
ret = subprocess.run(cmd, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # noqa: S603
# The Phylum CLI will return a unique error code of 14 when a project that already
# exists is attempted to be created. This situation is recognized and allowed to happen
# since it means the project exists as expected. Any other exit code is an error.
cli_exit_code_project_already_exists = 14
maxrake marked this conversation as resolved.
Show resolved Hide resolved
if ret.returncode == 0:
print(f" [-] Project {self.phylum_project} created successfully.")
if self._project_file_already_existed:
print(f" [!] Overwrote previous `.phylum_project` file found at: {self._phylum_project_file}")
elif ret.returncode == 14:
elif ret.returncode == cli_exit_code_project_already_exists:
print(f" [-] Project {self.phylum_project} already exists. Continuing with it ...")
else:
print(f" [!] There was a problem creating the project with command: {shlex.join(cmd)}")
Expand Down Expand Up @@ -407,11 +408,12 @@ def analyze(self) -> ReturnCode:

print(f" [*] Performing analysis with command: {shlex.join(cmd)}")
try:
analysis_result = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout
analysis_result = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout # noqa: S603
except subprocess.CalledProcessError as err:
# The Phylum project will fail analysis if the configured policy criteria are not met.
# This causes the return code to be 100 and lands us here. Check for this case to proceed.
if err.returncode == 100:
cli_exit_code_failed_policy = 100
if err.returncode == cli_exit_code_failed_policy:
analysis_result = err.stdout
else:
print(f" [!] stdout:\n{err.stdout}")
Expand All @@ -430,13 +432,12 @@ def analyze(self) -> ReturnCode:
else:
print(" [+] The analysis is complete and there were NO failures")
returncode = ReturnCode.SUCCESS
elif analysis.is_failure:
print(" [!] There were failures in one or more completed packages")
returncode = ReturnCode.FAILURE_INCOMPLETE
else:
if analysis.is_failure:
print(" [!] There were failures in one or more completed packages")
returncode = ReturnCode.FAILURE_INCOMPLETE
else:
print(" [+] There were no failures in the packages that have completed so far")
returncode = ReturnCode.INCOMPLETE
print(" [+] There were no failures in the packages that have completed so far")
returncode = ReturnCode.INCOMPLETE

return returncode

Expand Down
10 changes: 5 additions & 5 deletions src/phylum/ci/ci_bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
* https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/
* https://developer.atlassian.com/cloud/bitbucket/rest/intro/#pullrequest
"""
from argparse import Namespace
from functools import cached_property, lru_cache
import os
import re
import shlex
import subprocess
import urllib.parse
from argparse import Namespace
from functools import cached_property, lru_cache
from typing import Optional
import urllib.parse

import requests

Expand Down Expand Up @@ -61,7 +61,7 @@ def is_in_pr() -> bool:
class CIBitbucket(CIBase):
"""Provide methods for a Bitbucket Pipelines environment."""

def __init__(self, args: Namespace) -> None:
def __init__(self, args: Namespace) -> None: # noqa: D107 ; the base __init__ docstring is better here
super().__init__(args)
self.ci_platform_name = "Bitbucket Pipelines"
if is_in_pr():
Expand Down Expand Up @@ -167,7 +167,7 @@ def common_ancestor_commit(self) -> Optional[str]:
cmd = ["git", "merge-base", src_branch, tgt_branch]
print(f" [*] Finding common ancestor commit with command: {shlex.join(cmd)}")
try:
common_commit = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout.strip()
common_commit = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout.strip() # noqa: S603
except subprocess.CalledProcessError as err:
ref_url = "https://support.atlassian.com/bitbucket-cloud/docs/git-clone-behavior/"
print(f" [!] The common ancestor commit could not be found: {err}")
Expand Down
Loading