Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
include MANIFEST.in
include README.rst
include setup.py
include LICENSE

recursive-include useful *

global-exclude *.pyc
global-exclude *.pyo
include README.rst
recursive-include useful/django/locale *.po *.mo
global-exclude __pycache__ *.py[cod] .DS_Store
11 changes: 3 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ python-useful
=============

A collection of utilities of everyday use when writing
Python 3.x code *or* Django 3.2+ projects.
Python 3.10+ code *or* Django 4.2+ projects.

**Python 2.x support has been dropped.**

Expand All @@ -25,16 +25,10 @@ in the docstring of each utility.
Installation
------------

Install either using pip::
Using pip::

pip install useful

or from source::

git clone https://github.com/tuttle/python-useful src/useful
cd src/useful
python setup.py develop

If you are about to use the Django features like the templatetags,
you can add to ``INSTALLED_APPS``::

Expand All @@ -44,4 +38,5 @@ you can add to ``INSTALLED_APPS``::
)

Please file an Issue on GitHub if you have bugreport, tip, anything.

Thank you!
9 changes: 0 additions & 9 deletions conftest.py

This file was deleted.

171 changes: 171 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
[build-system]
requires = ["setuptools>=70", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "useful"
dynamic = ["version"]
description = "Everyday utilities for writing Python code or Django projects."
readme = { file = "README.rst", content-type = "text/x-rst" }
requires-python = ">=3.10"
authors = [{ name = "Vlada Macek", email = "[email protected]" }]
license = { file = "LICENSE" }
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.2",
"Topic :: Software Development :: Libraries",
]

[project.optional-dependencies]
django = ["Django>=4.2,<6.0"]
test = ["pytest>=8.2", "pytest-django>=4.8"]
dev = ["ruff>=0.5.0", "tox>=4.14", "tox-uv>=1.8", "pylint>=3.2", "pylint-django>=2.6"]

[project.urls]
Homepage = "https://github.com/tuttle/python-useful"
"Bug Tracker" = "https://github.com/tuttle/python-useful/issues"

[tool.setuptools]
include-package-data = true

[tool.setuptools.dynamic]
version = { attr = "useful.__versionstr__" }

[tool.setuptools.package-data]
"useful" = ["django/locale/**/LC_MESSAGES/django.*"]

[tool.setuptools.packages.find]
include = ["useful*"]
exclude = ["tests*"]

# pytest-django: point tests at your Django settings module.
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "tests.settings"
addopts = "-ra"
testpaths = ["tests"]
# Mirror your previous config:
django_find_project = false

# Ruff: linter + formatter.
[tool.ruff]
target-version = "py310"
line-length = 120
src = ["useful", "tests"]

# Ruff will use unstable rules (E303 Too many blank lines, E241 Multiple spaces after comma), fixes, and formatting.
# After these interesting rules become stable, we can turn the preview mode off.
preview = true

[tool.ruff.lint]

# ---- 1 The core set ---------------------------------------------------------
# *Strictly* correctness, parity with stock Flake8, and import sorting
select = [
"F", # Pyflakes - undefined names, unused vars, etc. (must-have)
"E", # pycodestyle *errors* (not warnings)
"W", # pycodestyle *warnings* (line-ending whitespace, etc.)
"PGH", # pygrep-hooks - A collection of fast, cheap, regex based pre-commit hooks.
]

# ---- 2 Low-risk “quality” rules --------------------------------------------
# Things most teams enable on day 1 because fixes are mostly automatic
extend-select = [
"DJ", # flake8-django - Django specific checks
"I", # isort replacement - deterministic imports
"UP", # pyupgrade - modern syntax / deprecation fixes
"B", # flake8-bugbear - likely bugs & bad patterns
"C90", # mccabe complexity - keeps functions honest
"N", # pep8-naming - class_/func_ name consistency
"S", # flake8-bandit - basic security red flags
"C4", # flake8-comprehensions - C-style loops
"PERF", # Perflint - tiny perf wins
"PLE", # Pylint (PL) Errors
]

# ---- 3 Optional but popular for larger orgs ---------------------------------
# Enable *after* the first green CI, usually one group at a time
# ANN → require & check type hints
# D → pydocstyle (or DOC) - doc-string conventions
# PGH → pre-commit hook helpers
# …and whatever domain-specific sets you care about (PD, NPY, DJ, AIR, etc.)
#
# Example:
# extend-select = ["ANN", "D", "S", "PERF"]

ignore = [
"Q000", # Single quotes found but double quotes preferred
"UP031", # Use format specifiers instead of percent format
"N802", # Function name should be lowercase
"N803", # Argument name should be lowercase
# "E722", # Do not use bare `except`
# "N818", # Exception name `BadParameter` should be named with an Error suffix
# "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling"
# "UP031", # Use format specifiers instead of percent format"
# "S605", # Starting a process with a shell, possible injection detected
# "S403", # `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
# "C408", # Unnecessary `dict()` call (rewrite as a literal)
]

[tool.ruff.lint.per-file-ignores]

[tool.ruff.lint.mccabe]
max-complexity = 15

