diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3608b8..0a5047c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,21 +10,14 @@ on: jobs: - flake8: - name: Flake8 + ruff: + name: Ruff runs-on: ubuntu-latest steps: - uses: actions/checkout@v7.0.1 - - name: Set Up Python - uses: actions/setup-python@v7.0.0 + - uses: astral-sh/ruff-action@v4.1.0 with: - python-version: '3.10' - - name: Install dependencies - run: | - python -m pip install -U pip - python -m pip install flake8 - - name: Check - run: python -m flake8 + args: check --ignore=UP031 --target-version=py38 mypy: name: MyPy @@ -49,7 +42,7 @@ jobs: test: name: Test needs: - - flake8 + - ruff - mypy runs-on: ubuntu-latest strategy: diff --git a/httpx_gssapi/__init__.py b/httpx_gssapi/__init__.py index 3c53224..b05268c 100644 --- a/httpx_gssapi/__init__.py +++ b/httpx_gssapi/__init__.py @@ -14,21 +14,21 @@ Both the sync and async HTTPX APIs should be fully supported. """ __all__ = ( + 'DISABLED', + 'OPTIONAL', + 'REQUIRED', + 'SPNEGO', 'HTTPSPNEGOAuth', 'MutualAuthenticationError', - 'SPNEGO', - 'REQUIRED', - 'OPTIONAL', - 'DISABLED', ) -import os import logging +import os -from .gssapi_ import HTTPSPNEGOAuth, SPNEGO, REQUIRED, OPTIONAL, DISABLED +from ._version import get_versions from .exceptions import MutualAuthenticationError +from .gssapi_ import DISABLED, OPTIONAL, REQUIRED, SPNEGO, HTTPSPNEGOAuth -from ._version import get_versions __version__ = get_versions()['version'] del get_versions diff --git a/httpx_gssapi/_version.py b/httpx_gssapi/_version.py index 72e8816..b62207e 100644 --- a/httpx_gssapi/_version.py +++ b/httpx_gssapi/_version.py @@ -10,17 +10,18 @@ # https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" +from __future__ import annotations import errno +import functools import os import re import subprocess import sys -from typing import Any, Callable, Dict, List, Optional, Tuple -import functools +from typing import Any, Callable -def get_keywords() -> Dict[str, str]: +def get_keywords() -> dict[str, str]: """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must @@ -62,8 +63,8 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY: Dict[str, str] = {} -HANDLERS: Dict[str, Dict[str, Callable]] = {} +LONG_VERSION_PY: dict[str, str] = {} +HANDLERS: dict[str, dict[str, Callable]] = {} def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator @@ -78,18 +79,18 @@ def decorate(f: Callable) -> Callable: def run_command( - commands: List[str], - args: List[str], - cwd: Optional[str] = None, + commands: list[str], + args: list[str], + cwd: str | None = None, verbose: bool = False, hide_stderr: bool = False, - env: Optional[Dict[str, str]] = None, -) -> Tuple[Optional[str], Optional[int]]: + env: dict[str, str] | None = None, +) -> tuple[str | None, int | None]: """Call the given command(s).""" assert isinstance(commands, list) process = None - popen_kwargs: Dict[str, Any] = {} + popen_kwargs: dict[str, Any] = {} if sys.platform == "win32": # This hides the console window if pythonw.exe is used startupinfo = subprocess.STARTUPINFO() @@ -109,18 +110,18 @@ def run_command( if e.errno == errno.ENOENT: continue if verbose: - print("unable to run %s" % dispcmd) + print(f"unable to run {dispcmd}") print(e) return None, None else: if verbose: - print("unable to find command, tried %s" % (commands,)) + print(f"unable to find command, tried {commands}") return None, None stdout = process.communicate()[0].strip().decode() if process.returncode != 0: if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) + print(f"unable to run {dispcmd} (error)") + print(f"stdout was {stdout}") return None, process.returncode return stdout, process.returncode @@ -129,7 +130,7 @@ def versions_from_parentdir( parentdir_prefix: str, root: str, verbose: bool, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -148,19 +149,18 @@ def versions_from_parentdir( root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print(f"Tried directories {rootdirs!s} but none started with prefix {parentdir_prefix}") raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: +def git_get_keywords(versionfile_abs: str) -> dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords: Dict[str, str] = {} + keywords: dict[str, str] = {} try: with open(versionfile_abs, "r") as fobj: for line in fobj: @@ -183,10 +183,10 @@ def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: @register_vcs_handler("git", "keywords") def git_versions_from_keywords( - keywords: Dict[str, str], + keywords: dict[str, str], tag_prefix: str, verbose: bool, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get version information from git keywords.""" if "refnames" not in keywords: raise NotThisMethod("Short version file found") @@ -223,9 +223,9 @@ def git_versions_from_keywords( # "stabilization", as well as "HEAD" and "master". tags = {r for r in refs if re.search(r'\d', r)} if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) + print("discarding '{}', no digits".format(",".join(refs - tags))) if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) + print("likely tags: {}".format(",".join(sorted(tags)))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): @@ -236,7 +236,7 @@ def git_versions_from_keywords( if not re.match(r'\d', r): continue if verbose: - print("picking %s" % r) + print(f"picking {r}") return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, @@ -255,7 +255,7 @@ def git_pieces_from_vcs( root: str, verbose: bool, runner: Callable = run_command -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -277,7 +277,7 @@ def git_pieces_from_vcs( hide_stderr=not verbose) if rc != 0: if verbose: - print("Directory %s not under git control" % root) + print(f"Directory {root} not under git control") raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] @@ -295,7 +295,7 @@ def git_pieces_from_vcs( raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces: Dict[str, Any] = {} + pieces: dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None @@ -350,8 +350,7 @@ def git_pieces_from_vcs( mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = (f"unable to parse git-describe output: '{describe_out}'") return pieces # tag @@ -360,8 +359,7 @@ def git_pieces_from_vcs( if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = (f"tag '{full_tag}' doesn't start with prefix '{tag_prefix}'") return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] @@ -387,14 +385,14 @@ def git_pieces_from_vcs( return pieces -def plus_or_dot(pieces: Dict[str, Any]) -> str: +def plus_or_dot(pieces: dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces: Dict[str, Any]) -> str: +def render_pep440(pieces: dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -419,7 +417,7 @@ def render_pep440(pieces: Dict[str, Any]) -> str: return rendered -def render_pep440_branch(pieces: Dict[str, Any]) -> str: +def render_pep440_branch(pieces: dict[str, Any]) -> str: """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . The ".dev0" means not master branch. Note that .dev0 sorts backwards @@ -449,7 +447,7 @@ def render_pep440_branch(pieces: Dict[str, Any]) -> str: return rendered -def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: +def pep440_split_post(ver: str) -> tuple[str, int | None]: """Split pep440 version string at the post-release segment. Returns the release segments before the post-release and the @@ -459,7 +457,7 @@ def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: return vc[0], int(vc[1] or 0) if len(vc) == 2 else None -def render_pep440_pre(pieces: Dict[str, Any]) -> str: +def render_pep440_pre(pieces: dict[str, Any]) -> str: """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: @@ -483,7 +481,7 @@ def render_pep440_pre(pieces: Dict[str, Any]) -> str: return rendered -def render_pep440_post(pieces: Dict[str, Any]) -> str: +def render_pep440_post(pieces: dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -500,17 +498,17 @@ def render_pep440_post(pieces: Dict[str, Any]) -> str: if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] + rendered += "g{}".format(pieces["short"]) else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" - rendered += "+g%s" % pieces["short"] + rendered += "+g{}".format(pieces["short"]) return rendered -def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: +def render_pep440_post_branch(pieces: dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . The ".dev0" means not master branch. @@ -525,7 +523,7 @@ def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] + rendered += "g{}".format(pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: @@ -533,13 +531,13 @@ def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: rendered = "0.post%d" % pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" - rendered += "+g%s" % pieces["short"] + rendered += "+g{}".format(pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered -def render_pep440_old(pieces: Dict[str, Any]) -> str: +def render_pep440_old(pieces: dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. @@ -561,7 +559,7 @@ def render_pep440_old(pieces: Dict[str, Any]) -> str: return rendered -def render_git_describe(pieces: Dict[str, Any]) -> str: +def render_git_describe(pieces: dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -581,7 +579,7 @@ def render_git_describe(pieces: Dict[str, Any]) -> str: return rendered -def render_git_describe_long(pieces: Dict[str, Any]) -> str: +def render_git_describe_long(pieces: dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -601,7 +599,7 @@ def render_git_describe_long(pieces: Dict[str, Any]) -> str: return rendered -def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: +def render(pieces: dict[str, Any], style: str) -> dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", @@ -630,14 +628,14 @@ def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: - raise ValueError("unknown style '%s'" % style) + raise ValueError(f"unknown style '{style}'") return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} -def get_versions() -> Dict[str, Any]: +def get_versions() -> dict[str, Any]: """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some diff --git a/httpx_gssapi/exceptions.py b/httpx_gssapi/exceptions.py index 86a6868..2b1ac88 100644 --- a/httpx_gssapi/exceptions.py +++ b/httpx_gssapi/exceptions.py @@ -5,17 +5,16 @@ This module contains the set of exceptions. """ +from __future__ import annotations -from typing import Optional - -from httpx import RequestError, Request, Response +from httpx import Request, RequestError, Response class MutualAuthenticationError(RequestError): """Mutual Authentication Error""" def __init__(self, *, - request: Optional[Request] = None, + request: Request | None = None, response: Response): self.response = response super().__init__( diff --git a/httpx_gssapi/gssapi_.py b/httpx_gssapi/gssapi_.py index 0f5a99b..dd9ef26 100644 --- a/httpx_gssapi/gssapi_.py +++ b/httpx_gssapi/gssapi_.py @@ -1,16 +1,16 @@ -import re +from __future__ import annotations + import logging -from itertools import chain +import re +from base64 import b64decode, b64encode from functools import wraps -from typing import Generator, Optional, List, Any, Union - -from base64 import b64encode, b64decode +from itertools import chain +from typing import Any, Generator import gssapi +import httpx from gssapi import SecurityContext from gssapi.exceptions import GSSError - -import httpx from httpx import Auth, Request, Response from .exceptions import MutualAuthenticationError, SPNEGOExchangeError @@ -36,10 +36,10 @@ # OID for the SPNEGO mechanism SPNEGO = gssapi.OID.from_int_seq("1.3.6.1.5.5.2") -_find_auth = re.compile(r'Negotiate\s*([^,]*)', re.I).search +_find_auth = re.compile(r'Negotiate\s*([^,]*)', re.IGNORECASE).search -def _negotiate_value(response: Response) -> Optional[bytes]: +def _negotiate_value(response: Response) -> bytes | None: """Extracts the gssapi authentication token from the appropriate header""" authreq = response.headers.get('www-authenticate', None) if authreq and (match := _find_auth(authreq)): @@ -138,11 +138,11 @@ class HTTPSPNEGOAuth(Auth): def __init__(self, mutual_authentication: int = DISABLED, - target_name: Optional[Union[str, gssapi.Name]] = "HTTP", + target_name: str | gssapi.Name | None = "HTTP", delegate: bool = False, opportunistic_auth: bool = False, - creds: Optional[gssapi.Credentials] = None, - mech: Optional[Union[bytes, gssapi.OID]] = SPNEGO, + creds: gssapi.Credentials | None = None, + mech: bytes | gssapi.OID | None = SPNEGO, sanitize_mutual_error_response: bool = True): self.mutual_authentication = mutual_authentication self.target_name = target_name @@ -164,7 +164,7 @@ def auth_flow(self, request: Request) -> FlowGen: def handle_response(self, response: Response, - ctx: Optional[SecurityContext] = None) -> FlowGen: + ctx: SecurityContext | None = None) -> FlowGen: num_401s = 0 while response.status_code == 401 and num_401s < 2: num_401s += 1 @@ -234,7 +234,7 @@ def handle_mutual_auth(self, response: Response, ctx: SecurityContext): @_handle_gsserror(gss_stage='stepping', result=_gss_to_spnego_error) def set_auth_header(self, request: Request, - response: Optional[Response] = None) -> SecurityContext: + response: Response | None = None) -> SecurityContext: """ Create a new security context, generate the GSSAPI authentication token, and insert it into the request header. The new security context @@ -299,7 +299,7 @@ def _make_context(self, request: Request) -> SecurityContext: ) @property - def _gssflags(self) -> List[gssapi.RequirementFlag]: + def _gssflags(self) -> list[gssapi.RequirementFlag]: """List of configured GSSAPI requirement flags.""" flags = [gssapi.RequirementFlag.out_of_sequence_detection] if self.delegate: diff --git a/setup.cfg b/setup.cfg index fb45282..f1debf9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -83,7 +83,7 @@ packages = tests [tox:tox] -envlist = py36, py37, py38, py39, py310 +envlist = py38, py39, py310, py311, py312, py313, py314, py315 [testenv] deps = diff --git a/setup.py b/setup.py index b347e22..c9940b9 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,8 @@ #!/usr/bin/env python from pathlib import Path + from setuptools import setup + import versioneer path = Path(__file__).parent @@ -9,7 +11,7 @@ long_desc = readme.read_text() if readme.exists() else '' if history.exists(): - long_desc = '\n\n'.join([long_desc, history.read_text()]) + long_desc = f'{long_desc}\n\n{history.read_text()}' setup( long_description=long_desc, diff --git a/tests/conftest.py b/tests/conftest.py index 984faf2..b05c7c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,23 +1,23 @@ # TODO: Provide this through k5test? +import copy +import multiprocessing as mp import os import re -import copy import threading as th -from contextlib import contextmanager from base64 import b64decode +from contextlib import contextmanager from http.server import HTTPServer, BaseHTTPRequestHandler from typing import Generator, cast -import pytest -import k5test # type: ignore[import-untyped] - import gssapi.exceptions +import k5test # type: ignore[import-untyped] +import pytest WWW_AUTHENTICATE = 'WWW-Authenticate' AUTHORIZATION = 'Authorization' NEGOTIATE = 'Negotiate' -_find_auth = re.compile(r'Negotiate\s*([^,]*)', re.I).search +_find_auth = re.compile(r'Negotiate\s*([^,]*)', re.IGNORECASE).search class KrbRequestHandler(BaseHTTPRequestHandler): @@ -102,7 +102,7 @@ def krb_realm() -> k5test.K5Realm: os.environ.update(realm.env) yield realm realm.stop() - os.environ = env + os.environ = env # noqa: B003 @pytest.fixture(scope='session') diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py old mode 100644 new mode 100755 diff --git a/tests/test_mocked.py b/tests/test_mocked.py old mode 100644 new mode 100755 index 770bfc3..23e8cc9 --- a/tests/test_mocked.py +++ b/tests/test_mocked.py @@ -5,15 +5,13 @@ from base64 import b64encode from unittest.mock import Mock, patch -import pytest - -import httpx - import gssapi import gssapi.exceptions +import httpx +import pytest import httpx_gssapi -from httpx_gssapi import REQUIRED, OPTIONAL +from httpx_gssapi import OPTIONAL, REQUIRED logging.basicConfig() @@ -75,7 +73,8 @@ def null_request(method='GET', url="http://www.example.org/", **kwargs): return httpx.Request(method, url, **kwargs) -def null_response(status=200, request=null_request(), **kwargs): +def null_response(status=200, request=None, **kwargs): + request = request or null_request() return httpx.Response(status, request=request, **kwargs) diff --git a/versioneer.py b/versioneer.py index 1e3753e..8fd7222 100644 --- a/versioneer.py +++ b/versioneer.py @@ -308,17 +308,18 @@ # pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with # pylint:disable=attribute-defined-outside-init,too-many-arguments +from __future__ import annotations + import configparser import errno +import functools import json import os import re import subprocess import sys from pathlib import Path -from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union -from typing import NoReturn -import functools +from typing import Any, Callable, NoReturn, cast have_tomllib = True if sys.version_info >= (3, 11): @@ -337,9 +338,9 @@ class VersioneerConfig: style: str tag_prefix: str versionfile_source: str - versionfile_build: Optional[str] - parentdir_prefix: Optional[str] - verbose: Optional[bool] + versionfile_build: str | None + parentdir_prefix: str | None + verbose: bool | None def get_root() -> str: @@ -384,8 +385,7 @@ def get_root() -> str: me_dir = os.path.normcase(os.path.splitext(my_path)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(my_path), versioneer_py)) + print(f"Warning: build in {os.path.dirname(my_path)} is using versioneer.py from {versioneer_py}") except NameError: pass return root @@ -400,7 +400,7 @@ def get_config_from_root(root: str) -> VersioneerConfig: root_pth = Path(root) pyproject_toml = root_pth / "pyproject.toml" setup_cfg = root_pth / "setup.cfg" - section: Union[Dict[str, Any], configparser.SectionProxy, None] = None + section: dict[str, Any] | configparser.SectionProxy | None = None if pyproject_toml.exists() and have_tomllib: try: with open(pyproject_toml, 'rb') as fobj: @@ -444,8 +444,8 @@ class NotThisMethod(Exception): # these dictionaries contain VCS-specific tools -LONG_VERSION_PY: Dict[str, str] = {} -HANDLERS: Dict[str, Dict[str, Callable]] = {} +LONG_VERSION_PY: dict[str, str] = {} +HANDLERS: dict[str, dict[str, Callable]] = {} def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator @@ -458,18 +458,18 @@ def decorate(f: Callable) -> Callable: def run_command( - commands: List[str], - args: List[str], - cwd: Optional[str] = None, + commands: list[str], + args: list[str], + cwd: str | None = None, verbose: bool = False, hide_stderr: bool = False, - env: Optional[Dict[str, str]] = None, -) -> Tuple[Optional[str], Optional[int]]: + env: dict[str, str] | None = None, +) -> tuple[str | None, int | None]: """Call the given command(s).""" assert isinstance(commands, list) process = None - popen_kwargs: Dict[str, Any] = {} + popen_kwargs: dict[str, Any] = {} if sys.platform == "win32": # This hides the console window if pythonw.exe is used startupinfo = subprocess.STARTUPINFO() @@ -489,18 +489,18 @@ def run_command( if e.errno == errno.ENOENT: continue if verbose: - print("unable to run %s" % dispcmd) + print(f"unable to run {dispcmd}") print(e) return None, None else: if verbose: - print("unable to find command, tried %s" % (commands,)) + print(f"unable to find command, tried {commands}") return None, None stdout = process.communicate()[0].strip().decode() if process.returncode != 0: if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) + print(f"unable to run {dispcmd} (error)") + print(f"stdout was {stdout}") return None, process.returncode return stdout, process.returncode @@ -1192,13 +1192,13 @@ def get_versions() -> Dict[str, Any]: @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: +def git_get_keywords(versionfile_abs: str) -> dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords: Dict[str, str] = {} + keywords: dict[str, str] = {} try: with open(versionfile_abs, "r") as fobj: for line in fobj: @@ -1221,10 +1221,10 @@ def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: @register_vcs_handler("git", "keywords") def git_versions_from_keywords( - keywords: Dict[str, str], + keywords: dict[str, str], tag_prefix: str, verbose: bool, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get version information from git keywords.""" if "refnames" not in keywords: raise NotThisMethod("Short version file found") @@ -1261,9 +1261,9 @@ def git_versions_from_keywords( # "stabilization", as well as "HEAD" and "master". tags = {r for r in refs if re.search(r'\d', r)} if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) + print("discarding '{}', no digits".format(",".join(refs - tags))) if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) + print("likely tags: {}".format(",".join(sorted(tags)))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): @@ -1274,7 +1274,7 @@ def git_versions_from_keywords( if not re.match(r'\d', r): continue if verbose: - print("picking %s" % r) + print(f"picking {r}") return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, @@ -1293,7 +1293,7 @@ def git_pieces_from_vcs( root: str, verbose: bool, runner: Callable = run_command -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -1315,7 +1315,7 @@ def git_pieces_from_vcs( hide_stderr=not verbose) if rc != 0: if verbose: - print("Directory %s not under git control" % root) + print(f"Directory {root} not under git control") raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] @@ -1333,7 +1333,7 @@ def git_pieces_from_vcs( raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces: Dict[str, Any] = {} + pieces: dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None @@ -1388,8 +1388,7 @@ def git_pieces_from_vcs( mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = (f"unable to parse git-describe output: '{describe_out}'") return pieces # tag @@ -1398,8 +1397,7 @@ def git_pieces_from_vcs( if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = (f"tag '{full_tag}' doesn't start with prefix '{tag_prefix}'") return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] @@ -1425,7 +1423,7 @@ def git_pieces_from_vcs( return pieces -def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None: +def do_vcs_install(versionfile_source: str, ipy: str | None) -> None: """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py @@ -1450,7 +1448,7 @@ def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None: try: with open(".gitattributes", "r") as fobj: for line in fobj: - if line.strip().startswith(versionfile_source): + if line.strip().startswith(versionfile_source): # noqa: SIM102 if "export-subst" in line.strip().split()[1:]: present = True break @@ -1467,7 +1465,7 @@ def versions_from_parentdir( parentdir_prefix: str, root: str, verbose: bool, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -1486,8 +1484,7 @@ def versions_from_parentdir( root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print(f"Tried directories {rootdirs!s} but none started with prefix {parentdir_prefix}") raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -1509,7 +1506,7 @@ def get_versions(): """ -def versions_from_file(filename: str) -> Dict[str, Any]: +def versions_from_file(filename: str) -> dict[str, Any]: """Try to determine the version from _version.py if present.""" try: with open(filename) as f: @@ -1517,33 +1514,33 @@ def versions_from_file(filename: str) -> Dict[str, Any]: except OSError: raise NotThisMethod("unable to read _version.py") mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) + contents, re.MULTILINE | re.DOTALL) if not mo: mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) + contents, re.MULTILINE | re.DOTALL) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) -def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None: +def write_to_version_file(filename: str, versions: dict[str, Any]) -> None: """Write the given version number to the given _version.py file.""" contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) - print("set %s to '%s'" % (filename, versions["version"])) + print("set {} to '{}'".format(filename, versions["version"])) -def plus_or_dot(pieces: Dict[str, Any]) -> str: +def plus_or_dot(pieces: dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces: Dict[str, Any]) -> str: +def render_pep440(pieces: dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -1568,7 +1565,7 @@ def render_pep440(pieces: Dict[str, Any]) -> str: return rendered -def render_pep440_branch(pieces: Dict[str, Any]) -> str: +def render_pep440_branch(pieces: dict[str, Any]) -> str: """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . The ".dev0" means not master branch. Note that .dev0 sorts backwards @@ -1598,7 +1595,7 @@ def render_pep440_branch(pieces: Dict[str, Any]) -> str: return rendered -def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: +def pep440_split_post(ver: str) -> tuple[str, int | None]: """Split pep440 version string at the post-release segment. Returns the release segments before the post-release and the @@ -1608,7 +1605,7 @@ def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: return vc[0], int(vc[1] or 0) if len(vc) == 2 else None -def render_pep440_pre(pieces: Dict[str, Any]) -> str: +def render_pep440_pre(pieces: dict[str, Any]) -> str: """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: @@ -1632,7 +1629,7 @@ def render_pep440_pre(pieces: Dict[str, Any]) -> str: return rendered -def render_pep440_post(pieces: Dict[str, Any]) -> str: +def render_pep440_post(pieces: dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -1649,17 +1646,17 @@ def render_pep440_post(pieces: Dict[str, Any]) -> str: if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] + rendered += "g{}".format(pieces["short"]) else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" - rendered += "+g%s" % pieces["short"] + rendered += "+g{}".format(pieces["short"]) return rendered -def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: +def render_pep440_post_branch(pieces: dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . The ".dev0" means not master branch. @@ -1674,7 +1671,7 @@ def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] + rendered += "g{}".format(pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: @@ -1682,13 +1679,13 @@ def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: rendered = "0.post%d" % pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" - rendered += "+g%s" % pieces["short"] + rendered += "+g{}".format(pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered -def render_pep440_old(pieces: Dict[str, Any]) -> str: +def render_pep440_old(pieces: dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. @@ -1710,7 +1707,7 @@ def render_pep440_old(pieces: Dict[str, Any]) -> str: return rendered -def render_git_describe(pieces: Dict[str, Any]) -> str: +def render_git_describe(pieces: dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -1730,7 +1727,7 @@ def render_git_describe(pieces: Dict[str, Any]) -> str: return rendered -def render_git_describe_long(pieces: Dict[str, Any]) -> str: +def render_git_describe_long(pieces: dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -1750,7 +1747,7 @@ def render_git_describe_long(pieces: Dict[str, Any]) -> str: return rendered -def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: +def render(pieces: dict[str, Any], style: str) -> dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", @@ -1779,7 +1776,7 @@ def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: - raise ValueError("unknown style '%s'" % style) + raise ValueError(f"unknown style '{style}'") return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, @@ -1790,7 +1787,7 @@ class VersioneerBadRootError(Exception): """The project root directory is unknown or missing key files.""" -def get_versions(verbose: bool = False) -> Dict[str, Any]: +def get_versions(verbose: bool = False) -> dict[str, Any]: """Get the project version from whatever source is available. Returns dict with two keys: 'version' and 'full'. @@ -1804,7 +1801,7 @@ def get_versions(verbose: bool = False) -> Dict[str, Any]: assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" handlers = HANDLERS.get(cfg.VCS) - assert handlers, "unrecognized VCS '%s'" % cfg.VCS + assert handlers, f"unrecognized VCS '{cfg.VCS}'" verbose = verbose or bool(cfg.verbose) # `bool()` used to avoid `None` assert cfg.versionfile_source is not None, \ "please set versioneer.versionfile_source" @@ -1825,7 +1822,7 @@ def get_versions(verbose: bool = False) -> Dict[str, Any]: keywords = get_keywords_f(versionfile_abs) ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) if verbose: - print("got version from expanded keyword %s" % ver) + print(f"got version from expanded keyword {ver}") return ver except NotThisMethod: pass @@ -1833,7 +1830,7 @@ def get_versions(verbose: bool = False) -> Dict[str, Any]: try: ver = versions_from_file(versionfile_abs) if verbose: - print("got version from file %s %s" % (versionfile_abs, ver)) + print(f"got version from file {versionfile_abs} {ver}") return ver except NotThisMethod: pass @@ -1844,7 +1841,7 @@ def get_versions(verbose: bool = False) -> Dict[str, Any]: pieces = from_vcs_f(cfg.tag_prefix, root, verbose) ver = render(pieces, cfg.style) if verbose: - print("got version from VCS %s" % ver) + print(f"got version from VCS {ver}") return ver except NotThisMethod: pass @@ -1853,7 +1850,7 @@ def get_versions(verbose: bool = False) -> Dict[str, Any]: if cfg.parentdir_prefix: ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) if verbose: - print("got version from parentdir %s" % ver) + print(f"got version from parentdir {ver}") return ver except NotThisMethod: pass @@ -1871,7 +1868,7 @@ def get_version() -> str: return get_versions()["version"] -def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None): +def get_cmdclass(cmdclass: dict[str, Any] | None = None): """Get the custom setuptools subclasses used by Versioneer. If the package uses a different cmdclass (e.g. one from numpy), it @@ -1899,8 +1896,8 @@ def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None): class cmd_version(Command): description = "report generated version string" - user_options: List[Tuple[str, str, str]] = [] - boolean_options: List[str] = [] + user_options: list[tuple[str, str, str]] = [] # noqa: RUF012 + boolean_options: list[str] = [] # noqa: RUF012 def initialize_options(self) -> None: pass @@ -1910,12 +1907,12 @@ def finalize_options(self) -> None: def run(self) -> None: vers = get_versions(verbose=True) - print("Version: %s" % vers["version"]) - print(" full-revisionid: %s" % vers.get("full-revisionid")) - print(" dirty: %s" % vers.get("dirty")) - print(" date: %s" % vers.get("date")) + print("Version: {}".format(vers["version"])) + print(" full-revisionid: {}".format(vers.get("full-revisionid"))) + print(" dirty: {}".format(vers.get("dirty"))) + print(" date: {}".format(vers.get("date"))) if vers["error"]: - print(" error: %s" % vers["error"]) + print(" error: {}".format(vers["error"])) cmds["version"] = cmd_version # we override "build_py" in setuptools @@ -1957,7 +1954,7 @@ def run(self) -> None: if cfg.versionfile_build: target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) - print("UPDATING %s" % target_versionfile) + print(f"UPDATING {target_versionfile}") write_to_version_file(target_versionfile, versions) cmds["build_py"] = cmd_build_py @@ -1989,7 +1986,7 @@ def run(self) -> None: "version update. This can happen if you are running build_ext " "without first running build_py.") return - print("UPDATING %s" % target_versionfile) + print(f"UPDATING {target_versionfile}") write_to_version_file(target_versionfile, versions) cmds["build_ext"] = cmd_build_ext @@ -2008,7 +2005,7 @@ def run(self) -> None: cfg = get_config_from_root(root) versions = get_versions() target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) + print(f"UPDATING {target_versionfile}") write_to_version_file(target_versionfile, versions) _build_exe.run(self) @@ -2037,7 +2034,7 @@ def run(self) -> None: cfg = get_config_from_root(root) versions = get_versions() target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) + print(f"UPDATING {target_versionfile}") write_to_version_file(target_versionfile, versions) _py2exe.run(self) @@ -2105,7 +2102,7 @@ def run(self) -> None: self.distribution.metadata.version = versions["version"] return _sdist.run(self) - def make_release_tree(self, base_dir: str, files: List[str]) -> None: + def make_release_tree(self, base_dir: str, files: list[str]) -> None: root = get_root() cfg = get_config_from_root(root) _sdist.make_release_tree(self, base_dir, files) @@ -2113,7 +2110,7 @@ def make_release_tree(self, base_dir: str, files: List[str]) -> None: # (remembering that it may be a hardlink) and replace it with an # updated value target_versionfile = os.path.join(base_dir, cfg.versionfile_source) - print("UPDATING %s" % target_versionfile) + print(f"UPDATING {target_versionfile}") write_to_version_file(target_versionfile, self._versioneer_generated_versions) cmds["sdist"] = cmd_sdist @@ -2185,7 +2182,7 @@ def do_setup() -> int: print(CONFIG_ERROR, file=sys.stderr) return 1 - print(" creating %s" % cfg.versionfile_source) + print(f" creating {cfg.versionfile_source}") with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", @@ -2197,7 +2194,7 @@ def do_setup() -> int: ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") - maybe_ipy: Optional[str] = ipy + maybe_ipy: str | None = ipy if os.path.exists(ipy): try: with open(ipy, "r") as f: @@ -2207,17 +2204,17 @@ def do_setup() -> int: module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] snippet = INIT_PY_SNIPPET.format(module) if OLD_SNIPPET in old: - print(" replacing boilerplate in %s" % ipy) + print(f" replacing boilerplate in {ipy}") with open(ipy, "w") as f: f.write(old.replace(OLD_SNIPPET, snippet)) elif snippet not in old: - print(" appending to %s" % ipy) + print(f" appending to {ipy}") with open(ipy, "a") as f: f.write(snippet) else: - print(" %s unmodified" % ipy) + print(f" {ipy} unmodified") else: - print(" %s doesn't exist, ok" % ipy) + print(f" {ipy} doesn't exist, ok") maybe_ipy = None # Make VCS-specific changes. For git, this means creating/changing @@ -2233,7 +2230,7 @@ def scan_setup_py() -> int: setters = False errors = 0 with open("setup.py", "r") as f: - for line in f.readlines(): + for line in f: if "import versioneer" in line: found.add("import") if "versioneer.get_cmdclass()" in line: @@ -2245,21 +2242,21 @@ def scan_setup_py() -> int: if "versioneer.versionfile_source" in line: setters = True if len(found) != 3: - print("") + print() print("Your setup.py appears to be missing some important items") print("(but I might be wrong). Please make sure it has something") print("roughly like the following:") - print("") + print() print(" import versioneer") print(" setup( version=versioneer.get_version(),") print(" cmdclass=versioneer.get_cmdclass(), ...)") - print("") + print() errors += 1 if setters: print("You should remove lines like 'versioneer.VCS = ' and") print("'versioneer.versionfile_source = ' . This configuration") print("now lives in setup.cfg, and should be removed from setup.py") - print("") + print() errors += 1 return errors