diff --git a/.github/matrix.py b/.github/matrix.py index 17a63d3..34883f1 100644 --- a/.github/matrix.py +++ b/.github/matrix.py @@ -1,3 +1,4 @@ +# noqa:INP001 import fileinput import json import re @@ -9,21 +10,22 @@ def main(): actions_matrix = [] - for tox_env in fileinput.input(): - tox_env = tox_env.rstrip() - - if python_match := PY_VERSIONS_RE.match(tox_env): - version_tuple = python_match.groups() - else: - version_tuple = sys.version_info[0:2] - - python_version = "{}.{}".format(*version_tuple) - actions_matrix.append( - { - "python": python_version, - "tox_env": tox_env, - } - ) + with fileinput.input() as f: + for tox_env_line in f: + tox_env = tox_env_line.rstrip() + + if python_match := PY_VERSIONS_RE.match(tox_env): + version_tuple = python_match.groups() + else: + version_tuple = sys.version_info[0:2] + + python_version = "{}.{}".format(*version_tuple) + actions_matrix.append( + { + "python": python_version, + "tox_env": tox_env, + } + ) print(json.dumps(actions_matrix)) # noqa:T201 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be659df..fc61d38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,20 +3,21 @@ on: pull_request concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: {} jobs: matrix: name: Build test matrix runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "requirements/*.txt" - name: Run tox @@ -37,12 +38,12 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} cache: "pip" @@ -66,17 +67,18 @@ jobs: if: always() steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "requirements/*.txt" - - uses: actions/download-artifact@v4 + - name: Download coverage data + uses: actions/download-artifact@v5 with: pattern: coverage-data-* merge-multiple: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2890aab..2944026 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,6 +5,8 @@ on: tags: - "*" +permissions: {} + jobs: build: name: Build packages @@ -13,13 +15,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" - name: Build packages run: | pip install -r requirements/testing.txt @@ -44,7 +46,7 @@ jobs: steps: - name: Download packages - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: python-package-distributions path: dist/ @@ -61,20 +63,13 @@ jobs: url: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }} permissions: contents: write - id-token: write steps: - name: Download packages - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: python-package-distributions path: dist/ - - name: Sign packages - uses: sigstore/gh-action-sigstore-python@v3.0.0 - with: - inputs: >- - ./dist/*.tar.gz - ./dist/*.whl - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} @@ -83,7 +78,7 @@ jobs: "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --title "${GITHUB_REPOSITORY#*/} $GITHUB_REF_NAME" - - name: Upload artifact signatures to GitHub Release + - name: Upload packages to GitHub Release env: GH_TOKEN: ${{ github.token }} run: >- diff --git a/pyimgproxy/image.py b/pyimgproxy/image.py index b35ca04..22bb691 100644 --- a/pyimgproxy/image.py +++ b/pyimgproxy/image.py @@ -3,7 +3,7 @@ import hmac import re from functools import cached_property -from typing import TYPE_CHECKING, Any, Optional, Union, overload +from typing import TYPE_CHECKING, Any, overload if TYPE_CHECKING: from .imgproxy import ImgProxy @@ -49,14 +49,14 @@ def add_option(self, option_name: str, *args: Any) -> "Image": def resize( self, - resizing_type: Optional[str] = None, - width: Optional[int] = None, - height: Optional[int] = None, - enlarge: Optional[bool] = None, - extend: Optional[bool] = None, - gravity_type: Optional[str] = None, - x_offset: Optional[Union[int, float]] = None, - y_offset: Optional[Union[int, float]] = None, + resizing_type: str | None = None, + width: int | None = None, + height: int | None = None, + enlarge: bool | None = None, + extend: bool | None = None, + gravity_type: str | None = None, + x_offset: int | float | None = None, + y_offset: int | float | None = None, ) -> "Image": """ This is a meta-option that defines the resizing type, width, height, enlarge, and extend. @@ -76,13 +76,13 @@ def resize( def size( self, - width: Optional[int] = None, - height: Optional[int] = None, - enlarge: Optional[bool] = None, - extend: Optional[bool] = None, - gravity_type: Optional[str] = None, - x_offset: Optional[Union[int, float]] = None, - y_offset: Optional[Union[int, float]] = None, + width: int | None = None, + height: int | None = None, + enlarge: bool | None = None, + extend: bool | None = None, + gravity_type: str | None = None, + x_offset: int | float | None = None, + y_offset: int | float | None = None, ) -> "Image": """ This is a meta-option that defines the width, height, enlarge, and extend. All arguments @@ -160,7 +160,7 @@ def min_height(self, height: int) -> "Image": """ return self.add_option("min-height", height) - def zoom(self, x: Union[int, float], y: Optional[Union[int, float]] = None) -> "Image": + def zoom(self, x: int | float, y: int | float | None = None) -> "Image": """ When set, imgproxy will multiply the image dimensions according to these factors. The values must be greater than 0. @@ -175,7 +175,7 @@ def zoom(self, x: Union[int, float], y: Optional[Union[int, float]] = None) -> " """ return self.add_option("zoom", x, y) - def dpr(self, dpr: Union[int, float]) -> "Image": + def dpr(self, dpr: int | float) -> "Image": """ When set, imgproxy will multiply the image dimensions according to this factor for HiDPI (Retina) devices. The value must be greater than 0. @@ -197,10 +197,10 @@ def enlarge(self, enlarge: bool = False) -> "Image": def extend( self, - extend: Optional[bool] = None, - gravity_type: Optional[str] = None, - x_offset: Optional[Union[int, float]] = None, - y_offset: Optional[Union[int, float]] = None, + extend: bool | None = None, + gravity_type: str | None = None, + x_offset: int | float | None = None, + y_offset: int | float | None = None, ) -> "Image": """ - When extend is set to `True`, imgproxy will extend the image if it is smaller than the @@ -214,10 +214,10 @@ def extend( def extend_aspect_ratio( self, - extend: Optional[bool] = None, - gravity_type: Optional[str] = None, - x_offset: Optional[Union[int, float]] = None, - y_offset: Optional[Union[int, float]] = None, + extend: bool | None = None, + gravity_type: str | None = None, + x_offset: int | float | None = None, + y_offset: int | float | None = None, ) -> "Image": """ - When extend is set to `True`, imgproxy will extend the image to the requested aspect @@ -231,9 +231,9 @@ def extend_aspect_ratio( def gravity( self, - gravity_type: Optional[str] = None, - x_offset: Optional[Union[int, float]] = None, - y_offset: Optional[Union[int, float]] = None, + gravity_type: str | None = None, + x_offset: int | float | None = None, + y_offset: int | float | None = None, ) -> "Image": """ When imgproxy needs to cut some parts of the image, it is guided by the gravity option. @@ -260,11 +260,11 @@ def gravity( def crop( self, - width: Optional[Union[int, float]] = None, - height: Optional[Union[int, float]] = None, - gravity_type: Optional[str] = None, - x_offset: Optional[Union[int, float]] = None, - y_offset: Optional[Union[int, float]] = None, + width: int | float | None = None, + height: int | float | None = None, + gravity_type: str | None = None, + x_offset: int | float | None = None, + y_offset: int | float | None = None, ) -> "Image": """ Defines an area of the image to be processed (crop before resize). @@ -282,10 +282,10 @@ def crop( def trim( self, - threshold: Optional[Union[int, float]] = None, - color: Optional[str] = None, - equal_hor: Optional[bool] = None, - equal_ver: Optional[bool] = None, + threshold: int | float | None = None, + color: str | None = None, + equal_hor: bool | None = None, + equal_ver: bool | None = None, ) -> "Image": """ Removes surrounding background. @@ -303,9 +303,9 @@ def trim( def padding( self, top: int, - right: Optional[int] = None, - bottom: Optional[int] = None, - left: Optional[int] = None, + right: int | None = None, + bottom: int | None = None, + left: int | None = None, ) -> "Image": """ Defines padding size using CSS-style syntax. All arguments are optional but at least one @@ -348,10 +348,10 @@ def background(self, *, red: int, green: int, blue: int) -> "Image": ... def background( self, *, - red: Optional[int] = None, - green: Optional[int] = None, - blue: Optional[int] = None, - hex_color: Optional[str] = None, + red: int | None = None, + green: int | None = None, + blue: int | None = None, + hex_color: str | None = None, ) -> "Image": """ When set, imgproxy will fill the resulting image background with the specified color. @@ -366,7 +366,7 @@ def background( return self.add_option("background", red, green, blue) - def background_alpha(self, alpha: Union[int, float]) -> "Image": + def background_alpha(self, alpha: int | float) -> "Image": """ Adds an alpha channel to `background`. The value of `alpha` is a positive floating point number between `0` and `1`. @@ -377,9 +377,9 @@ def background_alpha(self, alpha: Union[int, float]) -> "Image": def adjust( self, - brightness: Optional[int] = None, - contrast: Optional[Union[int, float]] = None, - saturation: Optional[Union[int, float]] = None, + brightness: int | None = None, + contrast: int | float | None = None, + saturation: int | float | None = None, ) -> "Image": """ This is a meta-option that defines the brightness, contrast, and saturation. All arguments @@ -396,7 +396,7 @@ def brightness(self, brightness: int) -> "Image": """ return self.add_option("brightness", brightness) - def contrast(self, contrast: Union[int, float]) -> "Image": + def contrast(self, contrast: int | float) -> "Image": """ When set, imgproxy will adjust the contrast of the resulting image. `contrast` is a positive floating point number, where a value of `1` leaves the contrast unchanged. @@ -405,7 +405,7 @@ def contrast(self, contrast: Union[int, float]) -> "Image": """ return self.add_option("contrast", contrast) - def saturation(self, saturation: Union[int, float]) -> "Image": + def saturation(self, saturation: int | float) -> "Image": """ When set, imgproxy will adjust saturation of the resulting image. `saturation` is a positive floating-point number, where a value of `1` leaves the saturation unchanged. @@ -414,7 +414,7 @@ def saturation(self, saturation: Union[int, float]) -> "Image": """ return self.add_option("saturation", saturation) - def blur(self, sigma: Union[int, float]) -> "Image": + def blur(self, sigma: int | float) -> "Image": """ When set, imgproxy will apply a gaussian blur filter to the resulting image. The value of `sigma` defines the size of the mask imgproxy will use. @@ -423,7 +423,7 @@ def blur(self, sigma: Union[int, float]) -> "Image": """ return self.add_option("blur", sigma) - def sharpen(self, sigma: Union[int, float]) -> "Image": + def sharpen(self, sigma: int | float) -> "Image": """ When set, imgproxy will apply the sharpen filter to the resulting image. The value of `sigma` defines the size of the mask imgproxy will use. @@ -446,9 +446,9 @@ def pixelate(self, size: int) -> "Image": def unsharp_masking( self, - mode: Optional[str] = None, - weight: Optional[Union[int, float]] = None, - divider: Optional[Union[int, float]] = None, + mode: str | None = None, + weight: int | float | None = None, + divider: int | float | None = None, ) -> "Image": """ Allows redefining unsharp masking options. All arguments have the same meaning as Unsharp @@ -456,9 +456,7 @@ def unsharp_masking( """ return self.add_option("unsharp_masking", mode, weight, divider) - def blur_detections( - self, sigma: Union[int, float], class_names: Optional[list[str]] = None - ) -> "Image": + def blur_detections(self, sigma: int | float, class_names: list[str] | None = None) -> "Image": """ imgproxy detects objects of the provided classes and blurs them. If class names are omitted, imgproxy blurs all the detected objects. @@ -469,7 +467,7 @@ def blur_detections( class_names = [] return self.add_option("blur_detections", sigma, *class_names) - def draw_detections(self, draw: bool, class_names: Optional[list[str]] = None) -> "Image": + def draw_detections(self, draw: bool, class_names: list[str] | None = None) -> "Image": """ When draw is set to `True`, imgproxy detects objects of the provided classes and draws their bounding boxes. If class names are omitted, imgproxy draws the bounding boxes of all @@ -715,9 +713,7 @@ def _source_url_needs_encoding(self) -> bool: """ Return a boolean if a source URL needs encoding with base64. """ - if self.url_escape_regex.search(self._source_url): - return True - return False + return bool(self.url_escape_regex.search(self._source_url)) @cached_property def url(self) -> str: diff --git a/pyproject.toml b/pyproject.toml index aa28c6f..61c93c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,25 +4,25 @@ version = '0.3.1' description = 'Python client for imgproxy' readme = 'README.md' maintainers = [{ name = 'The Developer Society', email = 'studio@dev.ngo' }] -requires-python = '>= 3.9' +requires-python = '>= 3.10' +license = 'BSD-3-Clause' classifiers = [ - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', ] [project.urls] -Homepage = "https://github.com/developersociety/pyimgproxy" +Homepage = 'https://github.com/developersociety/pyimgproxy' [build-system] -requires = ['setuptools >= 61.0'] +requires = ['setuptools >= 77.0.3'] build-backend = 'setuptools.build_meta' [tool.setuptools] @@ -33,27 +33,82 @@ include = ['pyimgproxy*'] [tool.ruff] line-length = 99 -target-version = 'py39' +target-version = 'py310' [tool.ruff.lint] -select = [ - 'F', # pyflakes - 'E', # pycodestyle - 'W', # pycodestyle - 'I', # isort - 'N', # pep8-naming - 'UP', # pyupgrade - 'S', # flake8-bandit - 'BLE', # flake8-blind-except - 'C4', # flake8-comprehensions - 'EM', # flake8-errmsg - 'T20', # flake8-print - 'RET', # flake8-return - 'RUF', # ruff +extend-select = [ + 'ERA', # eradicate + 'YTT', # flake8-2020 + 'ASYNC', # flake8-async + 'S', # flake8-bandit + 'BLE', # flake8-blind-except + 'B', # flake8-bugbear + 'A', # flake8-builtins + 'COM', # flake8-commas + 'C4', # flake8-comprehensions + 'DTZ', # flake8-datetimez + 'T10', # flake8-debugger + 'DJ', # flake8-django + 'EM', # flake8-errmsg + 'EXE', # flake8-executable + 'FA', # flake8-future-annotations + 'INT', # flake8-gettext + 'ISC', # flake8-implicit-str-concat + 'ICN', # flake8-import-conventions + 'LOG', # flake8-logging + 'G', # flake8-logging-format + 'INP', # flake8-no-pep420 + 'PIE', # flake8-pie + 'T20', # flake8-print + 'PYI', # flake8-pyi + 'Q', # flake8-quotes + 'RSE', # flake8-raise + 'RET', # flake8-return + 'SLOT', # flake8-slots + 'SIM', # flake8-simplify + 'TID', # flake8-tidy-imports + 'TD', # flake8-todos + 'TCH', # flake8-type-checking + 'PTH', # flake8-use-pathlib + 'FLY', # flynt + 'I', # isort + 'NPY', # numpy-specific rules + 'PD', # pandas-vet + 'N', # pep8-naming + 'PERF', # perflint + 'E', # pycodestyle + 'W', # pycodestyle + 'F', # pyflakes + 'PGH', # pygrep-hooks + 'PLC', # pylint + 'PLE', # pylint + 'PLW', # pylint + 'UP', # pyupgrade + 'FURB', # refurb + 'RUF', # ruff-specific rules + 'TRY', # tryceratops ] ignore = [ - 'EM101', # flake8-errmsg: raw-string-in-exception + 'COM812', # flake8-commas: missing-trailing-comma + 'EM101', # flake8-errmsg: raw-string-in-exception + 'ISC001', # flake8-implicit-str-concat: single-line-implicit-string-concatenation + 'PYI041', # flake8-pyi: redundant-numeric-union + 'RUF012', # ruff-specific rules: mutable-class-default + 'SIM105', # flake8-simplify: suppressible-exception + 'SIM108', # flake8-simplify: if-else-block-instead-of-if-exp + 'TD002', # flake8-todos: missing-todo-author + 'TRY003', # tryceratops: raise-vanilla-args ] [tool.ruff.lint.isort] combine-as-imports = true +section-order = [ + 'future', + 'standard-library', + 'third-party', + 'first-party', + 'local-folder', +] + +[tool.ruff.lint.pep8-naming] +extend-ignore-names = ['assert*'] diff --git a/requirements/local.txt b/requirements/local.txt index 8efbdb1..fbf01b3 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -1,5 +1,5 @@ -r testing.txt -bump-my-version==0.31.1 -tox==4.24.1 -tox-uv==1.22.1 +bump-my-version==1.2.4 +tox==4.31.0 +tox-uv==1.28.1 diff --git a/requirements/testing.txt b/requirements/testing.txt index 7dd4bd2..27bd38d 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,8 +1,8 @@ -build==1.2.2.post1 -check-wheel-contents==0.6.1 -coverage==7.6.10 -mypy==1.15.0 -pipdeptree==2.25.0 -pytest==8.3.4 -ruff==0.9.4 -twine==6.1.0 +build==1.3.0 +check-wheel-contents==0.6.3 +coverage==7.10.7 +mypy==1.18.2 +pipdeptree==2.28.0 +pytest==8.4.2 +ruff==0.14.0 +twine==6.2.0 diff --git a/tox.ini b/tox.ini index 1196f42..d669689 100644 --- a/tox.ini +++ b/tox.ini @@ -2,11 +2,11 @@ env_list = check lint - py39 py310 py311 py312 py313 + py314 coverage no_package = true @@ -17,14 +17,14 @@ commands = make test package = editable [testenv:check] -base_python = python3.13 +base_python = python3.14 commands = make check uv_seed = true [testenv:lint] -base_python = python3.13 +base_python = python3.14 commands = make lint [testenv:coverage] -base_python = python3.13 +base_python = python3.14 commands = make coverage-report