Drop Python 3.5 support

Most of our users are already running on Python 3.6+
and dropping 3.5 and below in a future oriented release
allows us to focus on implementing more readable codebases.

Fixes #417

Signed-off-by: Aleksi Häkli <aleksi.hakli@iki.fi>
This commit is contained in:
Aleksi Häkli 2019-03-09 21:18:26 +02:00
parent 3bece1aaaa
commit b46e7cce01
No known key found for this signature in database
GPG key ID: 3E7146964D726BBE
12 changed files with 32 additions and 52 deletions

View file

@ -1,4 +1,3 @@
from collections import OrderedDict
from datetime import timedelta
from hashlib import md5
from ipaddress import ip_address
@ -73,10 +72,10 @@ def get_cool_off_iso8601(delta: timedelta) -> str:
hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24)
days_str = '{:.0f}D'.format(days) if days else ''
days_str = f'{days:.0f}D' if days else ''
time_str = ''.join(
'{value:.0f}{designator}'.format(value=value, designator=designator)
f'{value:.0f}{designator}'
for value, designator
in [
[hours, 'H'],
@ -87,11 +86,8 @@ def get_cool_off_iso8601(delta: timedelta) -> str:
)
if time_str:
template = 'P{days_str}T{time_str}'
else:
template = 'P{days_str}'
return template.format(days_str=days_str, time_str=time_str)
return f'P{days_str}T{time_str}'
return f'P{days_str}'
def get_credentials(username: str = None, **kwargs) -> dict:
@ -173,15 +169,15 @@ def get_client_http_accept(request: HttpRequest) -> str:
return request.META.get('HTTP_ACCEPT', '<unknown>')[:1025]
def get_client_parameters(username: str, ip_address: str, user_agent: str) -> OrderedDict:
def get_client_parameters(username: str, ip_address: str, user_agent: str) -> dict:
"""
Get query parameters for filtering AccessAttempt queryset.
This method returns an OrderedDict that guarantees iteration order for keys and values,
This method returns a dict that guarantees iteration order for keys and values,
and can so be used in e.g. the generation of hash keys or other deterministic functions.
"""
filter_kwargs = OrderedDict() # type: OrderedDict
filter_kwargs = dict()
if settings.AXES_ONLY_USER_FAILURES:
# 1. Only individual usernames can be tracked with parametrization
@ -209,7 +205,7 @@ def get_client_str(username: str, ip_address: str, user_agent: str, path_info: s
Example log format would be ``{username: "example", ip_address: "127.0.0.1", path_info: "/example/"}``
"""
client_dict = OrderedDict() # type: OrderedDict
client_dict = dict()
if settings.AXES_VERBOSE:
# Verbose mode logs every attribute that is available
@ -227,7 +223,7 @@ def get_client_str(username: str, ip_address: str, user_agent: str, path_info: s
# Template the internal dictionary representation into a readable and concatenated key: "value" format
template = ', '.join(
'{key}: "{value}"'.format(key=key, value=value)
f'{key}: "{value}"'
for key, value
in client_dict.items()
)
@ -254,7 +250,7 @@ def get_query_str(query: Type[QueryDict], max_length: int = 1024) -> str:
query_dict.pop(settings.AXES_PASSWORD_FORM_FIELD, None)
query_str = '\n'.join(
'{key}={value}'.format(key=key, value=value)
f'{key}={value}'
for key, value
in query_dict.items()
)
@ -381,6 +377,6 @@ def get_client_cache_key(request_or_attempt: Union[HttpRequest, Any], credential
cache_key_components = ''.join(filter_kwargs.values())
cache_key_digest = md5(cache_key_components.encode()).hexdigest()
cache_key = 'axes-{}'.format(cache_key_digest)
cache_key = f'axes-{cache_key_digest}'
return cache_key

View file

@ -8,8 +8,4 @@ class Command(BaseCommand):
def handle(self, *args, **options): # pylint: disable=unused-argument
for obj in AccessAttempt.objects.all():
self.stdout.write('{ip}\t{username}\t{failures_since_start}'.format(
ip=obj.ip_address,
username=obj.username,
failures_since_start=obj.failures_since_start,
))
self.stdout.write(f'{obj.ip_address}\t{obj.username}\t{obj.failures_since_start}')

View file

@ -10,6 +10,6 @@ class Command(BaseCommand):
count = reset()
if count:
self.stdout.write('{0} attempts removed.'.format(count))
self.stdout.write(f'{count} attempts removed.')
else:
self.stdout.write('No attempts found.')

View file

@ -16,6 +16,6 @@ class Command(BaseCommand):
count += reset(ip=ip)
if count:
self.stdout.write('{0} attempts removed.'.format(count))
self.stdout.write(f'{count} attempts removed.')
else:
self.stdout.write('No attempts found.')

View file

@ -16,6 +16,6 @@ class Command(BaseCommand):
count += reset(username=username)
if count:
self.stdout.write('{0} attempts removed.'.format(count))
self.stdout.write(f'{count} attempts removed.')
else:
self.stdout.write('No attempts found.')

View file

@ -57,7 +57,7 @@ class AccessAttempt(AccessBase):
)
def __str__(self):
return 'Attempted Access: {}'.format(self.attempt_time)
return f'Attempted Access: {self.attempt_time}'
class Meta:
verbose_name = _('access attempt')
@ -79,7 +79,7 @@ class AccessLog(AccessBase):
)
def __str__(self):
return 'Access Log for {} @ {}'.format(self.username, self.attempt_time)
return f'Access Log for {self.username} @ {self.attempt_time}'
class Meta:
verbose_name = _('access log')

View file

@ -1,4 +1,4 @@
from datetime import datetime # noqa
from datetime import datetime
from django.http import HttpRequest
@ -8,12 +8,8 @@ class AxesHttpRequest(HttpRequest):
Type definition for the HTTP request Axes uses.
"""
def __init__(self):
super().__init__()
# TODO: Move attribute definitions to class level in Python 3.6+
self.axes_attempt_time = None # type: datetime
self.axes_ip_address = None # type: str
self.axes_user_agent = None # type: str
self.axes_path_info = None # type: str
self.axes_http_accept = None # type: str
axes_attempt_time: datetime
axes_ip_address: str
axes_user_agent: str
axes_path_info: str
axes_http_accept: str

View file

@ -94,9 +94,7 @@ class AxesHandlerTestCase(AxesTestCase):
def check_empty_request(self, log, handler):
AxesProxyHandler.user_login_failed(sender=None, credentials={}, request=None)
log.error.assert_called_with(
'AXES: {handler}.user_login_failed does not function without a request.'.format(handler=handler)
)
log.error.assert_called_with(f'AXES: {handler}.user_login_failed does not function without a request.')
@override_settings(

View file

@ -258,10 +258,10 @@ class ClientCacheKeyTestCase(AxesTestCase):
Test the cache key format.
"""
cache_hash_digest = md5(self.ip_address.encode()).hexdigest()
# Getting cache key from request
cache_hash_key = 'axes-{}'.format(
md5(self.ip_address.encode()).hexdigest()
)
cache_hash_key = f'axes-{cache_hash_digest}'
request_factory = RequestFactory()
request = request_factory.post(
@ -295,10 +295,8 @@ class ClientCacheKeyTestCase(AxesTestCase):
# Getting cache key from request
ip_address = self.ip_address
cache_hash_key = 'axes-{}'.format(
md5(ip_address.encode()).hexdigest()
)
cache_hash_digest = md5(ip_address.encode()).hexdigest()
cache_hash_key = f'axes-{cache_hash_digest}'
request_factory = RequestFactory()
request = request_factory.post(

View file

@ -1,5 +1,5 @@
[mypy]
python_version = 3.5
python_version = 3.6
ignore_missing_imports = True
[mypy-axes.migrations.*]

View file

@ -55,7 +55,6 @@ setup(
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: Log Analysis',

View file

@ -1,12 +1,9 @@
[tox]
envlist =
py{py3,35,36,37}-django{111,20,21}
py{36,37}-djangomaster
py{36,37}-django{111,20,21,master}
[travis]
python =
pypy3.5: pypy3
3.5: py35
3.6: py36
3.7: py37
@ -28,6 +25,6 @@ usedevelop = True
commands =
pytest
prospector
py{35,36,37}: mypy axes
py{36,37}: mypy axes
setenv =
PYTHONDONTWRITEBYTECODE=1