diff --git a/.github/workflows/auto_updates.yml b/.github/workflows/auto_updates.yml index 41cf1ccc..20761aaa 100644 --- a/.github/workflows/auto_updates.yml +++ b/.github/workflows/auto_updates.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: # It's only one Python version specified in a "matrix", but on purpose to stay DRY - python-version: ["3.11"] + python-version: ["3.12"] defaults: run: shell: bash diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 2c708418..f977bec2 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -25,7 +25,7 @@ jobs: strategy: matrix: # It's only one Python version specified in a "matrix", but on purpose to stay DRY - python-version: ["3.11"] + python-version: ["3.12"] defaults: run: shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 410be697..aef76984 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: strategy: matrix: # It's only one Python version specified in a "matrix", but on purpose to stay DRY - python-version: ["3.11"] + python-version: ["3.12"] defaults: run: shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbc534ca..530b216c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: # It's only one Python version specified in a "matrix", but on purpose to stay DRY - python-version: ["3.11"] + python-version: ["3.12"] steps: - name: Checkout the repo uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 @@ -64,7 +64,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.12"] steps: - name: Checkout the repo uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 @@ -101,7 +101,7 @@ jobs: fail-fast: false matrix: # It's only one Python version specified in a "matrix", but on purpose to stay DRY - python-version: ["3.11"] + python-version: ["3.12"] dockerfile: ["Dockerfile", "Dockerfile.slim"] build: ["wheel", "source"] env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13373d8f..469e5b3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,9 +102,9 @@ Here's how to set up `phylum-ci` for local development. ``` 3. Ensure all supported Python versions are installed locally - 1. The strategy is to support all released minor versions of Python that are not end-of-life yet + 1. The strategy is to support the current/latest release plus the previous three minor versions of Python 2. The current list - 1. at the time of this writing is 3.8, 3.9, 3.10, and 3.11 + 1. at the time of this writing is 3.9, 3.10, 3.11, and 3.12 2. can be inferred with the Python Developer's Guide, which maintains the [status of active Python releases](https://devguide.python.org/versions/) 3. It is recommended to use [`pyenv`](https://github.com/pyenv/pyenv) to manage multiple Python installations @@ -116,14 +116,14 @@ Here's how to set up `phylum-ci` for local development. # NOTE: These versions are examples; the latest patch version available from # pyenv should be used in place of `.x`. - pyenv install 3.8.x pyenv install 3.9.x pyenv install 3.10.x pyenv install 3.11.x + pyenv install 3.12.x pyenv rehash # Ensure all environments are available globally (helps tox to find them) - pyenv global 3.11.x 3.10.x 3.9.x 3.8.x + pyenv global 3.12.x 3.11.x 3.10.x 3.9.x ``` 4. Ensure [poetry v1.6+](https://python-poetry.org/docs/) is installed @@ -216,7 +216,7 @@ Before you submit a pull request, check that it meets these guidelines: * Have you created sufficient tests? * Have you updated all affected documentation? -The pull request should work for Python 3.8, 3.9, 3.10, and 3.11. +The pull request should work for Python 3.9, 3.10, 3.11, and 3.12. Check and make sure that the tests pass for all supported Python versions. diff --git a/Dockerfile b/Dockerfile index f045d3c1..587429fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,7 +69,7 @@ # $ scripts/docker_tests.sh --image phylum-ci ########################################################################################## -FROM python:3.11-slim-bookworm AS builder +FROM python:3.12-slim-bookworm AS builder # PKG_SRC is the path to a built distribution/wheel and PKG_NAME is the name of the built # distribution/wheel. Both can optionally be specified in glob form. When not defined, @@ -121,7 +121,7 @@ RUN find ${PHYLUM_VENV} -type f -name '*.pyc' -delete # in the final layer and also known to be part of the $PATH COPY entrypoint.sh ${PHYLUM_VENV}/bin/ -FROM python:3.11-slim-bookworm +FROM python:3.12-slim-bookworm # CLI_VER specifies the Phylum CLI version to install in the image. # Values should be provided in a format acceptable to the `phylum-init` script. diff --git a/Dockerfile.slim b/Dockerfile.slim index cf9867d0..6adc4038 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -77,7 +77,7 @@ # $ scripts/docker_tests.sh --image phylum-ci --slim ########################################################################################## -FROM python:3.11-slim-bookworm AS builder +FROM python:3.12-slim-bookworm AS builder # PKG_SRC is the path to a built distribution/wheel and PKG_NAME is the name of the built # distribution/wheel. Both can optionally be specified in glob form. When not defined, @@ -129,7 +129,7 @@ RUN find ${PHYLUM_VENV} -type f -name '*.pyc' -delete # in the final layer and also known to be part of the $PATH COPY entrypoint.sh ${PHYLUM_VENV}/bin/ -FROM python:3.11-slim-bookworm +FROM python:3.12-slim-bookworm # CLI_VER specifies the Phylum CLI version to install in the image. # Values should be provided in a format acceptable to the `phylum-init` script. diff --git a/README.md b/README.md index bf69fc85..5f152bd6 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ pipx run --spec phylum phylum-init pipx run --spec phylum phylum-ci ``` -These installation methods require Python 3.8+ to run. +These installation methods require Python 3.9+ to run. For a self contained environment, consider using the Docker image as described below. ### Usage diff --git a/poetry.lock b/poetry.lock index 6c57b57d..258bca27 100644 --- a/poetry.lock +++ b/poetry.lock @@ -476,20 +476,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.37" +version = "3.1.38" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.37-py3-none-any.whl", hash = "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33"}, - {file = "GitPython-3.1.37.tar.gz", hash = "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54"}, + {file = "GitPython-3.1.38-py3-none-any.whl", hash = "sha256:9e98b672ffcb081c2c8d5aa630d4251544fb040fb158863054242f24a2a2ba30"}, + {file = "GitPython-3.1.38.tar.gz", hash = "sha256:4d683e8957c8998b58ddb937e3e6cd167215a180e1ffd4da769ab81c620a89fe"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar"] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] [[package]] name = "identify" @@ -535,24 +535,6 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] -[[package]] -name = "importlib-resources" -version = "6.1.0" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.1.0-py3-none-any.whl", hash = "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83"}, - {file = "importlib_resources-6.1.0.tar.gz", hash = "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"] - [[package]] name = "iniconfig" version = "2.0.0" @@ -621,9 +603,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} jsonschema-specifications = ">=2023.03.6" -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -643,7 +623,6 @@ files = [ ] [package.dependencies] -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.28.0" [[package]] @@ -659,7 +638,6 @@ files = [ [package.dependencies] importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} -importlib-resources = {version = "*", markers = "python_version < \"3.9\""} "jaraco.classes" = "*" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} @@ -865,17 +843,6 @@ files = [ [package.extras] testing = ["pytest", "pytest-cov"] -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -optional = false -python-versions = ">=3.6" -files = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] - [[package]] name = "platformdirs" version = "3.11.0" @@ -1341,7 +1308,6 @@ files = [ [package.dependencies] commonmark = ">=0.9.0,<0.10.0" pygments = ">=2.6.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] @@ -1658,7 +1624,6 @@ files = [ [package.dependencies] rich = ">=12.3.0,<13.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [[package]] name = "tomli" @@ -1796,13 +1761,13 @@ files = [ [[package]] name = "urllib3" -version = "2.0.6" +version = "2.0.7" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"}, - {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, ] [package.extras] @@ -1862,5 +1827,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" -python-versions = ">=3.8,<3.12" -content-hash = "ded2f4db479fb0b523d70e725dd17e4159627b8c23f2fa626f1cce0eecfa32e4" +python-versions = ">=3.9,<3.13" +content-hash = "3ad7bd0de7d0928e6397921fcf43b1d5dfe35d3e03db1b8849f9c6229fb3b716" diff --git a/pyproject.toml b/pyproject.toml index d5182ed7..61d6b47b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,10 +27,10 @@ classifiers = [ "Topic :: Software Development", "Topic :: Software Development :: Quality Assurance", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] packages = [ { include = "phylum", from = "src" }, @@ -46,7 +46,7 @@ phylum-init = "phylum.init.cli:main" phylum-ci = "phylum.ci.cli:script_main" [tool.poetry.dependencies] -python = ">=3.8,<3.12" +python = ">=3.9,<3.13" requests = "*" cryptography = "*" packaging = "*" @@ -90,13 +90,13 @@ paths = ["src", "tests"] exclude = ["conftest.py"] [tool.refurb] -python_version = "3.8" +python_version = "3.9" format = "github" [tool.ruff] # Reference: https://beta.ruff.rs/docs/settings line-length = 120 -target-version = "py38" +target-version = "py39" force-exclude = true src = ["src", "tests"] select = [ @@ -119,7 +119,7 @@ ignore = [ "B019", # cached-instance-method # Assigning to a variable before a return statement is more readable and useful for debugging "RET504", # unnecessary-assign - # Allowing exception handling within loops improves readability with ony a negligible performance impact. + # Allowing exception handling within loops improves readability with only a negligible performance impact. "PERF203", # try-except-in-loop # These ignores will be removed during https://github.com/phylum-dev/phylum-ci/issues/238 "ANN", # flake8-annotations diff --git a/src/phylum/ci/ci_azure.py b/src/phylum/ci/ci_azure.py index 2733fc8c..c5bdffb8 100644 --- a/src/phylum/ci/ci_azure.py +++ b/src/phylum/ci/ci_azure.py @@ -23,7 +23,7 @@ import shlex import subprocess import textwrap -from typing import Dict, List, Optional, Tuple +from typing import Optional import urllib.parse import requests @@ -152,11 +152,7 @@ def phylum_label(self) -> str: pr_number = os.getenv("SYSTEM_PULLREQUEST_PULLREQUESTNUMBER") if pr_number is None: pr_number = os.getenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "unknown-number") - pr_src_branch = os.getenv("SYSTEM_PULLREQUEST_SOURCEBRANCH", "unknown-ref") - ref_prefix = "refs/heads/" - # Starting with Python 3.9, the str.removeprefix() method was introduced to do this same thing - if pr_src_branch.startswith(ref_prefix): - pr_src_branch = pr_src_branch.replace(ref_prefix, "", 1) + pr_src_branch = os.getenv("SYSTEM_PULLREQUEST_SOURCEBRANCH", "unknown-ref").removeprefix("refs/heads/") label = f"{self.ci_platform_name}_PR#{pr_number}_{pr_src_branch}" else: current_branch = os.getenv("BUILD_SOURCEBRANCHNAME", "unknown-branch") @@ -279,7 +275,7 @@ def post_output(self) -> None: post_github_comment(comments_url, self.github_token, self.analysis_report) -def get_pr_branches() -> Tuple[str, str]: +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. @@ -325,7 +321,7 @@ def get_most_recent_phylum_comment_azure(azure_token: str) -> Optional[str]: if resp.status_code != requests.codes.OK: msg = f"Are the permissions on the Azure token `AZURE_TOKEN` correct? {AZURE_PAT_ERR_MSG}" raise SystemExit(msg) - pr_threads_resp: Dict = resp.json() + pr_threads_resp: dict = resp.json() pr_threads_count = pr_threads_resp.get("count", 0) LOG.debug("PR threads found: %s", pr_threads_count) @@ -334,14 +330,14 @@ def get_most_recent_phylum_comment_azure(azure_token: str) -> Optional[str]: return None LOG.info("Checking pull request threads for existing Phylum-generated comments ...") - pr_threads: List = pr_threads_resp.get("value", []) + pr_threads: list = pr_threads_resp.get("value", []) # NOTE: The API call returns the comments in ascending order by ID...thus the need to reverse the list. # Detecting Phylum comments is done simply by looking for those that start with a known string value. # We only care about the most recent Phylum comment. - pr_thread: Dict + pr_thread: dict for pr_thread in reversed(pr_threads): thread_comments = pr_thread.get("comments", []) - thread_comment: Dict + thread_comment: dict for thread_comment in thread_comments: # All Phylum generated comments will be the first in their own thread if thread_comment.get("id", 0) != 1: @@ -396,7 +392,7 @@ def post_azure_comment(azure_token: str, comment: str) -> None: resp.raise_for_status() -def get_headers(azure_token: str) -> Dict[str, str]: +def get_headers(azure_token: str) -> dict[str, str]: """Provide the headers to use when making Azure Pipelines API calls.""" # To provide the personal access token through an HTTP header, you must first convert it to a base64 string. # NOTE: The colon (`:`) appended to the front of the PAT is intentional as it is expected by the endpoints. @@ -408,7 +404,7 @@ def get_headers(azure_token: str) -> Dict[str, str]: return headers -def get_query_params() -> Tuple[Dict, str]: +def get_query_params() -> tuple[dict, str]: """Provide the query parameters to use when making Azure Pipelines API calls.""" # This is the latest available API version a/o SEP 2023. While it is a "preview" version, it was chosen to # lean forward in an effort to maintain relevance and recency for a longer period of time going forward. diff --git a/src/phylum/ci/ci_base.py b/src/phylum/ci/ci_base.py index 3bbc7b32..a1d2e9e4 100644 --- a/src/phylum/ci/ci_base.py +++ b/src/phylum/ci/ci_base.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from argparse import Namespace from collections import OrderedDict -from functools import cached_property +from functools import cached_property, lru_cache import json import os from pathlib import Path @@ -16,7 +16,7 @@ import subprocess import tempfile import textwrap -from typing import Dict, List, Optional +from typing import Optional from packaging.version import Version from rich.markdown import Markdown @@ -66,6 +66,7 @@ def __init__(self, args: Namespace) -> None: self._ensure_project_exists() + @lru_cache(maxsize=1) def _backup_project_file(self) -> None: """Create a copy of the original `.phylum_project` file values, when the file exists. @@ -78,7 +79,7 @@ def _backup_project_file(self) -> None: except subprocess.CalledProcessError as err: msg = "Phylum status check failed" raise PhylumCalledProcessError(err, msg) from err - self._project_settings: Dict = json.loads(status_output) + self._project_settings: dict = json.loads(status_output) project_root = self._project_settings.get("root") if project_root: self._phylum_project_file = Path(project_root).joinpath(".phylum_project").resolve() @@ -112,7 +113,7 @@ def lockfiles(self) -> Lockfiles: When no valid lockfiles are provided otherwise, an attempt will be made to automatically detect them. """ - arg_lockfiles: Optional[List[List[Path]]] = self.args.lockfile + arg_lockfiles: Optional[list[list[Path]]] = self.args.lockfile if arg_lockfiles: # flatten the list of lists provided_arg_lockfiles = [LockfileEntry(path) for sub_list in arg_lockfiles for path in sub_list] @@ -123,7 +124,7 @@ def lockfiles(self) -> Lockfiles: return valid_lockfiles LOG.info("No valid dependency files were provided as arguments. An attempt will be made to detect them.") - lockfile_entries: List[OrderedDict] = self._project_settings.get("lockfiles", []) + lockfile_entries: list[OrderedDict] = self._project_settings.get("lockfiles", []) detected_lockfiles = [LockfileEntry(lfe.get("path", ""), lfe.get("type", "auto")) for lfe in lockfile_entries] if lockfile_entries and self._project_settings.get("root"): LOG.debug("Dependency files provided in `.phylum_project` file: %s", detected_lockfiles) @@ -515,4 +516,4 @@ def parse_analysis_result(self, analysis_result: str) -> None: # Type alias -CIEnvs = List[CIBase] +CIEnvs = list[CIBase] diff --git a/src/phylum/ci/ci_bitbucket.py b/src/phylum/ci/ci_bitbucket.py index d0f43181..d6f40c7f 100644 --- a/src/phylum/ci/ci_bitbucket.py +++ b/src/phylum/ci/ci_bitbucket.py @@ -23,7 +23,7 @@ import shlex import subprocess import textwrap -from typing import Dict, List, Optional +from typing import Optional import urllib.parse import requests @@ -286,8 +286,8 @@ def most_recent_phylum_comment(self) -> Optional[str]: LOG.debug("The repository UUID %s maps to workspace and repository name: %s", repo_uuid, repo_full_name) req = requests.get(url, params=query_params, headers=self.headers, timeout=REQ_TIMEOUT) req.raise_for_status() - pr_comments_resp: Dict = req.json() - pr_comments_values: List = pr_comments_resp.get("values", []) + pr_comments_resp: dict = req.json() + pr_comments_values: list = pr_comments_resp.get("values", []) if pr_comments_values: # The most recently posted Phylum pull request comment was found. # NOTE: The API call normally returns all the comments in chronological order. Query parameters are used to diff --git a/src/phylum/ci/ci_github.py b/src/phylum/ci/ci_github.py index cdb752ac..50124c7e 100644 --- a/src/phylum/ci/ci_github.py +++ b/src/phylum/ci/ci_github.py @@ -18,7 +18,7 @@ import re import subprocess import textwrap -from typing import Dict, List, Optional +from typing import Optional import requests @@ -86,7 +86,7 @@ def _check_prerequisites(self) -> None: # Unfortunately, there's not always a simple default environment variable that contains the desired information. # Instead, the full event webhook payload can be used to obtain the information. Reference: - # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request + # https://docs.github.com/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request if os.getenv("GITHUB_EVENT_NAME") != "pull_request": msg = "The workflow event must be `pull_request`" raise SystemExit(msg) @@ -187,7 +187,7 @@ def get_most_recent_phylum_comment_github(comments_url: str, github_token: str) """ query_params = {"per_page": 100} LOG.info("Getting all current pull request comments with GET URL: %s ...", comments_url) - pr_comments: List = github_request(comments_url, params=query_params, github_token=github_token) + pr_comments: list = github_request(comments_url, params=query_params, github_token=github_token) if not pr_comments: LOG.debug("No existing pull request comments found.") @@ -196,7 +196,7 @@ def get_most_recent_phylum_comment_github(comments_url: str, github_token: str) # NOTE: The API call returns the comments in ascending order by ID...thus the need to reverse the list. # Detecting Phylum comments is done simply by looking for those that start with a known string value. # We only care about the most recent Phylum comment. - pr_comment: Dict + pr_comment: dict for pr_comment in reversed(pr_comments): comment_body: str = pr_comment.get("body", "") if comment_body.lstrip().startswith(PHYLUM_HEADER.strip()): diff --git a/src/phylum/ci/ci_gitlab.py b/src/phylum/ci/ci_gitlab.py index 990f79d4..a5276b6b 100644 --- a/src/phylum/ci/ci_gitlab.py +++ b/src/phylum/ci/ci_gitlab.py @@ -15,7 +15,7 @@ import shlex import subprocess import textwrap -from typing import Dict, List, Optional +from typing import Optional import requests @@ -227,7 +227,7 @@ def most_recent_phylum_note(self) -> Optional[str]: LOG.info("Getting all current merge request notes with GET URL: %s ...", url) req = requests.get(url, headers=self.headers, timeout=REQ_TIMEOUT) req.raise_for_status() - mr_notes: List = req.json() + mr_notes: list = req.json() if not mr_notes: LOG.debug("No existing merge request notes found.") @@ -236,7 +236,7 @@ def most_recent_phylum_note(self) -> Optional[str]: # NOTE: The API defaults to returning the notes in descending order by the `created_at` field. # Detecting Phylum notes is done simply by looking for notes that start with a known string value. # We only care about the most recent Phylum note. - mr_note: Dict + mr_note: dict for mr_note in mr_notes: note_body: str = mr_note.get("body", "") if note_body.lstrip().startswith(PHYLUM_HEADER.strip()): diff --git a/src/phylum/ci/ci_precommit.py b/src/phylum/ci/ci_precommit.py index 35b73cbc..b871bcb5 100644 --- a/src/phylum/ci/ci_precommit.py +++ b/src/phylum/ci/ci_precommit.py @@ -14,7 +14,7 @@ import re import subprocess import sys -from typing import List, Optional +from typing import Optional from phylum.ci.ci_base import CIBase from phylum.ci.git import git_curent_branch_name @@ -25,7 +25,7 @@ class CIPreCommit(CIBase): """Provide methods for operating within a pre-commit hook.""" - def __init__(self, args: argparse.Namespace, remainder: List[str]) -> None: # noqa: D107 + def __init__(self, args: argparse.Namespace, remainder: list[str]) -> None: # noqa: D107 # The base __init__ docstring is better here self.extra_args = remainder super().__init__(args) @@ -38,6 +38,7 @@ def _check_prerequisites(self) -> None: * The extra unparsed arguments passed to the CLI represent the staged files, no more and no less """ super()._check_prerequisites() + self._backup_project_file() cmd = ["git", "diff", "--cached", "--name-only"] try: diff --git a/src/phylum/ci/cli.py b/src/phylum/ci/cli.py index 3679eb3a..9e1822ec 100644 --- a/src/phylum/ci/cli.py +++ b/src/phylum/ci/cli.py @@ -1,10 +1,11 @@ """Console script for phylum-ci.""" import argparse +from collections.abc import Sequence import os import pathlib import sys import textwrap -from typing import List, Optional, Sequence, Tuple +from typing import Optional from phylum import __version__ from phylum.ci import SCRIPT_NAME @@ -21,7 +22,7 @@ from phylum.logger import LOG, set_logger_level -def detect_ci_platform(args: argparse.Namespace, remainder: List[str]) -> CIBase: +def detect_ci_platform(args: argparse.Namespace, remainder: list[str]) -> CIBase: """Detect CI platform via known CI-based environment variables. Reference: https://github.com/watson/ci-info/blob/master/vendors.json @@ -70,7 +71,7 @@ def detect_ci_platform(args: argparse.Namespace, remainder: List[str]) -> CIBase return ci_env -def get_args(args: Optional[Sequence[str]] = None) -> Tuple[argparse.Namespace, List[str]]: +def get_args(args: Optional[Sequence[str]] = None) -> tuple[argparse.Namespace, list[str]]: """Get the arguments from the command line and return them.""" parser = argparse.ArgumentParser( prog=SCRIPT_NAME, diff --git a/src/phylum/ci/common.py b/src/phylum/ci/common.py index 350d0d41..901f85e6 100644 --- a/src/phylum/ci/common.py +++ b/src/phylum/ci/common.py @@ -4,7 +4,7 @@ import json import os from pathlib import Path -from typing import List, Optional +from typing import Optional @dataclasses.dataclass(order=True, frozen=True) @@ -18,7 +18,7 @@ class PackageDescriptor: # Type alias -Packages = List[PackageDescriptor] +Packages = list[PackageDescriptor] @dataclasses.dataclass() @@ -56,7 +56,7 @@ def __repr__(self) -> str: # Type alias -LockfileEntries = List[LockfileEntry] +LockfileEntries = list[LockfileEntry] class ReturnCode(IntEnum): diff --git a/src/phylum/ci/git.py b/src/phylum/ci/git.py index 60a85bd7..1d8a46b5 100644 --- a/src/phylum/ci/git.py +++ b/src/phylum/ci/git.py @@ -4,13 +4,13 @@ import shlex import subprocess import textwrap -from typing import List, Optional +from typing import Optional from phylum.exceptions import PhylumCalledProcessError, pprint_subprocess_error from phylum.logger import LOG -def git_base_cmd(git_c_path: Optional[Path] = None) -> List[str]: +def git_base_cmd(git_c_path: Optional[Path] = None) -> list[str]: """Provide a normalized base command list for use in constructing git commands. The optional `git_c_path` is used to tell `git` to run as if it were started in that @@ -133,14 +133,8 @@ def git_default_branch_name(remote: str, git_c_path: Optional[Path] = None) -> s except subprocess.CalledProcessError as inner_err: msg = "Failed to get the remote HEAD ref even after setting it." raise PhylumCalledProcessError(inner_err, msg) from outer_err - - default_branch_name = default_branch_name.strip() - # Starting with Python 3.9, the str.removeprefix() method was introduced to do this same thing - if default_branch_name.startswith(prefix): - default_branch_name = default_branch_name.replace(prefix, "", 1) - + default_branch_name = default_branch_name.strip().removeprefix(prefix) LOG.debug("Default branch name: %s", default_branch_name) - return default_branch_name diff --git a/src/phylum/ci/lockfile.py b/src/phylum/ci/lockfile.py index 8b05f61b..ffb2ef17 100644 --- a/src/phylum/ci/lockfile.py +++ b/src/phylum/ci/lockfile.py @@ -17,7 +17,7 @@ import subprocess import tempfile import textwrap -from typing import List, Optional, TypeVar +from typing import Optional, TypeVar from phylum.ci.common import LockfileEntry, PackageDescriptor, Packages from phylum.exceptions import PhylumCalledProcessError, pprint_subprocess_error @@ -219,4 +219,4 @@ def get_previous_lockfile_packages(self, prev_lockfile_object: str) -> Packages: # Type alias -Lockfiles = List[Lockfile] +Lockfiles = list[Lockfile] diff --git a/src/phylum/console.py b/src/phylum/console.py index 4a8213af..de65625c 100644 --- a/src/phylum/console.py +++ b/src/phylum/console.py @@ -3,7 +3,8 @@ The `rich` library is used here for excellent control over the console. Reference: https://rich.readthedocs.io/en/latest/index.html """ -from typing import Mapping, Union +from collections.abc import Mapping +from typing import Union from rich.console import Console from rich.style import Style diff --git a/src/phylum/github.py b/src/phylum/github.py index baefcf51..cf8fab04 100644 --- a/src/phylum/github.py +++ b/src/phylum/github.py @@ -2,7 +2,7 @@ import os import textwrap import time -from typing import Any, Dict, Optional +from typing import Any, Optional import requests @@ -24,7 +24,7 @@ PAT_REF = "https://docs.github.com/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token" -def get_headers(github_token: Optional[str] = None) -> Dict[str, str]: +def get_headers(github_token: Optional[str] = None) -> dict[str, str]: """Get the headers to use for a GitHub API request. Authenticated requests are made by providing a GitHub token. The token can be passed by parameter diff --git a/src/phylum/init/cli.py b/src/phylum/init/cli.py index 56d24cc0..e12bf380 100644 --- a/src/phylum/init/cli.py +++ b/src/phylum/init/cli.py @@ -1,5 +1,6 @@ """Console script for phylum-init.""" import argparse +from collections.abc import Sequence from functools import lru_cache import itertools import os @@ -10,7 +11,7 @@ import subprocess import sys import tempfile -from typing import Dict, List, Optional, Sequence, Tuple +from typing import Optional import zipfile from packaging.utils import canonicalize_version @@ -54,7 +55,6 @@ def get_phylum_settings_path(): def get_expected_phylum_bin_path(): """Get the expected path to the Phylum CLI binary and return it.""" phylum_bin_path = pathlib.Path.home() / ".local" / "bin" / "phylum" - return phylum_bin_path @@ -66,16 +66,11 @@ def get_phylum_cli_version(cli_path: Path) -> str: except subprocess.CalledProcessError as err: msg = "There was an error retrieving the Phylum CLI version" raise PhylumCalledProcessError(err, msg) from err - - # Starting with Python 3.9, the str.removeprefix() method was introduced to do this same thing - prefix = "phylum " - if version.startswith(prefix): - version = version.replace(prefix, "", 1) - + version = version.removeprefix("phylum ") return version -def get_phylum_bin_path() -> Tuple[Optional[Path], Optional[str]]: +def get_phylum_bin_path() -> tuple[Optional[Path], Optional[str]]: """Get the current path and corresponding version to the Phylum CLI binary and return them.""" # Look for `phylum` on the PATH first which_cli_path = shutil.which("phylum") @@ -112,7 +107,7 @@ def get_latest_version() -> str: """Get the "latest" version programmatically and return it.""" # API Reference: https://docs.github.com/en/rest/releases/releases#get-the-latest-release github_api_url = "https://api.github.com/repos/phylum-dev/cli/releases/latest" - req_json: Dict = github_request(github_api_url) + req_json: dict = github_request(github_api_url) # The "name" entry stores the GitHub Release name, which could be set to something other than the version. # Using the "tag_name" entry is better since the tags are much more tightly coupled with the release version. @@ -125,17 +120,17 @@ def get_latest_version() -> str: @lru_cache(maxsize=1) -def supported_releases() -> List[str]: +def supported_releases() -> list[str]: """Get the most recent supported releases programmatically and return them in sorted order, latest first.""" # API Reference: https://docs.github.com/en/rest/releases/releases#list-releases github_api_url = "https://api.github.com/repos/phylum-dev/cli/releases" query_params = {"per_page": 100} LOG.debug("Minimum supported Phylum CLI version required for install: %s", MIN_CLI_VER_FOR_INSTALL) - req_json: List = github_request(github_api_url, params=query_params) + req_json: list = github_request(github_api_url, params=query_params) cli_releases = {} - rel: Dict + rel: dict for rel in req_json: # The "name" entry stores the GitHub Release name, which could be set to something other than the version. # Using the "tag_name" entry is better since the tags are much more tightly coupled with the release version. @@ -164,7 +159,7 @@ def is_supported_version(version: str) -> bool: @lru_cache(maxsize=1) -def supported_targets(release_tag: str) -> List[str]: +def supported_targets(release_tag: str) -> list[str]: """Get the supported Rust target triples programmatically for a given release tag and return them. Targets are identified by their "target triple" which is the string to inform the compiler what kind of output @@ -183,15 +178,16 @@ def supported_targets(release_tag: str) -> List[str]: # API Reference: https://docs.github.com/en/rest/releases/releases#get-a-release-by-tag-name github_api_url = f"https://api.github.com/repos/phylum-dev/cli/releases/tags/{release_tag}" - req_json: Dict = github_request(github_api_url) + req_json: dict = github_request(github_api_url) assets = req_json.get("assets", []) - targets: List[str] = [] + targets: list[str] = [] prefix, suffix = "phylum-", ".zip" + asset: dict for asset in assets: - name = asset.get("name", "") + name: str = asset.get("name", "") if name.startswith(prefix) and name.endswith(suffix): - target = name.replace(prefix, "").replace(suffix, "") + target = name.removeprefix(prefix).removesuffix(suffix) targets.append(target) return list(set(targets)) @@ -309,7 +305,7 @@ def setup_token(token: str) -> None: phylum_settings_path = get_phylum_settings_path() ensure_settings_file() yaml = YAML() - settings: Dict = yaml.load(phylum_settings_path.read_text(encoding="utf-8")) + settings: dict = yaml.load(phylum_settings_path.read_text(encoding="utf-8")) settings.setdefault("auth_info", {}) settings["auth_info"]["offline_access"] = token with phylum_settings_path.open("w", encoding="utf-8") as f: @@ -350,7 +346,7 @@ def process_uri_option(args: argparse.Namespace) -> None: settings_file_existed = phylum_settings_path.exists() ensure_settings_file() yaml = YAML() - settings: Dict = yaml.load(phylum_settings_path.read_text(encoding="utf-8")) + settings: dict = yaml.load(phylum_settings_path.read_text(encoding="utf-8")) configured_uri = settings.get("connection", {}).get("uri") if api_uri: diff --git a/src/phylum/logger.py b/src/phylum/logger.py index 16ed8620..59520b76 100644 --- a/src/phylum/logger.py +++ b/src/phylum/logger.py @@ -3,7 +3,7 @@ import logging import sys from types import FunctionType, MethodType -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Optional from rich.logging import RichHandler from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn @@ -188,8 +188,8 @@ def add_trace_logging() -> None: pkg_namespace = f"{PKG_NAME}." phylum_modules = (module for module in sys.modules if module.startswith(pkg_namespace)) - phylum_funcs: Dict[str, FunctionType] = {} - phylum_classes: Dict[str, type] = {} + phylum_funcs: dict[str, FunctionType] = {} + phylum_classes: dict[str, type] = {} for module in phylum_modules: module_functions = inspect.getmembers(sys.modules[module], inspect.isfunction) if module_functions: diff --git a/tests/conftest.py b/tests/conftest.py index bbd44a65..4bc25040 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ """Aggregate the pytest fixtures in one location.""" -from typing import Generator +from collections.abc import Generator import pytest diff --git a/tests/unit/test_package_metadata.py b/tests/unit/test_package_metadata.py index 30328221..987760e4 100644 --- a/tests/unit/test_package_metadata.py +++ b/tests/unit/test_package_metadata.py @@ -18,7 +18,7 @@ def test_project_version(): def test_python_version(): """Ensure the python version used to test is a supported version.""" - supported_minor_versions = (8, 9, 10, 11) + supported_minor_versions = (9, 10, 11, 12) python_version = sys.version_info acceptable_python_major_version = 3 assert python_version.major == acceptable_python_major_version, "Only Python 3 is supported" diff --git a/tox.ini b/tox.ini index 08bfb512..3852d47f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,15 @@ [tox] # Sub-commands were introduced in v4. These are used in workflows and docs. min_version = 4 -envlist = py38, py39, py310, py311 +envlist = py39, py310, py311, py312 isolated_build = true [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 [testenv] description = Test environment for minor Python version