[tool.ruff.format]
# Keep defaults; add options if you care (e.g., "quote-style = 'single'")

[tool.pylint.MASTER]

load-plugins = ["pylint_django"]
# Let pylint-django know which settings to use (same module you use for pytest)
"django-settings-module" = "tests.settings"
# Speed up on multicore
jobs = 0
ignore = ["__pycache__"]

[tool.pylint.messages_control]
disable = [
"C0103", # invalid-name: Constant name "..." doesn't conform to UPPER_CASE naming style
"C0114", # missing-module-docstring: Missing module docstring
"C0115", # missing-class-docstring: Missing class docstring
"C0116", # missing-function-docstring: Missing function or method docstring
"C0209", # consider-using-f-string: Consider using an f-string
"E0401", # import-error: Unable to import module
"R0902", # too-many-instance-attributes: Too many instance attributes
"R0903", # too-few-public-methods: Too few public methods
"R0912", # too-many-branches: Too many branches
"R0913", # too-many-arguments: Too many arguments
"R0914", # too-many-locals: Too many local variables
"R0915", # too-many-statements: Too many statements
"R0917", # too-many-positional-arguments: Too many positional arguments
"R1702", # too-many-nested-blocks: Too many nested blocks
# "R1707", # trailing-comma-tuple: Disallow trailing comma tuple
# "R1735", # use-dict-literal: recommends {...} instead of dict(...)
# "W0104", # pointless-statement: Statement seems to have no effect
# "W0201", # attribute-defined-outside-init: Attribute defined outside __init__
# "W0511", # fixme: Used when a warning note as FIXME or XXX is detected
# "W0611", # unused-import: Unused import
# "W0612", # unused-variable: Unused variable
# "W0702", # bare-except: No exception type(s) specified
# "W0707", # raise-missing-from: "Consider explicitly re-raising using 'except ... as exc"
# "W0718", # broad-exception-caught: Catching too general exception
# "W1201", # logging-not-lazy: Use lazy % formatting in logging functions
# "W1203", # logging-fstring-interpolation: Use % formatting in logging functions
# "W1514", # unspecified-encoding: Using open without explicitly specifying an encoding
]

[tool.pylint.format]
max-line-length = 120

[tool.pylint.basic]
good-names = ["i", "j", "k", "n", "pk", "qs"]
32 changes: 0 additions & 32 deletions setup.py

This file was deleted.

13 changes: 6 additions & 7 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from django.conf.global_settings import * # noqa

from django.conf.global_settings import * # noqa: F403 # pylint: disable=wildcard-import,unused-wildcard-import

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "mem_db",
}
},
}

INSTALLED_APPS = [
Expand All @@ -31,9 +30,9 @@
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
]
}
}
],
},
},
]

MIDDLEWARE = [
Expand All @@ -49,5 +48,5 @@
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
},
}
4 changes: 2 additions & 2 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class EmailLoginModelBackendTest(TestCase):
def test_authenticates_using_email(self):
user = User(
username='foobar',
email='[email protected]'
email='[email protected]',
)
user.set_password('qwerty321')
user.save()
Expand All @@ -27,7 +27,7 @@ class UsernameOrEmailLoginModelBackendTest(TestCase):
def test_authenticates_using_email(self):
user = User(
username='foobar',
email='[email protected]'
email='[email protected]',
)
user.set_password('qwerty321')
user.save()
Expand Down
4 changes: 1 addition & 3 deletions tests/test_cached_auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from django.core.management import call_command
from django.test import TestCase
from django.test import override_settings

from django.test import TestCase, override_settings

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
Expand Down
2 changes: 1 addition & 1 deletion tests/test_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ def test_returns_result_for_long_arguments(self):
def test_bytestring(self):
b_text = b'\xc4\xbe\xc5\xa1\xc4\x8d\xc5\xa5\xc5\xbe\xc3\xbd\xc3\xa1\xc3\xad\xc3\xa9' * 10
result = expensive_function(
b_text, b_text.decode('utf-8')
b_text, b_text.decode('utf-8'),
)
self.assertEqual(result, 2)
7 changes: 3 additions & 4 deletions tests/test_getters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from django.contrib.auth.models import Group, User
from django.test import TestCase

from useful.django.getters import prefetch_m2m


Expand All @@ -9,11 +8,11 @@ class PrefetchM2MTest(TestCase):
def test_returns_dict_of_lists(self):
user1 = User.objects.create(
username='foobar',
email='[email protected]'
email='[email protected]',
)
user2 = User.objects.create(
username='foobar2',
email='[email protected]'
email='[email protected]',
)
group1 = Group.objects.create(name='Foo')
group2 = Group.objects.create(name='Bar')
Expand All @@ -25,7 +24,7 @@ def test_returns_dict_of_lists(self):

self.assertEqual(
list(groups_m2m.keys()),
[user1.id, user2.id]
[user1.id, user2.id],
)
self.assertEqual(groups_m2m[user1.id], [group1, group2])
self.assertEqual(groups_m2m[user2.id], [group2, group3])
Loading