From 8ee0030363104c3584a75f6506d9a1b4d0a906b4 Mon Sep 17 00:00:00 2001 From: zerolab Date: Fri, 19 Jan 2024 17:39:49 +0000 Subject: [PATCH] Add tox, configure GHA, pre-commit, ruff --- .coveragerc | 37 +++++++++ .editorconfig | 14 ++++ .github/workflows/publish.yml | 51 +++++++++++++ .github/workflows/ruff.yml | 22 ++++++ .github/workflows/test.yml | 140 ++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 26 +++++++ CHANGELOG.md | 1 + README.md | 48 ++++++------ pyproject.toml | 40 ++++++++++ tox.ini | 81 ++++++++++++++++++++ 10 files changed, 437 insertions(+), 23 deletions(-) create mode 100644 .coveragerc create mode 100644 .editorconfig create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/ruff.yml create mode 100644 .github/workflows/test.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..0dc82a6 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,37 @@ +[run] +branch = True +concurrency = multiprocessing, thread +parallel = True +source_pkgs = wagtail_bynder +omit = **/migrations/*,tests/* + +[paths] +source = src,.tox/py*/**/site-packages + +[report] +show_missing = True +ignore_errors = True +skip_covered = True + +# Regexes for lines to exclude from consideration +exclude_also = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self.debug + if settings.DEBUG + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + # Nor complain about type checking + "if TYPE_CHECKING:", + class .*\bProtocol\): + @(abc\.)?abstractmethod diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..97d5b8a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf + +[*.{yaml,yml,json,md}] +indent_size = 2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..da3c2e6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,51 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read # to fetch code (actions/checkout) + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: "pip" + cache-dependency-path: "**/pyproject.toml" + + - name: ⬇️ Install build dependencies + run: | + python -m pip install flit + + - name: 🏗️ Build + run: python -m flit build + + - uses: actions/upload-artifact@v3 + with: + path: ./dist + + # https://docs.pypi.org/trusted-publishers/using-a-publisher/ + pypi-publish: + needs: build + environment: 'publish' + + name: ⬆️ Upload release to PyPI + runs-on: ubuntu-latest + permissions: + # Mandatory for trusted publishing + id-token: write + steps: + - uses: actions/download-artifact@v3 + + - name: 🚀 Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: artifact/ + print-hash: true diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..7bad454 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,22 @@ +name: Ruff + +on: + push: + branches: + - main + - 'stable/**' + pull_request: + branches: [main] + +jobs: + ruff: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - run: python -Im pip install --user ruff + + - name: Run ruff + working-directory: ./src + run: ruff --output-format=github wagtail_bynder diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1fd34b2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,140 @@ +name: Wagtail Bynder CI + +on: + push: + branches: + - main + - 'stable/**' + + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + FORCE_COLOR: '1' # Make tools pretty. + TOX_TESTENV_PASSENV: FORCE_COLOR + PIP_DISABLE_PIP_VERSION_CHECK: '1' + PIP_NO_PYTHON_VERSION_WARNING: '1' + PYTHON_LATEST: '3.11' + +jobs: + test-sqlite: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + django: ["4.2"] + wagtail: ["5.2"] + db: ["sqlite"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -Im pip install --upgrade pip + python -Im pip install flit tox tox-gh-actions + python -Im flit install --symlink + + - name: 🏗️ Build wheel + run: python -Im flit build --format wheel + + - name: Test + env: + TOXENV: py${{ matrix.python-version }}-django${{ matrix.django }}-wagtail${{ matrix.wagtail }}-sqlite + run: tox --installpkg ./dist/*.whl + + - name: ⬆️ Upload coverage data + uses: actions/upload-artifact@v3 + with: + name: coverage-data + path: .coverage.* + if-no-files-found: ignore + retention-days: 1 + + test-postgres: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + db: ["postgres"] + + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -Im pip install --upgrade pip + python -Im pip install flit tox tox-gh-actions + python -Im flit install --symlink + + - name: 🏗️ Build wheel + run: python -Im flit build --format wheel + + - name: Test + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/wagtail_localize_git + run: tox --installpkg ./dist/*.whl + + - name: ⬆️ Upload coverage data + uses: actions/upload-artifact@v3 + with: + name: coverage-data + path: .coverage.* + if-no-files-found: ignore + retention-days: 1 + + coverage: + runs-on: ubuntu-latest + needs: + - test-sqlite + - test-postgres + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + # Use latest Python, so it understands all syntax. + python-version: ${{env.PYTHON_LATEST}} + + - run: python -Im pip install --upgrade coverage + + - name: ⬇️ Download coverage data + uses: actions/download-artifact@v3 + with: + name: coverage-data + + - name: + Combine coverage + run: | + python -Im coverage combine + python -Im coverage html --skip-covered --skip-empty + python -Im coverage report + echo "## Coverage summary" >> $GITHUB_STEP_SUMMARY + python -Im coverage report --format=markdown >> $GITHUB_STEP_SUMMARY + - name: 📈 Upload HTML report + uses: actions/upload-artifact@v3 + with: + name: html-report + path: htmlcov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1edbd4b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +ci: + autofix_prs: false + +default_language_version: + python: python3.11 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + args: ["--unsafe"] + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.1.14' + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..601c2d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Wagtail Bynder Changelog diff --git a/README.md b/README.md index 118edb2..a6ef811 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Bynder integration for Wagtail +[![Build status](https://img.shields.io/github/actions/workflow/status/torchbox/wagtail-bynder/test.yml?branch=main)](https://github.com/torchbox/wagtail-bynder/actions) [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![PyPI version](https://img.shields.io/pypi/v/wagtail-bynder.svg?style=flat)](https://pypi.org/project/wagtail-bynder) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) ## Links @@ -11,23 +13,23 @@ - [Discussions](https://github.com/torchbox/wagtail-bynder/discussions) - [Security](https://github.com/torchbox/wagtail-bynder/security) -[Bynder](https://www.bynder.com) is a Digital Asset Management System (DAMS) and platform that allows organisations +[Bynder](https://www.bynder.com) is a Digital Asset Management System (DAMS) and platform that allows organisations to manage their digital assets, which includes the images and documents used in Wagtail content. -The data flow is one way: Bynder assets are always treated as the source of truth, and Wagtail uses read-only API access +The data flow is one way: Bynder assets are always treated as the source of truth, and Wagtail uses read-only API access to create copies of assets and keep them up-to-date. ## How it works -The main points of integration are Wagtail's image and document chooser views, which are patched by this app to show an +The main points of integration are Wagtail's image and document chooser views, which are patched by this app to show an asset selection UI for Bynder instead of a list of Wagtail images or documents. -When an asset is selected, Wagtail silently downloads the file and related metadata, and saves it as an `Image` or -`Document` object, allowing it to be used in a typical way. The ID of the selected asset (as well as a few other bits of data) -are saved on the object when this happens, helping Wagtail to recognise when it already has a copy of an asset, +When an asset is selected, Wagtail silently downloads the file and related metadata, and saves it as an `Image` or +`Document` object, allowing it to be used in a typical way. The ID of the selected asset (as well as a few other bits of data) +are saved on the object when this happens, helping Wagtail to recognise when it already has a copy of an asset, and to help keep them up-to-date with changes made in Bynder. -Currently, changes are synced from Bynder back to Wagtail via a couple of well optimised management commands, +Currently, changes are synced from Bynder back to Wagtail via a couple of well optimised management commands, intended to be run regularly (via a cron job): - `python manage.py update_stale_images` @@ -53,7 +55,7 @@ MIDDLEWARE = [ ] ``` -Import the abstract `BynderSyncedImage` model and have your project's custom image model definition subclass it instead +Import the abstract `BynderSyncedImage` model and have your project's custom image model definition subclass it instead of `wagtail.images.models.AbstractImage`. For example ```python @@ -65,7 +67,7 @@ class CustomImage(BynderSyncedImage): pass ``` -Import the abstract `BynderSyncedDocument` model and have your project's custom document model definition subclass it instead of +Import the abstract `BynderSyncedDocument` model and have your project's custom document model definition subclass it instead of `wagtail.documents.models.AbstractDocument`. For example: ```python @@ -86,12 +88,12 @@ $ python manage.py migrate ### Optional: To use videos from Bynder -To use videos from Bynder in content across the site, this app includes a specialised model to help store relevant data for videos, -plus blocks and chooser widgets to help use them in your project. However, because not all projects use video, -and project-specific requirements around video usage can be a little more custom, +To use videos from Bynder in content across the site, this app includes a specialised model to help store relevant data for videos, +plus blocks and chooser widgets to help use them in your project. However, because not all projects use video, +and project-specific requirements around video usage can be a little more custom, the model is `abstract` - you need to subclass it in order to use the functionality. -First, import the abstract `BynderSyncedVideo` model and subclass it within your project to create a concrete model. +First, import the abstract `BynderSyncedVideo` model and subclass it within your project to create a concrete model. For example: ```python @@ -103,7 +105,7 @@ class Video(BynderSyncedVideo): pass ``` -Next, in your project's Django settings, add a `BYNDER_VIDEO_MODEL` item to establish your custom model as the 'official' +Next, in your project's Django settings, add a `BYNDER_VIDEO_MODEL` item to establish your custom model as the 'official' video model. The value should be a string in the format `"app_label.Model"`. For example: ```python @@ -144,8 +146,8 @@ Example: `"64ae04f71460cfed1b289c4c1db4c9b273b238dx2030c51298dcad245b5ff1f8"` Default: `None` -An API token for Bynder's JavaScript 'compact view' to use. The value is injected into the `admin_base.html` template for Wagtail -for the JavaScript to pick up, exposing it to Wagtail users. Because of this, it should be different to `BYNDER_API_TOKEN` +An API token for Bynder's JavaScript 'compact view' to use. The value is injected into the `admin_base.html` template for Wagtail +for the JavaScript to pick up, exposing it to Wagtail users. Because of this, it should be different to `BYNDER_API_TOKEN` and only needs to have basic read permissions. ### `BYNDER_IMAGE_SOURCE_THUMBNAIL_NAME` @@ -154,11 +156,11 @@ Example: `"WagtailSource"` Default: `"webimage"` -The name of the automatically generated derivative that should be downloaded and used as the `file` value for the +The name of the automatically generated derivative that should be downloaded and used as the `file` value for the representative Wagtail image (as it appears in `thumbnails` in the API representation). -WARNING: It's important to get this right, because if the specified derivative is NOT present in the response for an -image for any reason, the ORIGINAL will be downloaded - which will lead to slow chooser response times and higher memory +WARNING: It's important to get this right, because if the specified derivative is NOT present in the response for an +image for any reason, the ORIGINAL will be downloaded - which will lead to slow chooser response times and higher memory usage when generating renditions. ### `BYNDER_VIDEO_MODEL` @@ -185,8 +187,8 @@ Example: `True` Default: `False` -When `True`, local copies of images will be refreshed from the Bynder API representation whenever they are selected in -the chooser interface. This slows down the chooser experience slightly, but can be useful for seeing up-to-date data in +When `True`, local copies of images will be refreshed from the Bynder API representation whenever they are selected in +the chooser interface. This slows down the chooser experience slightly, but can be useful for seeing up-to-date data in environments that might not be using the management commands or other means to keep images up-to-date with their Bynder counterparts. ### `BYNDER_SYNC_EXISTING_DOCUMENTS_ON_CHOOSE` @@ -203,8 +205,8 @@ Example: `True` Default: `False` -When `True`, hitting Wagtail's built-in edit view for an image or document will result in a redirect to the asset +When `True`, hitting Wagtail's built-in edit view for an image or document will result in a redirect to the asset detail view in the Bynder interface. -The default is value is `False`, because it can be useful to use the Wagtail representation to check that file, metadata +The default is value is `False`, because it can be useful to use the Wagtail representation to check that file, metadata and focal points are being accurately reflected. diff --git a/pyproject.toml b/pyproject.toml index 95339dd..cbaaa48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,3 +69,43 @@ exclude = [ "CHANGELOG.md", "testmanage.py", ] + +[tool.ruff] +target-version = "py38" + +extend-select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "DJ", # flake8-django + "E", # pycodestyle errors + "F", # pyflakes + "FBT", # flake8-boolean-trap + "I", # isort + "INT", # flake8-gettext + "ISC", # flake8-implicit-string-concatenation + "PIE", # flake8-pie + "PGH", # pygrep-hooks + "S", # flake8-bandit + "SIM", # flake8-simplify + "W", # pycodestyle warnings + "YTT", # flake8-2020 + "UP", # pyupgrade + "RUF100", # unused noqa +] + +extend-ignore = [ + "E501", # no line lenght errors +] +fixable = ["C4", "E", "F", "I", "UP"] + +[tool.ruff.isort] +known-first-party = ["src", "wagtail_bynder"] +lines-between-types = 1 +lines-after-imports = 2 + +[tool.ruff.per-file-ignores] +"src/wagtail_bynder/models.py" = [ + # "Possible SQL injection vector through string-based query construction" + # This is a false positive because the lines in question raise an exception + "S608", +] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b66dc8d --- /dev/null +++ b/tox.ini @@ -0,0 +1,81 @@ +[tox] +min_version = 4.0 + +envlist = + py{3.8,3.9,3.10}-django{3.2}-wagtail{4.1, 5.2} + py{3.8,3.9,3.10,3.11,3.12}-django{4.2}-wagtail{5.2} + py{3.10,3.11,3.12}-django{5.0}-wagtail{5.2} + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + +[gh-actions:env] +DB = + sqlite: sqlite + postgres: postgres + +[testenv] +package = wheel +wheel_build_env = .pkg + +pass_env = + FORCE_COLOR + NO_COLOR + +setenv = + PYTHONPATH = {toxinidir}/tests:{toxinidir} + PYTHONDEVMODE = 1 + +deps = + flit>=3.8 + + django3.2: Django>=3.2,<3.3 + django4.2: Django>=4.2,<4.3 + django5.0: Django>=5.0,<5.1 + djmain: git+https://github.com/django/django.git@main#egg=Django + + wagtail4.1: wagtail>=4.1,<4.2 + wagtail5.2: wagtail>=5.2,<5.3 + wagtailmain: git+https://github.com/wagtail/wagtail.git + + postgres: psycopg2>=2.9 + + .[testing] + +install_command = python -Im pip install -U --pre {opts} {packages} +commands_pre = + python -I {toxinidir}/tests/manage.py migrate +commands = + python -m coverage run {toxinidir}/tests/manage.py test --deprecation all {posargs: -v 2} + +[testenv:coverage-report] +commands = + python -Im coverage combine + python -Im coverage report -m + +[testenv:interactive] +description = An interactive environment for local testing purposes +basepython = python3.11 + +commands_pre = + python {toxinidir}/tests/manage.py makemigrations + python {toxinidir}/tests/manage.py migrate + python {toxinidir}/tests/manage.py shell -c "from django.contrib.auth.models import User;(not User.objects.filter(username='admin').exists()) and User.objects.create_superuser('admin', 'super@example.com', 'changeme')" + python {toxinidir}/tests/manage.py createcachetable + +commands = + {posargs:python -Im {toxinidir}/tests/manage.py runserver 0.0.0.0:8020} + +setenv = + INTERACTIVE = 1 + +[testenv:wagtailmain] +deps = + flit>=3.8 + coverage>=7.0,<8.0 + wagtailmain: git+https://github.com/wagtail/wagtail.git@main#egg=Wagtail