Skip to content

Commit 951ea0b

Browse files
author
Vlada Macek
committedJul 8, 2022
Code, doc and compatibility cleanups.
1 parent f2fbfab commit 951ea0b

37 files changed

+65
-107
lines changed
 

‎.flake8

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[flake8]
2+
max-line-length = 120
3+
select = C,E,F,W,B,B9
4+
#ignore = E203, E501, W503
5+
ignore = W503
6+
exclude = build/*
7+
#migrations/*,tests_mixulo/*,mixulo/settings/*

‎CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
## [dev]
44

55
## [0.9.0]
6-
- Added support for Django 4.0
7-
- Dropped support for Django <3.2
86
- Dropped support for Python 2.x
7+
- Dropped support for Django <3.2
8+
- Added support for Django 4.0
99
- Fixed CachedAuthenticationMiddleware to comply with Django system check framework
1010

1111
## [0.8.8]

‎README.rst

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ python-useful
55
A collection of utilities of everyday use when writing
66
Python 3.x code *or* Django 3.2+ projects.
77

8+
**Python 2.x support has been dropped.**
9+
810
Overview
911
--------
1012

@@ -27,11 +29,7 @@ Install either using pip::
2729

2830
pip install useful
2931

30-
Or using e-i::
31-
32-
easy_install useful
33-
34-
Or from source::
32+
or from source::
3533

3634
git clone https://github.com/tuttle/python-useful src/useful
3735
cd src/useful

‎setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"License :: OSI Approved :: BSD License",
2727
"Operating System :: OS Independent",
2828
"Programming Language :: Python",
29-
"Programming Language :: Python :: 2",
3029
"Programming Language :: Python :: 3",
3130
"Topic :: Software Development :: Libraries",
3231
],

‎tests/settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from django.conf.global_settings import *
1+
from django.conf.global_settings import * # noqa
22

33

44
DATABASES = {
55
"default": {
66
"ENGINE": "django.db.backends.sqlite3",
7-
"NAME": "mem_db"
7+
"NAME": "mem_db",
88
}
99
}
1010

@@ -16,7 +16,7 @@
1616
"django.contrib.sessions",
1717
"django.contrib.sites",
1818
"useful.django",
19-
"tests.testapp.apps.TestAppConfig"
19+
"tests.testapp.apps.TestAppConfig",
2020
]
2121

2222
SECRET_KEY = 'justfortests'

‎tests/test_auth.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
21
from django.contrib.auth import authenticate
32
from django.contrib.auth.models import User
4-
from django.test import TestCase
5-
from django.test import override_settings
3+
from django.test import TestCase, override_settings
64

75

86
@override_settings(AUTHENTICATION_BACKENDS=['useful.django.auth.EmailLoginModelBackend', ])

‎tests/test_cached_auth.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.core.management import call_command
32
from django.test import TestCase
43
from django.test import override_settings

‎tests/test_caching.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.test import TestCase
32
from django.utils.crypto import get_random_string
43

‎tests/test_getters.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.contrib.auth.models import Group, User
32
from django.test import TestCase
43

‎tests/test_logentry.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import json
32

43
from django import VERSION

‎tests/test_templatetags.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.http import QueryDict
32
from django.template.loader import render_to_string
43
from django.test import TestCase

‎tests/test_urls.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
21
from django.conf import settings
3-
from django.contrib.auth.models import Permission
4-
from django.contrib.auth.models import User
2+
from django.contrib.auth.models import Permission, User
53
from django.contrib.contenttypes.models import ContentType
6-
from django.test import TestCase
7-
from django.test import override_settings
4+
from django.test import TestCase, override_settings
85
from django.urls import reverse
96

107
TEMPLATES = [

‎tests/test_views.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.test import TestCase
32
from django.urls import reverse
43

‎tests/urls.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.urls import include
32
from django.urls import path
43

‎useful/attrdict.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from collections import OrderedDict
32

43

‎useful/consistent_hash.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import bisect
32
import hashlib
43

‎useful/daemontools.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import atexit
2-
import grp
32
import logging.handlers
3+
import grp
44
import os
55
import pwd
66
import signal
@@ -59,7 +59,7 @@ def sig_handler(signum, frame):
5959
if s not in (signal.SIGCHLD, signal.SIGURG, signal.SIGWINCH):
6060
try:
6161
signal.signal(s, sig_handler)
62-
except:
62+
except: # noqa
6363
pass
6464

6565

@@ -88,7 +88,7 @@ def exit_function(pidfile_path_):
8888
try:
8989
logging.debug("Removing PID file %s." % pidfile_path_)
9090
os.unlink(pidfile_path_)
91-
except:
91+
except: # noqa
9292
logging.error("Cannot remove PID file %s." % pidfile_path_)
9393

9494
if os.path.exists(pidfile_path):

‎useful/django/auth.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
1+
from django.contrib.auth import get_user_model
22
from django.contrib.auth.backends import ModelBackend
3-
from django.core.validators import validate_email, ValidationError
3+
from django.core.validators import ValidationError, validate_email
44
from django.utils.crypto import get_random_string
5-
from django.contrib.auth import get_user_model
65

76
from useful.django.getters import get_object_or_none
87

9-
108
UserModel = get_user_model()
119

1210

@@ -56,7 +54,7 @@ def get_user_by_email(self, email):
5654

5755
return user
5856

59-
def authenticate(self, request, username=None, password=None):
57+
def authenticate(self, request, username=None, password=None, **kwargs):
6058
user = self.get_user_by_email(username)
6159
return user if user and user.check_password(password) else None
6260

@@ -71,7 +69,7 @@ class UsernameOrEmailLoginModelBackend(EmailLoginModelBackend):
7169
"""
7270
USERNAME_CASE_SENSITIVE = False
7371

74-
def authenticate(self, request, username=None, password=None):
72+
def authenticate(self, request, username=None, password=None, **kwargs):
7573
if self.USERNAME_CASE_SENSITIVE:
7674
user = get_object_or_none(UserModel, username__exact=username)
7775
else:

‎useful/django/cached_auth.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,28 @@
2525
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2626
"""
2727

28+
from django.apps import apps
2829
from django.conf import settings
29-
from django.contrib.auth import SESSION_KEY, get_user
30+
from django.contrib.auth import SESSION_KEY, get_user, get_user_model
3031
from django.contrib.auth.middleware import AuthenticationMiddleware
31-
from django.contrib.auth import get_user_model
3232
from django.core import cache # Importing this way so debug_toolbar can patch it later.
33-
from django.db import models
3433
from django.db.models.signals import post_delete, post_save
3534
from django.utils.functional import SimpleLazyObject
3635

37-
3836
UserModel = get_user_model()
3937

4038
CACHE_KEY = 'cached_auth_middleware_1.5:%s'
4139

40+
# The profile model support have been deprecated in Django long time ago.
41+
# Nevertheless some projects might still be using it.
4242
try:
4343
app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
44-
profile_model = models.get_model(app_label, model_name)
44+
profile_model = apps.get_model(app_label, model_name)
4545
except (ValueError, AttributeError):
4646
profile_model = None
4747

4848

49+
# noinspection PyUnusedLocal
4950
def invalidate_cache(sender, instance, **kwargs):
5051
if isinstance(instance, UserModel):
5152
key = CACHE_KEY % instance.id
@@ -80,6 +81,7 @@ def get_cached_user(request):
8081

8182
request._cached_user = user
8283

84+
# noinspection PyProtectedMember
8385
return request._cached_user
8486

8587

‎useful/django/caching.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
2-
from functools import wraps
31
import hashlib
2+
from functools import wraps
43

54
from django.core import cache # Importing this way so debug_toolbar can patch it later.
65
from django.core.cache.backends.base import DEFAULT_TIMEOUT
76

8-
97
ALLOWED_CACHED_FUNCTION_ARG_TYPES = {type(None), int, float, bool, str, bytes}
108

119

@@ -41,16 +39,16 @@ def expensive(arg1, arg2, arg3):
4139
@cached_function(num_args_to_key=2)
4240
4341
"""
44-
def cached_function_inner(func):
45-
if func.__defaults__:
42+
def cached_function_inner(fn):
43+
if getattr(fn, '__defaults__'):
4644
raise RuntimeError("cached_function does not allow function to have default args "
4745
"in order to keep the number of passed args fixed.")
4846

49-
if func.__name__ == '<lambda>':
47+
if getattr(fn, '__name__') == '<lambda>':
5048
raise RuntimeError("cached_function can't work on anonymous funcs in order "
5149
"to keep the name unique.")
5250

53-
@wraps(func)
51+
@wraps(fn)
5452
def wrapper(*args):
5553
key_args = args if num_args_to_key is None else args[:num_args_to_key]
5654

@@ -59,7 +57,7 @@ def wrapper(*args):
5957
raise RuntimeError("cached_function received unallowed types of keyable args: %s"
6058
% ', '.join(map(str, unallowed_types)))
6159

62-
key_fmt = 'cached_function:%s.%s(%%s)' % (func.__module__, func.__name__)
60+
key_fmt = 'cached_function:%s.%s(%%s)' % (fn.__module__, fn.__name__)
6361
key_args_repr = ','.join(map(repr, key_args))
6462
key = key_fmt % key_args_repr
6563

@@ -68,13 +66,14 @@ def wrapper(*args):
6866
key_args_bytestring = key_args_repr.encode('utf-8')
6967
else:
7068
key_args_bytestring = key_args_repr
69+
7170
key_args = hashlib.sha256(key_args_bytestring).hexdigest()
7271
key = key_fmt % key_args + '-hashed'
7372

7473
value = cache.cache.get(key)
7574

7675
if value is None:
77-
value = func(*args)
76+
value = fn(*args)
7877

7978
cache.cache.set(key, value, timeout=timeout)
8079

‎useful/django/can_url.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
21
from functools import lru_cache
32

43
from django.core.exceptions import PermissionDenied
5-
from django.urls import NoReverseMatch
6-
from django.urls import get_callable
7-
from django.urls import get_resolver
8-
from django.urls import URLResolver
4+
from django.urls import NoReverseMatch, URLResolver, get_callable, get_resolver
95
from django.urls.base import get_urlconf
106

117

‎useful/django/concurrency.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
21
import hashlib
32

4-
from django import VERSION
53
from django import forms
64
from django.contrib.auth.forms import UserChangeForm
75
from django.forms.utils import ErrorList
6+
from django.utils.crypto import constant_time_compare, salted_hmac
87
from django.utils.encoding import force_str, smart_str
98
from django.utils.safestring import mark_safe
10-
from django.utils.crypto import salted_hmac, constant_time_compare
119
from django.utils.translation import gettext_lazy as _
1210

13-
14-
1511
CONCURRENCY_EDIT_MESSAGE = _(
1612
"<br><b>WARNING:</b> Your changes are almost LOST now and can't be saved.<br><em>But you still can copy the values "
1713
"out manually now.</em><br>Then you can <a href=''>RELOAD</a> and edit again."
@@ -45,14 +41,17 @@ def _clean_object_version(slf):
4541
raise RuntimeError(_("Found not matching primary key in object_version field."))
4642

4743
if not slf.concurrency_problem:
44+
# noinspection PyProtectedMember
4845
db_obj = slf._meta.model.objects.get(pk=pk)
4946
db_hash = _gen_object_hash(db_obj)
5047
if form_hash != db_hash:
48+
# noinspection PyProtectedMember
5149
slf.concurrency_problem = \
5250
_("In the mean time, someone <b>changed</b> this item in the database.") \
5351
+ ' ' + force_str(slf._CONCURRENCY_EDIT_MESSAGE)
5452

5553
if slf.concurrency_problem:
54+
# noinspection PyProtectedMember
5655
errors = slf._errors.setdefault(forms.forms.NON_FIELD_ERRORS, ErrorList())
5756
errors.append(mark_safe(slf.concurrency_problem))
5857

‎useful/django/crypto.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
from django import VERSION
31
from django.utils.crypto import constant_time_compare, salted_hmac
42
from django.utils.encoding import smart_str
53

‎useful/django/db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.db import connection
32

43

@@ -7,6 +6,7 @@ def truncate_table(model_or_tablename, cascade=False, cursor=None):
76
cursor = connection.cursor()
87

98
if hasattr(model_or_tablename, '_meta'):
9+
# noinspection PyProtectedMember
1010
tablename = model_or_tablename._meta.db_table
1111
else:
1212
tablename = model_or_tablename

‎useful/django/getters.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from collections import defaultdict
32

43
from django.db import connection

‎useful/django/i18n.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import re
32

43
from django.conf import settings

‎useful/django/logentry.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
from django import VERSION
31
from django.contrib import admin
42
from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry
53
from django.contrib.admin.utils import construct_change_message

‎useful/django/readonly_admin.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.contrib import admin
32
from django.core.exceptions import PermissionDenied
43

‎useful/django/remote_addr.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.conf import settings
32
from django.core.exceptions import ImproperlyConfigured
43

@@ -24,7 +23,7 @@ class SetRemoteAddrMiddleware:
2423
"""
2524
def __init__(self, get_response):
2625
self.get_response = get_response
27-
26+
2827
def __call__(self, request):
2928
try:
3029
real_ip_varname = settings.REAL_IP_META_VARIABLE_NAME

‎useful/django/templatetags/freshstatic.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import os
32
import posixpath
43
import stat
@@ -8,7 +7,6 @@
87
from django.conf import settings
98
from django.contrib.staticfiles import finders, storage
109

11-
1210
register = template.Library()
1311

1412
STATIC_URL_CACHE = {}

‎useful/django/templatetags/useful.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
21
import os
32
import re
43
import urllib.parse
54

6-
from django import VERSION, template
5+
from django import template
76
from django.template.defaultfilters import stringfilter
7+
from django.utils.encoding import force_str
88
from django.utils.html import urlize
99
from django.utils.safestring import mark_safe
10-
from django.utils.encoding import force_str
11-
1210

1311
register = template.Library()
1412

‎useful/django/urlpatterns.py

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
1-
2-
from functools import wraps
3-
from functools import WRAPPER_ASSIGNMENTS
41
import re
2+
from functools import wraps
53
from urllib.parse import urlparse
64

75
from django.conf import settings
8-
from django.urls import re_path
96
from django.contrib.auth import REDIRECT_FIELD_NAME
107
from django.core.exceptions import PermissionDenied
118
from django.shortcuts import resolve_url
9+
from django.urls import re_path
1210
from django.utils.encoding import force_str
13-
from sys import version_info
14-
1511

16-
def available_attrs(fn):
17-
"""
18-
Return the list of functools-wrappable attributes on a callable.
19-
This is required as a workaround for http://bugs.python.org/issue3445
20-
under Python 2.
21-
TODO: still relevant in py3?
22-
"""
23-
return WRAPPER_ASSIGNMENTS
12+
NOT_GIVEN = object()
2413

2514

2615
class UrlPatterns(list):
@@ -175,7 +164,7 @@ def __init__(self, login_url=None, raise_exception=False, redirect_field_name=No
175164
self.raise_exception = raise_exception
176165
self.redirect_field_name = redirect_field_name or REDIRECT_FIELD_NAME
177166

178-
def url(self, regex, kwargs=None, name=(), perms=None):
167+
def url(self, regex, kwargs=None, name=NOT_GIVEN, perms=None):
179168
def decorator(view_func):
180169
if perms is None:
181170
_wrapped = view_func
@@ -191,7 +180,7 @@ def __can_url(user):
191180
_wrapped.can_url_perms_compiled = compiled_anded_perms
192181

193182
# Assigning different name to avoid changing the nonlocal variable.
194-
url_name = view_func.__name__ if name is () else name
183+
url_name = view_func.__name__ if name is NOT_GIVEN else name
195184
self.append(re_path(regex, _wrapped, kwargs=kwargs, name=url_name))
196185

197186
return _wrapped
@@ -223,7 +212,7 @@ def user_passes_test(self, test_func):
223212
Extended with passing the resolved aliased permissions to the view.
224213
"""
225214
def decorator(view_func):
226-
@wraps(view_func, assigned=available_attrs(view_func))
215+
@wraps(view_func)
227216
def _wrapped_view(request, *args, **kwargs):
228217
perm_aliases = test_func(request.user)
229218
if perm_aliases:
@@ -242,8 +231,8 @@ def _wrapped_view(request, *args, **kwargs):
242231
# use the path as the "next" url.
243232
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
244233
current_scheme, current_netloc = urlparse(path)[:2]
245-
if ((not login_scheme or login_scheme == current_scheme) and
246-
(not login_netloc or login_netloc == current_netloc)):
234+
if ((not login_scheme or login_scheme == current_scheme)
235+
and (not login_netloc or login_netloc == current_netloc)):
247236
path = request.get_full_path()
248237
from django.contrib.auth.views import redirect_to_login
249238
return redirect_to_login(path, resolved_login_url,

‎useful/django/views.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
1+
import functools
22
import os
33
import time
4-
import functools
54

65
import django
76
from django import shortcuts
87
from django.conf import settings
9-
from django.core.paginator import Paginator, EmptyPage, InvalidPage
10-
from django.urls import reverse
11-
from django.http.response import HttpResponse, HttpResponseBase, HttpResponseRedirect, HttpResponseBadRequest
8+
from django.core.paginator import EmptyPage, InvalidPage, Paginator
9+
from django.http.response import HttpResponse, HttpResponseBadRequest, HttpResponseBase, HttpResponseRedirect
1210
from django.template import RequestContext
11+
from django.urls import reverse
1312

1413
from .crypto import SecretTokenGenerator
1514

@@ -62,7 +61,7 @@ def page_decorator_inner_wrapper(request, *args, **kw):
6261
data.update(response_dict)
6362

6463
# The view can override the template to use.
65-
template_name = data.get('template', template)
64+
template_name = data.get('template', template)
6665

6766
# By adding the debug_template parameter we switch to possible debugging template:
6867
# payments/payments_info.html -> payments/payments_info_debug.html

‎useful/encoding.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import unicodedata
32

43

‎useful/filelock.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
21
import errno
32
import os
43
import sys
54
import time
65
from functools import wraps
76

8-
97
# Based on http://www.evanfosmark.com/2009/01/cross-platform-file-locking-support-in-python/
108

119

‎useful/shortcuts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
# noinspection PyPep8Naming
23
def first(predicate_or_None, iterable, default=None):
34
"""
45
Returns the first item of iterable for which predicate(item) is true.
@@ -18,6 +19,7 @@ def int_or_0(value):
1819
return 0
1920

2021

22+
# noinspection PyPep8Naming
2123
def int_or_None(value):
2224
try:
2325
return int(value)

‎useful/transforms.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import itertools
32
import operator
43
from collections import defaultdict
@@ -82,7 +81,7 @@ def iter_compare_dicts(dict1, dict2, only_common_keys=False, comparison_op=opera
8281

8382
def iter_ibatches(iterable, size):
8483
"""
85-
http://code.activestate.com/recipes/303279-getting-items-in-batches/
84+
https://code.activestate.com/recipes/303279-getting-items-in-batches/
8685
8786
Generates iterators of elements of fixed size from the source iterable. Does not create batch sequences in memory.
8887
The source iterable can be of an unknown arbitrary length, does not need to support anything else than iteration.

0 commit comments

Comments
 (0)
Please sign in to comment.