From b46e7cce01ff7fda7346431722c9903d4304523c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksi=20Ha=CC=88kli?= Date: Sat, 9 Mar 2019 21:18:26 +0200 Subject: [PATCH] Drop Python 3.5 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- axes/helpers.py | 26 ++++++++----------- .../management/commands/axes_list_attempts.py | 6 +---- axes/management/commands/axes_reset.py | 2 +- axes/management/commands/axes_reset_ip.py | 2 +- .../commands/axes_reset_username.py | 2 +- axes/models.py | 4 +-- axes/request.py | 16 +++++------- axes/tests/test_handlers.py | 4 +-- axes/tests/test_utils.py | 12 ++++----- mypy.ini | 2 +- setup.py | 1 - tox.ini | 7 ++--- 12 files changed, 32 insertions(+), 52 deletions(-) diff --git a/axes/helpers.py b/axes/helpers.py index 5af1c93..6b6ca29 100644 --- a/axes/helpers.py +++ b/axes/helpers.py @@ -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', '')[: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 diff --git a/axes/management/commands/axes_list_attempts.py b/axes/management/commands/axes_list_attempts.py index b4ca341..595eca9 100644 --- a/axes/management/commands/axes_list_attempts.py +++ b/axes/management/commands/axes_list_attempts.py @@ -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}') diff --git a/axes/management/commands/axes_reset.py b/axes/management/commands/axes_reset.py index f28c9f4..583a940 100644 --- a/axes/management/commands/axes_reset.py +++ b/axes/management/commands/axes_reset.py @@ -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.') diff --git a/axes/management/commands/axes_reset_ip.py b/axes/management/commands/axes_reset_ip.py index 657a1ac..5ac70b0 100644 --- a/axes/management/commands/axes_reset_ip.py +++ b/axes/management/commands/axes_reset_ip.py @@ -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.') diff --git a/axes/management/commands/axes_reset_username.py b/axes/management/commands/axes_reset_username.py index 2e6ec7c..86646d9 100644 --- a/axes/management/commands/axes_reset_username.py +++ b/axes/management/commands/axes_reset_username.py @@ -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.') diff --git a/axes/models.py b/axes/models.py index 46d8097..d94278a 100644 --- a/axes/models.py +++ b/axes/models.py @@ -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') diff --git a/axes/request.py b/axes/request.py index 158fdc0..649c19b 100644 --- a/axes/request.py +++ b/axes/request.py @@ -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 diff --git a/axes/tests/test_handlers.py b/axes/tests/test_handlers.py index 2fd1d35..f4c1c3e 100644 --- a/axes/tests/test_handlers.py +++ b/axes/tests/test_handlers.py @@ -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( diff --git a/axes/tests/test_utils.py b/axes/tests/test_utils.py index 42324fd..34c3d99 100644 --- a/axes/tests/test_utils.py +++ b/axes/tests/test_utils.py @@ -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( diff --git a/mypy.ini b/mypy.ini index 81bba3f..1cbb0ff 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,5 @@ [mypy] -python_version = 3.5 +python_version = 3.6 ignore_missing_imports = True [mypy-axes.migrations.*] diff --git a/setup.py b/setup.py index e7da5d4..73f4903 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/tox.ini b/tox.ini index fecbe83..e4ed45c 100644 --- a/tox.ini +++ b/tox.ini @@ -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