diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 2292bec..0000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -source = name -omit = */migrations/* -branch = true - -[report] -show_missing = true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 160c33a..9445866 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,6 @@ jobs: uses: actions/checkout@v3 - name: Start the app run: docker compose up -d - - name: Run the tests and flake8 + - name: Run the tests and ruff run: docker compose run --rm web tox diff --git a/Dockerfile b/Dockerfile index 801439b..ed37db9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 +FROM python:3.14 ENV PYTHONUNBUFFERED 1 ENV PYTHONPATH /app @@ -15,4 +15,5 @@ WORKDIR /app COPY . /app/ # Install dependencies -RUN pip install .'[dev,test,codestyle]' +RUN pip install tox +RUN pip install .'[dev,test]' diff --git a/README.md b/README.md index c4a00b5..6ef9648 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Contributors: ## Requirements -- Python 3.9 or later +- Python 3.12 - 3.14 ## Development @@ -123,7 +123,7 @@ $ source env/bin/activate Install the dev dependencies ```sh -$ pip install .'[dev,test,codestyle]' +$ pip install .'[dev,test]' ``` Replace the `DATABASES` Django setting in `./tests/settings/dev.py` with diff --git a/docker-compose.yml b/docker-compose.yml index ea4005b..6d51778 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: mariadb: - image: mariadb:10.5 + image: mariadb:11.4.13 environment: - MYSQL_ROOT_PASSWORD=root - MYSQL_DATABASE=name diff --git a/docs/conf.py b/docs/conf.py index 7ed78e0..5c72bc0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Django Name documentation build configuration file, created by # sphinx-quickstart on Fri May 8 09:55:15 2015. @@ -12,10 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os import re -import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -47,9 +44,9 @@ master_doc = 'index' # General information about the project. -project = u'Django Name' -copyright = u'2017, University of North Texas Libraries' -author = u'University of North Texas Libraries' +project = 'Django Name' +copyright = '2017, University of North Texas Libraries' +author = 'University of North Texas Libraries' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -235,8 +232,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'DjangoName.tex', u'Django Name Documentation', - u'University of North Texas Libraries', 'manual'), + (master_doc, 'DjangoName.tex', 'Django Name Documentation', + 'University of North Texas Libraries', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -265,7 +262,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'djangoname', u'Django Name Documentation', + (master_doc, 'djangoname', 'Django Name Documentation', [author], 1) ] @@ -279,7 +276,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'DjangoName', u'Django Name Documentation', + (master_doc, 'DjangoName', 'Django Name Documentation', author, 'DjangoName', 'One line description of project.', 'Miscellaneous'), ] diff --git a/name/__init__.py b/name/__init__.py index 8cf0c0e..44e23ef 100644 --- a/name/__init__.py +++ b/name/__init__.py @@ -1,2 +1,2 @@ __title__ = 'Django Name' -__version__ = '5.0.0' +__version__ = '6.0.0' diff --git a/name/api/serializers.py b/name/api/serializers.py index 848684a..9722139 100644 --- a/name/api/serializers.py +++ b/name/api/serializers.py @@ -144,7 +144,7 @@ def get_label(self, obj): " ()" """ if obj.disambiguation: - return '{0} ({1})'.format(obj.name, obj.disambiguation) + return f'{obj.name} ({obj.disambiguation})' return obj.name diff --git a/name/api/stats.py b/name/api/stats.py index d85ecd6..1fc82df 100644 --- a/name/api/stats.py +++ b/name/api/stats.py @@ -4,7 +4,7 @@ from dateutil.relativedelta import relativedelta -class NameStatisticsMonth(object): +class NameStatisticsMonth: """A simple datatype to represent Name statistics for a single month. """ @@ -15,7 +15,7 @@ def __init__(self, **kwargs): self.month = kwargs.get('month', None) -class NameStatisticsType(object): +class NameStatisticsType: """Statistics class for calculating the number of Name objects in the database using a DateTime field. @@ -98,7 +98,7 @@ def process_raw_stats(self): current += delta -class NameStatistics(object): +class NameStatistics: """Container class for all statistics gathered on Name objects. """ diff --git a/name/api/views.py b/name/api/views.py index ecb1ac2..e194d51 100644 --- a/name/api/views.py +++ b/name/api/views.py @@ -14,7 +14,7 @@ class JSONResponse(http.HttpResponse): def __init__(self, data, **kwargs): content = JSONRenderer().render(data, renderer_context={'indent': 4}) kwargs['content_type'] = 'application/json' - super(JSONResponse, self).__init__(content, **kwargs) + super().__init__(content, **kwargs) def name_json(request, name_id): diff --git a/name/decorators.py b/name/decorators.py index 1c14ed9..44418f3 100644 --- a/name/decorators.py +++ b/name/decorators.py @@ -22,7 +22,7 @@ def jsonp_wrapper(request, *args, **kwargs): if 'callback' in request.GET: callback = request.GET['callback'] resp['Content-Type'] = 'application/javascript; charset=utf-8' - resp.content = "%s(%s)" % (callback, resp.content) + resp.content = f'{callback}({resp.content})' return resp else: return resp diff --git a/name/feeds.py b/name/feeds.py index c9546dd..910dacf 100644 --- a/name/feeds.py +++ b/name/feeds.py @@ -18,12 +18,12 @@ class NameAtomFeedType(Atom1Feed): GEO_RSS_NS = 'http://www.georss.org/georss' def root_attributes(self): - attrs = super(NameAtomFeedType, self).root_attributes() + attrs = super().root_attributes() attrs['xmlns:georss'] = self.GEO_RSS_NS return attrs def add_item_elements(self, handler, item): - super(NameAtomFeedType, self).add_item_elements(handler, item) + super().add_item_elements(handler, item) # Add the georss:point element if the item has a geo_point # attribute. @@ -50,7 +50,7 @@ def item_title(self, obj): return obj.name def item_description(self, obj): - return 'Name Type: {0}'.format(obj.get_name_type_label()) + return f'Name Type: {obj.get_name_type_label()}' def item_link(self, obj): return obj.get_absolute_url() diff --git a/name/models.py b/name/models.py index 67e924f..44c50fb 100644 --- a/name/models.py +++ b/name/models.py @@ -148,7 +148,7 @@ def save(self, *args, **kwargs): self.normalized_variant = normalizeSimplified(self.variant) if 'update_fields' in kwargs: kwargs['update_fields'] = {'normalized_variant'}.union(kwargs['update_fields']) - super(Variant, self).save(*args, **kwargs) + super().save(*args, **kwargs) def __str__(self): return self.variant @@ -196,7 +196,7 @@ def ticket(self): return self.id def __str__(self): - return 'nm{ticket:07d}'.format(ticket=self.ticket) + return f'nm{self.ticket:07d}' class NameManager(models.Manager): @@ -513,8 +513,9 @@ def save(self, *args, **kwargs): self.__normalize_name() self.__assign_name_id() if 'update_fields' in kwargs: - kwargs['update_fields'] = {'normalized_name', 'name_id', 'last_modified'}.union(kwargs['update_fields']) - super(Name, self).save(*args, **kwargs) + kwargs['update_fields'] = ({'normalized_name', 'name_id', 'last_modified'} + .union(kwargs['update_fields'])) + super().save(*args, **kwargs) if self.is_building() and not self.location_set.count(): self.__find_location() @@ -522,7 +523,7 @@ def clean(self, *args, **kwargs): # Call merged_with_validator here so that we can pass in # the model instance. validate_merged_with(self) - super(Name, self).clean(*args, **kwargs) + super().clean(*args, **kwargs) def __str__(self): return self.name_id @@ -587,14 +588,14 @@ class Meta: base_manager_name = 'objects' def geo_point(self): - return '{lat} {lng}'.format(lat=self.latitude, lng=self.longitude) + return f'{self.latitude} {self.longitude}' def is_current(self): """True if the Location has a status of Current.""" return self.CURRENT == self.status def save(self, *args, **kwargs): - super(Location, self).save(*args, **kwargs) + super().save(*args, **kwargs) # When this instance's status is CURRENT, get all other locations # related the belong_to_name, and set the status to FORMER. if self.is_current(): diff --git a/name/views.py b/name/views.py index 8b7a1df..073b7dd 100644 --- a/name/views.py +++ b/name/views.py @@ -31,13 +31,13 @@ def label(request, name_value): except Name.DoesNotExist: return http.HttpResponseNotFound( - 'No matching term found - authoritative or variant - for "{0}"' - .format(name_value)) + f'No matching term found - authoritative or variant - for "{name_value}"' + ) except Name.MultipleObjectsReturned: return http.HttpResponseNotFound( - 'There are multiple Name objects with the same name: "{0}".' - .format(normalized_name)) + f'There are multiple Name objects with the same name: "{normalized_name}".' + ) def detail(request, name_id): diff --git a/pyproject.toml b/pyproject.toml index 3a1b05d..79d232e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,10 +6,10 @@ build-backend = "setuptools.build_meta" name = "django-name" description = "Name Authority App for Django." readme = "README.md" -version = "5.0.0" +version = "6.0.0" license = {file = 'LICENSE'} keywords = ["django", "name", "citation"] -requires-python = ">=3.9,<4.0" +requires-python = ">=3.12,<3.15" authors = [ { name = "University of North Texas Libraries", email = "mark.phillips@unt.edu" } ] @@ -19,7 +19,9 @@ classifiers = [ "Intended Audience :: End Users/Desktop", "Natural Language :: English", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application" ] dependencies = [ @@ -31,32 +33,50 @@ dependencies = [ [project.urls] Homepage = "https://github.com/unt-libraries/django-name" -Repository = "https://github.com/unt-libraries/django-name" +Repository = "https://github.com/unt-libraries/django-name.git" [project.optional-dependencies] -codestyle = ["flake8"] dev = [ - "Django==4.2.27", - "django-debug-toolbar==3.8.1", - "mysqlclient==2.0.1", + "Django~=6.1.0", + "django-debug-toolbar~=7.1.0", + "mysqlclient~=2.3.0", ] test = [ - "tox>=3.3.0", "markdown2>=2.4.0", "pytest>=7.1.0", "pytest-django>=4.6.2", "pytest-cov>=7.0.0", "psycopg2-binary>=2.9.9", "tomli>=2.0.1", + "ruff~=0.15.9", ] +[tool.ruff] +line-length = 99 +target-version = "py314" +extend-exclude = ["migrations"] + +[tool.ruff.format] +quote-style = "single" + +[tool.ruff.lint] +select = ["E", "F", "W"] + +[tool.coverage.run] +source = ["name"] +omit = ["*/migrations/*"] +branch = true + +[tool.coverage.report] +show_missing = true + [tool.tox] legacy_tox_ini =''' [tox] envlist = - py39-django42-{postgres,mariadb}, - py39-flake8, + py314-django61-{postgres,mariadb} + py314-ruff docs [testenv] @@ -65,22 +85,21 @@ legacy_tox_ini =''' extras = test dev - codestyle commands = - mariadb: ./runtests.py {posargs} --nolint --ds=tests.settings.test_mariadb - postgres: ./runtests.py {posargs} --nolint --ds=tests.settings.test_postgres + mariadb: ./runtests.py {posargs} --ds=tests.settings.test_mariadb + postgres: ./runtests.py {posargs} --ds=tests.settings.test_postgres - [testenv:py39-django42-mariadb] - commands = - ./runtests.py {posargs} --nolint --ds=tests.settings.test_mariadb --cov=name + [testenv:py314-django61-mariadb] + commands = + ./runtests.py {posargs} --ds=tests.settings.test_mariadb --cov=name allowlist_externals = ./runtests.py - [testenv:py39-flake8] + [testenv:py314-ruff] commands = - ./runtests.py --lintonly - allowlist_externals = ./runtests.py + ruff check . [testenv:docs] + skip_install = true deps = Sphinx>=4.0 changedir = docs commands = {envbindir}/sphinx-build -W -b html . {envtmpdir}/html diff --git a/runtests.py b/runtests.py index cbcb417..59fb2fd 100755 --- a/runtests.py +++ b/runtests.py @@ -1,10 +1,8 @@ #! /usr/bin/env python -from __future__ import print_function import pytest import sys import os -import subprocess PYTEST_ARGS = { @@ -12,9 +10,6 @@ 'fast': ['tests', '-q'], } -FLAKE8_ARGS = ['name', 'tests', '--ignore=F403,E501,F999,W504'] - - sys.path.append(os.path.dirname(__file__)) @@ -23,16 +18,9 @@ def exit_on_failure(ret, message=None): sys.exit(ret) -def flake8_main(args): - print('Running flake8 code linting') - ret = subprocess.call(['flake8'] + args) - print('flake8 failed' if ret else 'flake8 passed') - return ret - - def split_class_and_function(string): class_string, function_string = string.split('.', 1) - return "%s and %s" % (class_string, function_string) + return f'{class_string} and {function_string}' def is_function(string): @@ -46,27 +34,12 @@ def is_class(string): if __name__ == "__main__": - try: - sys.argv.remove('--nolint') - except ValueError: - run_flake8 = True - else: - run_flake8 = False - - try: - sys.argv.remove('--lintonly') - except ValueError: - run_tests = True - else: - run_tests = False - try: sys.argv.remove('--fast') except ValueError: style = 'default' else: style = 'fast' - run_flake8 = False if len(sys.argv) > 1: pytest_args = sys.argv[1:] @@ -85,7 +58,4 @@ def is_class(string): else: pytest_args = PYTEST_ARGS[style] - if run_tests: - exit_on_failure(pytest.main(pytest_args)) - if run_flake8: - exit_on_failure(flake8_main(FLAKE8_ARGS)) + exit_on_failure(pytest.main(pytest_args)) diff --git a/tests/conftest.py b/tests/conftest.py index 1bec3b7..926c893 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,7 +36,7 @@ def twenty_name_fixtures(db, scope="session"): """ for x in range(21): Name.objects.create( - name="Name {0}".format(x), + name=f'Name {x}', name_type=random.randint(0, 4), begin='2012-01-12') return Name.objects.all() @@ -75,13 +75,13 @@ def search_fixtures(db, scope='module'): """ for x in range(1, 5): Name.objects.create( - name="Personal {0}".format(x), name_type=Name.PERSONAL) + name=f'Personal {x}', name_type=Name.PERSONAL) Name.objects.create( - name="Organization {0}".format(x), name_type=Name.ORGANIZATION) + name=f'Organization {x}', name_type=Name.ORGANIZATION) Name.objects.create( - name="Event {0}".format(x), name_type=Name.EVENT) + name=f'Event {x}', name_type=Name.EVENT) Name.objects.create( - name="Software {0}".format(x), name_type=Name.SOFTWARE) + name=f'Software {x}', name_type=Name.SOFTWARE) Name.objects.create( - name="Building {0}".format(x), name_type=Name.BUILDING) + name=f'Building {x}', name_type=Name.BUILDING) return Name.objects.all() diff --git a/tests/test_decorators.py b/tests/test_decorators.py index c35f86c..7fa1291 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -38,7 +38,7 @@ def test_jsonp_returns_with_callback(): result = decorated_f(request) - expected = 'init({0})'.format(json) + expected = f'init({json})' assert expected == result.content diff --git a/tests/test_models.py b/tests/test_models.py index 0d6aded..371f5b4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -60,10 +60,10 @@ class TestBaseTicketing: def test_has_str_method(self): ticket_id = 1 ticket = BaseTicketing(id=ticket_id) - assert 'nm{:07d}'.format(ticket_id) == str(ticket) + assert f'nm{ticket_id:07d}' == str(ticket) -class TestNameManager(object): +class TestNameManager: @pytest.fixture def time_series_names(self): diff --git a/tests/test_search.py b/tests/test_search.py index 2d22798..5651e5b 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -4,7 +4,7 @@ from django.urls import reverse # All test need access to the database in this file. -pytestmark = pytest.mark.django +pytestmark = pytest.mark.django_db query_template = '?q_type={0}&q={1}' diff --git a/tests/test_views.py b/tests/test_views.py index 75c05d8..194a8de 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -88,8 +88,7 @@ def test_label_returns_not_found_multiple_names_found(client): def test_export(client, name_fixture): response = client.get(reverse('name:export')) - expected = 'personal\ttest person\thttp://testserver{}\r\n'.format( - name_fixture.get_absolute_url()) + expected = f'personal\ttest person\thttp://testserver{name_fixture.get_absolute_url()}\r\n' assert expected == response.content.decode() assert 200 == response.status_code @@ -99,7 +98,7 @@ def test_export_json(client, name_fixture): expected = [ { 'i': name_fixture.name_id, - 'l': 'http://testserver{}'.format(name_fixture.get_absolute_url()), + 'l': f'http://testserver{name_fixture.get_absolute_url()}', 't': name_fixture.name, }, ]