Skip to content

Commit 7d58e87

Browse files
authored
Update linters formatters and supported versions (#29)
* Bump dev container image to python:3.8-bookworm * Removed the black config to skip string normalization and then ran black against all the things * removed old yapf config * Clean up docker-compose.yml a bit * Added ruff for linting * Update github actions to run ruff, drop testing django 3.2, add testing django 5.0, 5.1, 5.2 * drop testing on Django 4.1, add testing on Django 5.2 and Python 3.13
1 parent 8b10330 commit 7d58e87

File tree

28 files changed

+369
-344
lines changed

28 files changed

+369
-344
lines changed

.github/workflows/run-tests.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ jobs:
1313
runs-on: ubuntu-latest
1414
strategy:
1515
matrix:
16-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
16+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.1"]
17+
django-version: ["django==4.2.*", "django==5.0.*", "django==5.1.*", "django==5.2.*"]
18+
exclude:
19+
- python-version: "3.12"
20+
django-version: "django<4.2.8"
21+
- python-version: "3.13.0-rc.1"
22+
django-version: "django<5.2"
1723
steps:
1824
- uses: actions/checkout@v3
1925
- name: Set up Python ${{ matrix.python-version }}
@@ -24,11 +30,9 @@ jobs:
2430
run: |
2531
python -m pip install --upgrade pip
2632
pip install -r requirements_test.txt
27-
- name: Lint with flake8
33+
- name: Lint with ruff
2834
run: |
2935
# stop the build if there are Python syntax errors or undefined names
30-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
31-
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
32-
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
36+
make ruff-check
3337
- name: Run Tox
3438
run: tox

.style.yapf

Lines changed: 0 additions & 9 deletions
This file was deleted.

Dockerfile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.7-buster AS dev
1+
FROM python:3.8-bookworm AS dev
22
# Dockerfile for running a test django installation
33
LABEL maintainer="Justin Michalicek <[email protected]>"
44
ENV PYTHONUNBUFFERED 1
@@ -8,7 +8,6 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && DEBIAN_FRONTEND=noninteract
88
software-properties-common \
99
sudo \
1010
vim \
11-
telnet \
1211
postgresql-client \
1312
&& apt-get autoremove && apt-get clean
1413

Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ setup-and-run: setup migrate run
7272

7373
venv:
7474
python -m venv .venv
75+
pip install --upgrade pip wheel setuptools
7576

7677
run:
7778
python manage.py runserver 0.0.0.0:8000
@@ -81,8 +82,21 @@ migrate:
8182

8283
dev:
8384
docker compose run --service-ports django /bin/bash
85+
8486
shell:
8587
docker compose exec django /bin/bash
8688

8789
install-mailviewer:
8890
pip install -e /django/mailviewer --no-binary :all:
91+
92+
black:
93+
black django_mail_viewer tests test_project
94+
95+
black-check:
96+
black --check --diff django_mail_viewer tests test_project
97+
98+
ruff:
99+
ruff check django_mail_viewer --fix
100+
101+
ruff-check:
102+
ruff check django_mail_viewer

django_mail_viewer/apps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33

44
class DjangoMailViewerConfig(AppConfig):
5-
name = 'django_mail_viewer'
5+
name = "django_mail_viewer"
66
verbose_name = "Django Mail Viewer"

django_mail_viewer/backends/cache.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Backend for test environment.
33
"""
4+
45
from contextlib import contextmanager
56
from os import getpid
67
from time import monotonic, sleep
@@ -27,14 +28,14 @@ def __init__(self, *args, **kwargs):
2728
# This is for get_outbox() so that the system knows which cache keys are there
2829
# to retrieve them. Django does not have a built in way to get the keys
2930
# which exist in the cache.
30-
self.cache_keys_key = 'message_keys'
31-
self.cache_keys_lock_key = 'message_keys_lock'
31+
self.cache_keys_key = "message_keys"
32+
self.cache_keys_lock_key = "message_keys_lock"
3233

3334
def send_messages(self, messages):
3435
msg_count = 0
3536
for message in messages:
3637
m = message.message()
37-
message_id = m.get('message-id')
38+
message_id = m.get("message-id")
3839
self.cache.set(message_id, m)
3940

4041
# Use a lock key and spinlock
@@ -57,7 +58,7 @@ def send_messages(self, messages):
5758
msg_count += 1
5859
is_stored = True
5960
else:
60-
sleep(.01)
61+
sleep(0.01)
6162
return msg_count
6263

6364
def get_message(self, lookup_id):

django_mail_viewer/backends/database/admin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55

66
class EmailMessageAdmin(admin.ModelAdmin):
7-
list_display = ('pk', 'parent', 'message_id', 'created_at', 'updated_at')
8-
search_fields = ('pk', 'message_id', 'message_headers')
9-
readonly_fields = ('created_at', 'updated_at')
7+
list_display = ("pk", "parent", "message_id", "created_at", "updated_at")
8+
search_fields = ("pk", "message_id", "message_headers")
9+
readonly_fields = ("created_at", "updated_at")
1010

1111

1212
admin.site.register(EmailMessage, EmailMessageAdmin)

django_mail_viewer/backends/database/apps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44

55
class DatabaseBackendConfig(AppConfig):
6-
label = 'mail_viewer_database_backend'
7-
name = 'django_mail_viewer.backends.database'
6+
label = "mail_viewer_database_backend"
7+
name = "django_mail_viewer.backends.database"
88
# May want to change this verbose_name
99
verbose_name = _("Database Backend")

django_mail_viewer/backends/database/backend.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ def _parse_email_attachment(self, message, decode_file=True):
6262
elif name == "read-date":
6363
attachment.read_date = value # TODO: datetime
6464
return {
65-
'filename': Path(message.get_filename()).name,
66-
'content_type': message.get_content_type(),
67-
'file': attachment,
65+
"filename": Path(message.get_filename()).name,
66+
"content_type": message.get_content_type(),
67+
"file": attachment,
6868
}
6969
return None
7070

@@ -76,30 +76,30 @@ def send_messages(self, messages):
7676
if message.is_multipart():
7777
# TODO: Should this really be done recursively? I believe forwarded emails may
7878
# have multiple layers of parts/dispositions
79-
message_id = message.get('message-id')
79+
message_id = message.get("message-id")
8080
main_message = None
8181
for i, part in enumerate(message.walk()):
8282
content_type = part.get_content_type()
83-
charset = part.get_param('charset')
83+
charset = part.get_param("charset")
8484
# handle attachments - probably need to look at SingleEmailMixin._parse_email_attachment()
8585
# and make that more reusable
8686
content_disposition = part.get("Content-Disposition", None)
8787
if content_disposition:
8888
# attachment_data = part.get_payload(decode=True)
8989
attachment_data = self._parse_email_attachment(part)
9090
file_attachment = ContentFile(
91-
attachment_data.get('file').read(), name=attachment_data.get('filename', 'attachment')
91+
attachment_data.get("file").read(), name=attachment_data.get("filename", "attachment")
9292
)
93-
content = ''
94-
elif content_type in ['text/plain', 'text/html']:
95-
content = part.get_payload(decode=True).decode(charset, errors='replace')
96-
file_attachment = ''
93+
content = ""
94+
elif content_type in ["text/plain", "text/html"]:
95+
content = part.get_payload(decode=True).decode(charset, errors="replace")
96+
file_attachment = ""
9797
else:
9898
# the main multipart/alternative message for multipart messages has no content/payload
9999
# TODO: handle file attachments
100-
content = ''
101-
file_attachment = ''
102-
message_id = part.get('message-id', '') # do sub-parts have a message-id?
100+
content = ""
101+
file_attachment = ""
102+
message_id = part.get("message-id", "") # do sub-parts have a message-id?
103103
p = self._backend_model(
104104
message_id=message_id,
105105
content=content,
@@ -111,7 +111,7 @@ def send_messages(self, messages):
111111
if i == 0:
112112
main_message = p
113113
else:
114-
message_id = message.get('message-id')
114+
message_id = message.get("message-id")
115115
main_message = self._backend_model(
116116
message_id=message_id,
117117
content=message.get_payload(),

django_mail_viewer/backends/database/models.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ class AbstractBaseEmailMessage(models.Model):
1919
# Technically optional, but really should be there according to RFC 5322 section 3.6.4
2020
# and Django always creates the message_id on the main part of the message so we know
2121
# it will be there, but not for all sub-parts of a multi-part message
22-
message_id = models.CharField(max_length=250, blank=True, default='')
22+
message_id = models.CharField(max_length=250, blank=True, default="")
2323
# Would love to make message_headers be a JSONField, but do not want to tie this to
2424
# postgres only.
2525
message_headers = models.TextField()
26-
content = models.TextField(blank=True, default='')
26+
content = models.TextField(blank=True, default="")
2727
parent = models.ForeignKey(
28-
'self', blank=True, null=True, default=None, related_name='parts', on_delete=models.CASCADE
28+
"self", blank=True, null=True, default=None, related_name="parts", on_delete=models.CASCADE
2929
)
3030
created_at = models.DateTimeField(auto_now_add=True)
3131
updated_at = models.DateTimeField(auto_now=True)
@@ -38,8 +38,8 @@ def __init__(self, *args, **kwargs):
3838

3939
# methods here expect the concrete subclasses to implement the file_attachment field
4040
# should only be necessary until django 2.2 support is dropped... I hope
41-
if TYPE_CHECKING and not hasattr(self, 'file_attachment'):
42-
self.file_attachment = models.FileField(blank=True, default='', upload_to='mailviewer_attachments')
41+
if TYPE_CHECKING and not hasattr(self, "file_attachment"):
42+
self.file_attachment = models.FileField(blank=True, default="", upload_to="mailviewer_attachments")
4343

4444
# I really only need/use get_filename(), get_content_type(), get_payload(), walk()
4545
# returns Any due to failobj
@@ -59,14 +59,14 @@ def get(self, attr: str, failobj: Any = None) -> Any:
5959
return failobj
6060

6161
def date(self) -> str:
62-
return self.get('date')
62+
return self.get("date")
6363

6464
def is_multipart(self) -> bool:
6565
"""
6666
Returns True if the message is multipart
6767
"""
6868
# Not certain the self.parts.all() is accurate
69-
return self.get_content_type() == 'rfc/822' or self.parts.exists() # type: ignore
69+
return self.get_content_type() == "rfc/822" or self.parts.exists() # type: ignore
7070

7171
def headers(self) -> Dict[str, str]:
7272
"""
@@ -81,13 +81,13 @@ def values(self) -> Dict[str, str]:
8181
# not sure this is right...
8282
return self.headers()
8383

84-
def walk(self) -> 'Union[models.QuerySet[AbstractBaseEmailMessage], List[AbstractBaseEmailMessage]]':
84+
def walk(self) -> "Union[models.QuerySet[AbstractBaseEmailMessage], List[AbstractBaseEmailMessage]]":
8585
if not self.parts.all().exists(): # type: ignore
8686
# Or should I be saving a main message all the time and even just a plaintext has a child part, hmm
8787
return [self]
88-
return self.parts.all().order_by('-created_at', 'id') # type: ignore
88+
return self.parts.all().order_by("-created_at", "id") # type: ignore
8989

90-
def get_param(self, param: str, failobj=None, header: str = 'content-type', unquote: bool = True) -> str:
90+
def get_param(self, param: str, failobj=None, header: str = "content-type", unquote: bool = True) -> str:
9191
"""
9292
Return the value of the Content-Type header’s parameter param as a string. If the message has no Content-Type header or if there is no such parameter, then failobj is returned (defaults to None).
9393
@@ -97,24 +97,24 @@ def get_param(self, param: str, failobj=None, header: str = 'content-type', unqu
9797
# TODO: error handling skipped for sure here... need to see what the real email message does
9898
# Should also consider using cgi.parse_header
9999
h = self.get(header)
100-
params = h.split(';')
100+
params = h.split(";")
101101
for part in params[1:]:
102-
part_name, part_val = part.split('=')
102+
part_name, part_val = part.split("=")
103103
part_name = part_name.strip()
104104
part_val = part_val.strip()
105105
if part_name == param:
106106
return part_val
107-
return ''
107+
return ""
108108

109109
def get_payload(
110110
self, i: Union[int, None] = None, decode: bool = False
111-
) -> 'Union[bytes, AbstractBaseEmailMessage, models.QuerySet[AbstractBaseEmailMessage]]':
111+
) -> "Union[bytes, AbstractBaseEmailMessage, models.QuerySet[AbstractBaseEmailMessage]]":
112112
"""
113113
Temporary backwards compatibility with email.message.Message
114114
"""
115115
# TODO: sort out type hint for return value here. Maybe use monkeytype to figure this out.
116116
if not self.is_multipart():
117-
charset = self.get_param('charset')
117+
charset = self.get_param("charset")
118118
if self.file_attachment:
119119
self.file_attachment.seek(0)
120120
try:
@@ -134,18 +134,18 @@ def get_content_type(self) -> str:
134134
"""
135135
Return's the message's content-type or mime type.
136136
"""
137-
h = self.get('content-type')
138-
params = h.split(';')
137+
h = self.get("content-type")
138+
params = h.split(";")
139139
return params[0]
140140

141141
def get_filename(self, failobj=None) -> str:
142-
content_disposition = self.headers().get('Content-Disposition', '')
143-
parts = content_disposition.split(';')
142+
content_disposition = self.headers().get("Content-Disposition", "")
143+
parts = content_disposition.split(";")
144144
for part in parts:
145-
if part.strip().startswith('filename'):
146-
filename = part.split('=')[1].strip('"').strip()
145+
if part.strip().startswith("filename"):
146+
filename = part.split("=")[1].strip('"').strip()
147147
return email.utils.unquote(filename)
148-
return ''
148+
return ""
149149

150150

151151
class EmailMessage(AbstractBaseEmailMessage):
@@ -160,9 +160,9 @@ class EmailMessage(AbstractBaseEmailMessage):
160160
it just needs to be stored elsewhere, such as locally, or a different s3 bucket than the default storage.
161161
"""
162162

163-
file_attachment = models.FileField(blank=True, default='', upload_to='mailviewer_attachments')
163+
file_attachment = models.FileField(blank=True, default="", upload_to="mailviewer_attachments")
164164

165165
class Meta:
166-
db_table = 'mail_viewer_emailmessage'
167-
ordering = ('id',)
168-
indexes = [models.Index(fields=['message_id'])]
166+
db_table = "mail_viewer_emailmessage"
167+
ordering = ("id",)
168+
indexes = [models.Index(fields=["message_id"])]

0 commit comments

Comments
 (0)