Skip to content

Commit

Permalink
Tooling updates (Python, Django, Wagtail versions), and coverage summ…
Browse files Browse the repository at this point in the history
…ary (#21)

* Make Python 3.11 the minimum version, add Django 5.1, Wagtail 6.2
* Update GH test matrix
  - only run one combination on sqlite
  - bump min Postgres to 13 (as 12 is about to be obsolete)
* Include coverage report
* Drop the tox basepython map
* Use sysmon in Python 3.12
* Bump various test deps
* Run pyupgrade to Python 3.11+
  `git ls-files --others --cached --exclude-standard -- '*.py' | xargs pyupgrade --py311-plus`
* Tidy up based on code review
  • Loading branch information
zerolab authored Sep 13, 2024
1 parent 40708ed commit d19f483
Show file tree
Hide file tree
Showing 14 changed files with 148 additions and 102 deletions.
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[run]
branch = True
concurrency = multiprocessing, thread
include = src/wagtail_localize_smartling/*
omit = */migrations/*,*/tests/*
omit = **/migrations/*,tests/*,testapp/*

[paths]
source =
Expand Down
81 changes: 71 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ concurrency:
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.12'

jobs:
pyright:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.8', '3.9', '3.10', '3.11', '3.12']
python: ['3.11', '3.12']
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -30,15 +37,18 @@ jobs:
python-version: ${{ matrix.python }}
- name: Install
run: |
python -m pip install --upgrade pip setuptools wheel tox
python -m pip install --upgrade pip setuptools wheel tox tox-gh-actions
- name: Pyright (Python ${{ matrix.python }})
run: tox -e pyright -- --pythonversion ${{ matrix.python }}

test-sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.8', '3.9', '3.10', '3.11', '3.12']
python: ["3.11", "3.12"]
django: ["4.2"]
wagtail: ["6.2"]
db: ["sqlite"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }}
Expand All @@ -47,20 +57,29 @@ jobs:
python-version: ${{ matrix.python }}
- name: Install
run: |
python -m pip install --upgrade pip setuptools wheel tox
python -m pip install --upgrade pip setuptools wheel tox tox-gh-actions
- name: Test
run: tox
# run tox as quietly as possible, including skipping report output to the terminal
# also skip the coverage report as we include the files for the summary
run: tox -q -- -q --cov-report=
env:
DB: sqlite
TOXENV: python${{ matrix.python }}-django${{ matrix.django }}-wagtail${{ matrix.wagtail }}-sqlite
- name: Upload coverage data
uses: actions/upload-artifact@v4
with:
name: coverage-data-${{ matrix.python }}-sqlite
path: .coverage
if-no-files-found: ignore
retention-days: 1

test-postgres:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.8', '3.9', '3.10', '3.11', '3.12']
python: ['3.11', '3.12']
services:
postgres:
image: 'postgres:12'
image: 'postgres:13'
env:
POSTGRES_PASSWORD: postgres
ports:
Expand All @@ -74,9 +93,51 @@ jobs:
python-version: ${{ matrix.python }}
- name: Install
run: |
python -m pip install --upgrade pip setuptools wheel tox
python -m pip install --upgrade pip setuptools wheel tox tox-gh-actions
- name: Test
run: tox
# run tox as quietly as possible, including skipping report output to the terminal
# also skip the coverage report as we include the files for the summary
run: tox -q -- -q --cov-report= --cov-append
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/wagtail_localize_smartling
DB: postgres
- name: Upload coverage data
uses: actions/upload-artifact@v4
with:
name: coverage-data-${{ matrix.python }}
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@v4
with:
pattern: coverage-data-*
merge-multiple: true

- name: Combine coverage
run: |
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@v4
with:
name: html-report
path: htmlcov
5 changes: 2 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ default_language_version:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.6.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
Expand All @@ -24,8 +24,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
# ruff config is in ruff.toml
rev: v0.4.3
rev: v0.5.7
hooks:
- id: ruff
args: [--fix]
Expand Down
14 changes: 6 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,17 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"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",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.1",
"Framework :: Wagtail",
"Framework :: Wagtail :: 5",
"Framework :: Wagtail :: 6",
]
requires-python = ">=3.8"
requires-python = ">=3.11"
dynamic = [
"version",
]
Expand All @@ -46,20 +44,20 @@ dependencies = [
[project.optional-dependencies]
test = [
"beautifulsoup4==4.12.3",
"coverage==7.5.1",
"coverage>=7.6.1,<8.0",
"dj-database-url==2.1.0",
"django-stubs",
"djangorestframework-stubs",
"pre-commit==3.4.0",
"pyright==1.1.365",
"pyright==1.1.375",
"pytest-cov==5.0.0",
"pytest-django==4.8.0",
"pytest-responses==0.5.1",
"pytest-xdist==3.6.1",
"pytest==8.1.1",
"pytest==8.3.2",
"python-dotenv==1.0.1",
"responses==0.25.0",
"ruff==0.4.5",
"ruff==0.5.7",
"tox",
"tox-gh-actions",
"wagtail-factories==4.2.1"
Expand Down
36 changes: 16 additions & 20 deletions src/wagtail_localize_smartling/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@
import pprint
import textwrap

from collections.abc import Generator
from contextlib import contextmanager
from datetime import datetime, timedelta
from functools import cached_property
from io import BytesIO
from typing import (
TYPE_CHECKING,
Any,
Dict,
Generator,
List,
Literal,
Optional,
Type,
TypeVar,
cast,
)
Expand Down Expand Up @@ -73,7 +69,7 @@ class FailedResponse(SmartlingAPIError):
unsuccessful for some reason.
"""

def __init__(self, *, code: str, errors: List[types.SmartlingAPIErrorDict]):
def __init__(self, *, code: str, errors: list[types.SmartlingAPIErrorDict]):
self.code = code
self.errors = errors

Expand All @@ -98,8 +94,8 @@ class JobNotFound(SmartlingAPIError):
class SmartlingAPIClient:
def __init__(self):
self.token_type: str = "Bearer"
self.access_token: Optional[str] = None
self.refresh_token: Optional[str] = None
self.access_token: str | None = None
self.refresh_token: str | None = None

# Set expiry times in the past, initially
a_day_ago = timezone.now() - timedelta(days=1)
Expand All @@ -109,7 +105,7 @@ def __init__(self):
# Utilities

@property
def _headers(self) -> Dict[str, str]:
def _headers(self) -> dict[str, str]:
now = timezone.now()
if self.access_token is None or (self.access_token_expires_at <= now):
if self.refresh_token is not None and self.refresh_token_expires_at > now:
Expand Down Expand Up @@ -201,10 +197,10 @@ def _request(
*,
method: Literal["GET", "POST"],
path: str,
response_serializer_class: Type[ResponseSerializer],
response_serializer_class: type[ResponseSerializer],
send_headers: bool = True,
**kwargs,
) -> Dict[str, Any]:
) -> dict[str, Any]:
url = urljoin(self._base_url, path)
headers = self._headers if send_headers else {}

Expand Down Expand Up @@ -272,7 +268,7 @@ def get_project_details(
),
)

def list_jobs(self, *, name: Optional[str] = None) -> types.ListJobsResponseData:
def list_jobs(self, *, name: str | None = None) -> types.ListJobsResponseData:
params = {}
if name is not None:
params["jobName"] = name
Expand All @@ -290,19 +286,19 @@ def create_job(
self,
*,
job_name: str,
target_locale_ids: Optional[List[str]] = None,
description: Optional[str] = None,
reference_number: Optional[str] = None,
due_date: Optional[datetime] = None,
callback_url: Optional[str] = None,
callback_method: Optional[Literal["GET", "POST"]] = None,
target_locale_ids: list[str] | None = None,
description: str | None = None,
reference_number: str | None = None,
due_date: datetime | None = None,
callback_url: str | None = None,
callback_method: Literal["GET", "POST"] | None = None,
) -> types.CreateJobResponseData:
if (callback_url is None) != (callback_method is None):
raise ValueError(
"Both callback_url and callback_method must be provided, or neither"
)

params: Dict[str, Any] = {
params: dict[str, Any] = {
"jobName": job_name,
}
if target_locale_ids is not None:
Expand Down Expand Up @@ -373,7 +369,7 @@ def upload_po_file_for_job(self, *, job: "Job") -> str:
def add_file_to_job(self, *, job: "Job"):
# TODO handle 202 responses for files that get added asynchronously

body: Dict[str, Any] = {
body: dict[str, Any] = {
"fileUri": job.file_uri,
"targetLocaleIds": [
utils.format_smartling_locale_id(t.target_locale.language_code)
Expand Down
28 changes: 14 additions & 14 deletions src/wagtail_localize_smartling/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import timezone
from datetime import UTC
from functools import cached_property
from typing import Any, ClassVar, Dict, List, Tuple, Type
from typing import Any, ClassVar

from rest_framework import serializers

Expand Down Expand Up @@ -48,7 +48,7 @@ def __new__(cls, name, bases, attrs, **kwargs):
if any(isinstance(b, ResponseSerializerMetaclass) for b in bases):
# This a ResponseSerializer subclass, so loop over attrs and strip
# off any fields and nested serializers
data_serializer_fields: Dict[str, serializers.Field] = {}
data_serializer_fields: dict[str, serializers.Field] = {}
for attr_name, attr in list(attrs.items()):
if isinstance(attr, serializers.Field):
data_serializer_fields[attr_name] = attrs.pop(attr_name)
Expand Down Expand Up @@ -111,7 +111,7 @@ class InnerResponseSerializer(serializers.Serializer):
# Pyright a headache though.
errors = ErrorSerializer(required=False, many=True) # pyright: ignore[reportIncompatibleMethodOverride, reportAssignmentType]

def validate(self, attrs: Dict[str, Any]):
def validate(self, attrs: dict[str, Any]):
if "data" in attrs and "errors" in attrs:
raise serializers.ValidationError("data and errors cannot both be present")
return attrs
Expand All @@ -123,14 +123,14 @@ class ResponseSerializer(
):
# response = ...__InnerResponseSerializer() <- Created by ResponseSerializerMetaclass # noqa: E501

_data_serializer_class: ClassVar[Type[serializers.Serializer]]
_data_serializer_class: ClassVar[type[serializers.Serializer]]

@cached_property
def response_data(self) -> Dict[str, Any]:
def response_data(self) -> dict[str, Any]:
return self.validated_data["response"]["data"]

@cached_property
def response_errors(self) -> Tuple[str, List[types.SmartlingAPIErrorDict]]:
def response_errors(self) -> tuple[str, list[types.SmartlingAPIErrorDict]]:
return (
self.validated_data["response"]["code"],
self.validated_data["response"]["errors"],
Expand Down Expand Up @@ -198,30 +198,30 @@ class CreateJobResponseSerializer(ResponseSerializer):
callbackMethod = serializers.ChoiceField(choices=["GET", "POST"], allow_null=True)
callbackUrl = serializers.URLField(allow_null=True)
createdByUserUid = serializers.CharField()
createdDate = serializers.DateTimeField(default_timezone=timezone.utc)
createdDate = serializers.DateTimeField(default_timezone=UTC)
description = serializers.CharField(allow_blank=True)
dueDate = serializers.DateTimeField(default_timezone=timezone.utc, allow_null=True)
dueDate = serializers.DateTimeField(default_timezone=UTC, allow_null=True)
firstCompletedDate = serializers.DateTimeField(
default_timezone=timezone.utc,
default_timezone=UTC,
allow_null=True,
)
firstAuthorizedDate = serializers.DateTimeField(
default_timezone=timezone.utc,
default_timezone=UTC,
allow_null=True,
)
jobName = serializers.CharField()
jobNumber = serializers.CharField(allow_null=True)
jobStatus = serializers.ChoiceField(choices=types.JobStatus.values)
lastCompletedDate = serializers.DateTimeField(
default_timezone=timezone.utc,
default_timezone=UTC,
allow_null=True,
)
lastAuthorizedDate = serializers.DateTimeField(
default_timezone=timezone.utc,
default_timezone=UTC,
allow_null=True,
)
modifiedByUserUid = serializers.CharField(allow_null=True)
modifiedDate = serializers.DateTimeField(default_timezone=timezone.utc)
modifiedDate = serializers.DateTimeField(default_timezone=UTC)
targetLocaleIds = serializers.ListField(child=serializers.CharField())
translationJobUid = serializers.CharField()
referenceNumber = serializers.CharField(allow_null=True, allow_blank=True)
Expand Down
Loading

0 comments on commit d19f483

Please sign in to comment.