diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c60f4502..1a1aa6618 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.12'] services: postgres: @@ -23,15 +23,15 @@ jobs: options: --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.x @@ -60,4 +60,4 @@ jobs: - name: Lint if: matrix.python-version == 3.12 - run: make lint + run: make lint-ci diff --git a/.gitignore b/.gitignore index 828505958..91429c35c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ __pycache__/ *.whl build/ .cache/ +.ruff_cache/ pip-wheel-metadata/ # Tests diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 56c3731c1..000000000 --- a/.isort.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[isort] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 -lines_after_imports=2 -skip=newdle/client/node_modules -known_third_party=exchangelib diff --git a/Makefile b/Makefile index d40edaf3f..3f563b53c 100644 --- a/Makefile +++ b/Makefile @@ -94,19 +94,26 @@ react-server: i18n .PHONY: lint lint: @printf " \033[38;5;154mDEV\033[0m \033[38;5;77mLinting code\033[0m\n" - @npm run flake8 + @npm run ruff @npm run eslint:check @npm run prettier:check - @npm run isort:check - @npm run black:check + @npm run ruff-format:check + + +.PHONY: lint-ci +lint-ci: + @printf " \033[38;5;154mDEV\033[0m \033[38;5;77mLinting code (CI)\033[0m\n" + @npm run ruff -- --output-format github + @npm run eslint:check + @npm run prettier:check + @npm run ruff-format:check .PHONY: format format: @printf " \033[38;5;154mDEV\033[0m \033[38;5;77mFormatting code\033[0m\n" @npm run eslint - @npm run isort - @npm run black + @npm run ruff-format .PHONY: test diff --git a/README.md b/README.md index 0e7614889..89a9dff16 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ decided (still work in progress!). ## Development -We chose Python 3.12 as the backend language, so make sure you have it installed (3.9-3.11 should work as well). To prepare the development environment it is enough to run `make` which takes care of installing all required dependencies inside a new virtualenv. Typically that will be the `.venv` directory unless you override the environment variable `VENV` *e.g.* `VENV=.virtualenv make`. Activate your virtualenv using ``source .venv/bin/activate`` since this is required to run the various `flask` comments that come later. +We chose Python 3.12 as the backend language, so make sure you have it installed (`pyenv` helps if your distribution does not include it). To prepare the development environment it is enough to run `make` which takes care of installing all required dependencies inside a new virtualenv. Typically that will be the `.venv` directory unless you override the environment variable `VENV` *e.g.* `VENV=.virtualenv make`. Activate your virtualenv using ``source .venv/bin/activate`` since this is required to run the various `flask` comments that come later. Make sure you have the `python` binary in your PATH. You can also use the `PYTHON` environment variable to override the location of the `python` binary. *e.g.:* @@ -64,7 +64,7 @@ We provide a couple of additional `make` targets that should streamline the deve - `make clean` - removes all generated files - `make distclean` - runs `clean` target first and removes config files afterwards - `make lint` - runs linters, which report possible code style issues - - `make format` - runs code formatters over the entire codebase (black, isort, prettier) + - `make format` - runs code formatters over the entire codebase (ruff, prettier) - `make test` - runs Python and React tests - `make build` - builds a Python wheel which then could be used to install `newdle` in production diff --git a/ci/check_history_consistency.py b/ci/check_history_consistency.py index 92a3256db..130aa2615 100755 --- a/ci/check_history_consistency.py +++ b/ci/check_history_consistency.py @@ -3,6 +3,7 @@ Checks the consistency of the history, by comparing flask db output and the filesystem ordering of files. """ + import re import subprocess import sys @@ -37,7 +38,7 @@ def _check_history_consistency(): if revisions != file_revisions: print( '::error::' - 'The order of revisions in the database and in the filesystem is different. ' + 'The order of revisions in the database and filesystem is different. ' 'Please review them and make sure the revision history is linear. ' 'Make sure that all the migrations have a distinct "down_revision".' ) diff --git a/conftest.py b/conftest.py index 0a19a4633..2eeb8cf0a 100644 --- a/conftest.py +++ b/conftest.py @@ -9,7 +9,6 @@ from newdle.core.db import db from newdle.models import Newdle, Participant - TEST_DATABASE_URI = os.environ.get( 'NEWDLE_TEST_DATABASE_URI', 'postgresql:///newdle_tests' ) @@ -39,13 +38,13 @@ def database(app): flask_migrate.downgrade(revision='base') -@pytest.fixture() +@pytest.fixture def db_session(app, database): """Create a new database session.""" connection = db.engine.connect() transaction = connection.begin() - options = dict(bind=connection, binds={}) + options = {'bind': connection, 'binds': {}} session_factory = sessionmaker(**options) session = scoped_session(session_factory) @@ -67,7 +66,7 @@ def mail_queue(): return django_mail.outbox -@pytest.fixture() +@pytest.fixture def flask_client(app): return app.test_client() @@ -89,7 +88,7 @@ def dummy_participant_uid(): @pytest.fixture def create_newdle(dummy_uid, db_session): - """Return a callable which lets you create dummy newdles""" + """Return a callable which lets you create dummy newdles.""" def _create_newdle(id=None, **kwargs): kwargs.setdefault( diff --git a/newdle/api.py b/newdle/api.py index 6a404a1ad..80d40334d 100644 --- a/newdle/api.py +++ b/newdle/api.py @@ -22,26 +22,26 @@ from sqlalchemy.orm import selectinload from werkzeug.exceptions import Forbidden, ServiceUnavailable, UnprocessableEntity -from .answers import validate_answers -from .calendar import create_calendar_event -from .core.auth import search_users, user_info_from_app_token -from .core.db import db -from .core.util import ( +from newdle.answers import validate_answers +from newdle.calendar import create_calendar_event +from newdle.core.auth import search_users, user_info_from_app_token +from newdle.core.db import db +from newdle.core.util import ( DATE_FORMAT, avatar_info_from_payload, change_dt_timezone, range_union, render_user_avatar, ) -from .core.webargs import abort, use_args, use_kwargs -from .export import export_answers_to_csv, export_answers_to_xlsx -from .models import Availability, Newdle, Participant, StatKey, Stats -from .notifications import ( +from newdle.core.webargs import abort, use_args, use_kwargs +from newdle.export import export_answers_to_csv, export_answers_to_xlsx +from newdle.models import Availability, Newdle, Participant, StatKey, Stats +from newdle.notifications import ( notify_newdle_creator, notify_newdle_participants, send_invitation_emails, ) -from .schemas import ( +from newdle.schemas import ( DeletedNewdleSchema, MyNewdleSchema, NewdleParticipantSchema, @@ -57,7 +57,6 @@ UserSearchResultSchema, ) - api = Blueprint('api', __name__, url_prefix='/api') @@ -616,7 +615,7 @@ def send_deletion_emails(args, code): 'title': newdle.title, 'participant': p.name, 'summary_link': url_for('newdle_summary', code=newdle.code, _external=True), - 'comment': args['comment'] if 'comment' in args else None, + 'comment': args.get('comment'), }, ) return '', 204 diff --git a/newdle/auth.py b/newdle/auth.py index aae77f6d6..ea2636e74 100644 --- a/newdle/auth.py +++ b/newdle/auth.py @@ -1,7 +1,6 @@ from flask import Blueprint, current_app, redirect, render_template, session, url_for -from .core.auth import app_token_from_dummy, multipass - +from newdle.core.auth import app_token_from_dummy, multipass auth = Blueprint('auth', __name__) diff --git a/newdle/calendar.py b/newdle/calendar.py index bdceca6b1..45361f20e 100644 --- a/newdle/calendar.py +++ b/newdle/calendar.py @@ -12,7 +12,7 @@ def _email_to_vcal_address(email): def _add_participant_to_ical_event(event, participant): - """Adds a participant as an attendee to an ical event.""" + """Add a participant as an attendee to an ical event.""" attendee = _email_to_vcal_address(participant.email) attendee.params['cn'] = vText(participant.name) attendee.params['ROLE'] = vText('REQ-PARTICIPANT') @@ -47,8 +47,8 @@ def create_calendar_event(newdle, participant=None): if not newdle.private and not participant: participants = [p for p in newdle.participants if p.email] - for participant in participants: - _add_participant_to_ical_event(event, participant) + for part in participants: + _add_participant_to_ical_event(event, part) calendar.add_component(event) return calendar.to_ical() diff --git a/newdle/cli.py b/newdle/cli.py index f3c1b42c6..ed4358158 100644 --- a/newdle/cli.py +++ b/newdle/cli.py @@ -5,10 +5,13 @@ from flask import Blueprint, current_app from sqlalchemy import and_, or_ -from .core.db import db -from .models import Newdle -from .providers.free_busy.util import get_msal_app, get_msal_token, save_msal_cache - +from newdle.core.db import db +from newdle.models import Newdle +from newdle.providers.free_busy.util import ( + get_msal_app, + get_msal_token, + save_msal_cache, +) try: import exchangelib diff --git a/newdle/core/app.py b/newdle/core/app.py index 6811c072c..493905a75 100644 --- a/newdle/core/app.py +++ b/newdle/core/app.py @@ -4,14 +4,14 @@ from werkzeug.exceptions import HTTPException from werkzeug.middleware.proxy_fix import ProxyFix -from ..api import api -from ..auth import auth -from ..cli import cli -from .auth import multipass -from .cache import cache -from .db import db, migrate -from .marshmallow import mm -from .util import dedent +from newdle.api import api +from newdle.auth import auth +from newdle.cli import cli +from newdle.core.auth import multipass +from newdle.core.cache import cache +from newdle.core.db import db, migrate +from newdle.core.marshmallow import mm +from newdle.core.util import dedent def _configure_app(app, from_env=True): @@ -76,7 +76,7 @@ def _handle_exception(exc): # If the client prefers HTML (probably a browser), we keep the # default logic which results in standard "internal server error" # message or the debugger while in development mode. - raise + raise # noqa: PLE0704 app.logger.exception('Request failed') return jsonify(error='internal_error'), 500 diff --git a/newdle/core/auth.py b/newdle/core/auth.py index 50f06ba01..9fa590ccc 100644 --- a/newdle/core/auth.py +++ b/newdle/core/auth.py @@ -1,7 +1,7 @@ from flask import current_app, render_template from flask_multipass import Multipass -from .util import secure_timed_serializer +from newdle.core.util import secure_timed_serializer class NewdleMultipass(Multipass): diff --git a/newdle/core/cache.py b/newdle/core/cache.py index 6a26d6190..9d828e39f 100644 --- a/newdle/core/cache.py +++ b/newdle/core/cache.py @@ -1,4 +1,3 @@ from flask_caching import Cache - cache = Cache(with_jinja2_ext=False) diff --git a/newdle/core/marshmallow.py b/newdle/core/marshmallow.py index 60475d74d..1127bd356 100644 --- a/newdle/core/marshmallow.py +++ b/newdle/core/marshmallow.py @@ -1,4 +1,3 @@ from flask_marshmallow import Marshmallow - mm = Marshmallow() diff --git a/newdle/core/util.py b/newdle/core/util.py index fe4b3405b..9de6d240a 100644 --- a/newdle/core/util.py +++ b/newdle/core/util.py @@ -7,7 +7,6 @@ from itsdangerous import BadData, Signer, URLSafeSerializer, URLSafeTimedSerializer from werkzeug.local import LocalProxy - DATE_FORMAT = '%Y-%m-%d' DATETIME_FORMAT = '%Y-%m-%dT%H:%M' @@ -21,6 +20,7 @@ class AutoNameEnum(Enum): + @staticmethod def _generate_next_value_(name, start, count, last_values): return name diff --git a/newdle/core/webargs.py b/newdle/core/webargs.py index 5de6b9d54..1d051d37d 100644 --- a/newdle/core/webargs.py +++ b/newdle/core/webargs.py @@ -28,9 +28,9 @@ def pre_load(self, location_data, *, schema, req, location): def handle_error(error, req, schema, *, error_status_code, error_headers): # since 6.0.0b7 errors are namespaced by their source. this is nice for APIs taking # data from different locations to serve very specific errors, but in a typical web - # app where you usually have only one source and certainly not the same field name in - # different locations, it just makes handling errors in JS harder since we suddenly - # have to care if it's form data or json data + # app where you usually have only one source and certainly not the same field name + # in different locations, it just makes handling errors in JS harder since we + # suddenly have to care if it's form data or json data namespaced = error.messages # mutating this below is safe error.messages = namespaced.popitem()[1] assert not namespaced # we never expect to have more than one location diff --git a/newdle/export.py b/newdle/export.py index 1a7b0ca4f..f1fff2c35 100644 --- a/newdle/export.py +++ b/newdle/export.py @@ -2,6 +2,7 @@ import datetime from contextlib import contextmanager from io import BytesIO, TextIOWrapper +from operator import itemgetter from xlsxwriter import Workbook @@ -11,7 +12,7 @@ def _generate_answers_for_export(newdle): slots = [format_dt(slot) for slot in newdle.timeslots] rows = [] - rows.append(['Participant name'] + slots + ['Comment']) + rows.append(['Participant name', *slots, 'Comment']) for p in newdle.participants: answers = [p.name] @@ -23,7 +24,7 @@ def _generate_answers_for_export(newdle): answers.append(p.comment) rows.append(answers) # Sort participants by name - rows[1:] = sorted(rows[1:], key=lambda row: row[0]) + rows[1:] = sorted(rows[1:], key=itemgetter(0)) return rows diff --git a/newdle/migrations/env.py b/newdle/migrations/env.py index d5900aaba..9f1831f7c 100644 --- a/newdle/migrations/env.py +++ b/newdle/migrations/env.py @@ -4,7 +4,6 @@ from flask import current_app from sqlalchemy import engine_from_config, pool - # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config diff --git a/newdle/migrations/versions/20190912_1718_000000000000_create_initial_tables.py b/newdle/migrations/versions/20190912_1718_000000000000_create_initial_tables.py index f825e6620..5b6933cc9 100644 --- a/newdle/migrations/versions/20190912_1718_000000000000_create_initial_tables.py +++ b/newdle/migrations/versions/20190912_1718_000000000000_create_initial_tables.py @@ -8,7 +8,6 @@ from alembic import op from sqlalchemy.dialects import postgresql - # revision identifiers, used by Alembic. revision = '000000000000' down_revision = None diff --git a/newdle/migrations/versions/20200814_1707_93a638b96375_add_private_column_to_newdle_model.py b/newdle/migrations/versions/20200814_1707_93a638b96375_add_private_column_to_newdle_model.py index 9491dd515..a8edf4d0e 100644 --- a/newdle/migrations/versions/20200814_1707_93a638b96375_add_private_column_to_newdle_model.py +++ b/newdle/migrations/versions/20200814_1707_93a638b96375_add_private_column_to_newdle_model.py @@ -8,7 +8,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = '93a638b96375' down_revision = '000000000000' diff --git a/newdle/migrations/versions/20200914_1149_679eab4aab01_add_comment_column_to_participants.py b/newdle/migrations/versions/20200914_1149_679eab4aab01_add_comment_column_to_participants.py index 3a1bc8a24..2bd51106a 100644 --- a/newdle/migrations/versions/20200914_1149_679eab4aab01_add_comment_column_to_participants.py +++ b/newdle/migrations/versions/20200914_1149_679eab4aab01_add_comment_column_to_participants.py @@ -8,7 +8,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = '679eab4aab01' down_revision = '93a638b96375' diff --git a/newdle/migrations/versions/20200915_1105_239d7862e2fe_add_last_update_field_to_newdle_model.py b/newdle/migrations/versions/20200915_1105_239d7862e2fe_add_last_update_field_to_newdle_model.py index f0271ca9d..ed7d3b246 100644 --- a/newdle/migrations/versions/20200915_1105_239d7862e2fe_add_last_update_field_to_newdle_model.py +++ b/newdle/migrations/versions/20200915_1105_239d7862e2fe_add_last_update_field_to_newdle_model.py @@ -8,7 +8,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = '239d7862e2fe' down_revision = '679eab4aab01' @@ -27,11 +26,11 @@ def upgrade(): ), ) op.execute( - ''' + """ UPDATE newdles SET last_update = final_dt WHERE final_dt IS NOT NULL AND final_dt < now() AT TIME ZONE 'utc'; - ''' + """ ) diff --git a/newdle/migrations/versions/20200922_1759_43f0bf77bf97_add_newdle_creator_email_notify.py b/newdle/migrations/versions/20200922_1759_43f0bf77bf97_add_newdle_creator_email_notify.py index f83e33bcc..0e54e4ff7 100644 --- a/newdle/migrations/versions/20200922_1759_43f0bf77bf97_add_newdle_creator_email_notify.py +++ b/newdle/migrations/versions/20200922_1759_43f0bf77bf97_add_newdle_creator_email_notify.py @@ -8,7 +8,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = '43f0bf77bf97' down_revision = '239d7862e2fe' diff --git a/newdle/migrations/versions/20200928_1154_d85e02075214_add_newdle_deletion_date.py b/newdle/migrations/versions/20200928_1154_d85e02075214_add_newdle_deletion_date.py index 6e146691d..537a6992a 100644 --- a/newdle/migrations/versions/20200928_1154_d85e02075214_add_newdle_deletion_date.py +++ b/newdle/migrations/versions/20200928_1154_d85e02075214_add_newdle_deletion_date.py @@ -8,7 +8,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = 'd85e02075214' down_revision = '43f0bf77bf97' diff --git a/newdle/migrations/versions/20210531_1656_28103ebec68e_add_stats_table.py b/newdle/migrations/versions/20210531_1656_28103ebec68e_add_stats_table.py index 1d109132f..377afd04b 100644 --- a/newdle/migrations/versions/20210531_1656_28103ebec68e_add_stats_table.py +++ b/newdle/migrations/versions/20210531_1656_28103ebec68e_add_stats_table.py @@ -8,7 +8,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = '28103ebec68e' down_revision = 'd85e02075214' @@ -24,13 +23,13 @@ def upgrade(): sa.PrimaryKeyConstraint('key', name=op.f('pk_stats')), ) op.execute( - ''' + """ INSERT INTO stats (key, value) SELECT 'newdles_created', COUNT(*) FROM newdles; INSERT INTO stats (key, value) SELECT 'participants_created', COUNT(*) FROM participants; - ''' + """ ) diff --git a/newdle/migrations/versions/20220429_0945_30b5fe7b5949_add_limited_slots.py b/newdle/migrations/versions/20220429_0945_30b5fe7b5949_add_limited_slots.py index f8132db12..920d34afd 100644 --- a/newdle/migrations/versions/20220429_0945_30b5fe7b5949_add_limited_slots.py +++ b/newdle/migrations/versions/20220429_0945_30b5fe7b5949_add_limited_slots.py @@ -8,7 +8,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = '30b5fe7b5949' down_revision = '28103ebec68e' diff --git a/newdle/models.py b/newdle/models.py index 526085869..ab8a2292e 100644 --- a/newdle/models.py +++ b/newdle/models.py @@ -8,9 +8,8 @@ from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.schema import CheckConstraint -from .core.db import db -from .core.util import AutoNameEnum, format_dt, parse_dt - +from newdle.core.db import db +from newdle.core.util import AutoNameEnum, format_dt, parse_dt CODE_ALPHABET = '23456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ' @@ -158,7 +157,7 @@ def answers(self, value): def __repr__(self): return ''.format( - self.id, self.name, ' ({})'.format(self.email) if self.email else '' + self.id, self.name, f' ({self.email})' if self.email else '' ) diff --git a/newdle/notifications.py b/newdle/notifications.py index 8f51bbe8c..229002df0 100644 --- a/newdle/notifications.py +++ b/newdle/notifications.py @@ -1,7 +1,7 @@ from flask import current_app, g, render_template, url_for -from .vendor.django_mail import get_connection -from .vendor.django_mail.message import EmailMultiAlternatives +from newdle.vendor.django_mail import get_connection +from newdle.vendor.django_mail.message import EmailMultiAlternatives def send_invitation_emails(newdle, participants=None): diff --git a/newdle/providers/free_busy/exchange.py b/newdle/providers/free_busy/exchange.py index 640c151fa..982e0315a 100644 --- a/newdle/providers/free_busy/exchange.py +++ b/newdle/providers/free_busy/exchange.py @@ -19,9 +19,8 @@ from oauthlib.oauth2.rfc6749.tokens import OAuth2Token from werkzeug.exceptions import ServiceUnavailable -from ...core.util import find_overlap -from .util import get_msal_token - +from newdle.core.util import find_overlap +from newdle.providers.free_busy.util import get_msal_token TYPE_MAP = {'Busy': 'busy', 'Tentative': 'busy', 'OOF': 'busy'} diff --git a/newdle/providers/free_busy/ox.py b/newdle/providers/free_busy/ox.py index 0470f1442..c39fa7d1e 100644 --- a/newdle/providers/free_busy/ox.py +++ b/newdle/providers/free_busy/ox.py @@ -6,7 +6,7 @@ from flask import current_app from requests.models import HTTPBasicAuth -from ...core.util import find_overlap +from newdle.core.util import find_overlap def fetch_free_busy(date, tz, uid, email): @@ -52,10 +52,10 @@ def fetch_free_busy(date, tz, uid, email): # this includes BUSY, BUSY-UNAVAILABLE and BUSY-TENTATIVE if line.startswith('FREEBUSY;FBTYPE=BUSY'): try: - start_dt, end_dt = [ + start_dt, end_dt = ( datetime.strptime(dt, '%Y%m%dT%H%M%S%z') for dt in line.split(':')[1].split('/') - ] + ) except IndexError: continue diff --git a/newdle/providers/free_busy/random.py b/newdle/providers/free_busy/random.py index 80e1dadfb..a342a8566 100644 --- a/newdle/providers/free_busy/random.py +++ b/newdle/providers/free_busy/random.py @@ -4,7 +4,7 @@ import pytz -from ...core.util import find_overlap +from newdle.core.util import find_overlap def fetch_free_busy(date, tz, uid, email): diff --git a/newdle/providers/free_busy/util.py b/newdle/providers/free_busy/util.py index 2325eaeaf..3d783970c 100644 --- a/newdle/providers/free_busy/util.py +++ b/newdle/providers/free_busy/util.py @@ -2,7 +2,6 @@ from flask import current_app - try: from msal import PublicClientApplication, SerializableTokenCache diff --git a/newdle/schemas.py b/newdle/schemas.py index d1f02819a..6b9f4a0e0 100644 --- a/newdle/schemas.py +++ b/newdle/schemas.py @@ -12,15 +12,15 @@ from marshmallow_enum import EnumField from pytz import common_timezones_set -from .core.marshmallow import mm -from .core.util import ( +from newdle.core.marshmallow import mm +from newdle.core.util import ( DATETIME_FORMAT, avatar_payload_from_participant, avatar_payload_from_user_info, check_user_signature, sign_user, ) -from .models import Availability +from newdle.models import Availability class UserSchema(mm.Schema): diff --git a/newdle/vendor/django_mail/__init__.py b/newdle/vendor/django_mail/__init__.py index 2dd662ab2..bd3cf23e6 100644 --- a/newdle/vendor/django_mail/__init__.py +++ b/newdle/vendor/django_mail/__init__.py @@ -4,7 +4,7 @@ # The code in here is taken almost verbatim from `django.core.mail`, # which is licensed under the three-clause BSD license and is originally # available on the following URL: -# https://github.com/django/django/blob/stable/2.2.x/django/core/mail/__init__.py +# https://github.com/django/django/blob/425b26092f/django/core/mail/__init__.py # Credits of the original code go to the Django Software Foundation # and their contributors. @@ -16,7 +16,6 @@ from .backends.base import BaseEmailBackend from .module_loading_utils import import_string - __all__ = ['get_connection'] diff --git a/newdle/vendor/django_mail/backends/base.py b/newdle/vendor/django_mail/backends/base.py index d9d504de8..6668a6da6 100644 --- a/newdle/vendor/django_mail/backends/base.py +++ b/newdle/vendor/django_mail/backends/base.py @@ -1,7 +1,7 @@ # The code in here is taken almost verbatim from `django.core.mail.backends.base`, # which is licensed under the three-clause BSD license and is originally # available on the following URL: -# https://github.com/django/django/blob/stable/3.1.x/django/core/mail/backends/base.py +# https://github.com/django/django/blob/425b26092f/django/core/mail/backends/base.py # Credits of the original code go to the Django Software Foundation # and their contributors. diff --git a/newdle/vendor/django_mail/backends/console.py b/newdle/vendor/django_mail/backends/console.py index ddd002597..5f98593e5 100644 --- a/newdle/vendor/django_mail/backends/console.py +++ b/newdle/vendor/django_mail/backends/console.py @@ -1,7 +1,7 @@ # The code in here is taken almost verbatim from `django.core.mail.backends.console`, # which is licensed under the three-clause BSD license and is originally # available on the following URL: -# https://github.com/django/django/blob/stable/3.1.x/django/core/mail/backends/console.py +# https://github.com/django/django/blob/425b26092f/django/core/mail/backends/console.py # Credits of the original code go to the Django Software Foundation # and their contributors. diff --git a/newdle/vendor/django_mail/backends/locmem.py b/newdle/vendor/django_mail/backends/locmem.py index 5279f6995..e6eb5515c 100644 --- a/newdle/vendor/django_mail/backends/locmem.py +++ b/newdle/vendor/django_mail/backends/locmem.py @@ -1,7 +1,7 @@ # The code in here is taken almost verbatim from `django.core.mail.backends.locmem`, # which is licensed under the three-clause BSD license and is originally # available on the following URL: -# https://github.com/django/django/blob/stable/3.1.x/django/core/mail/backends/locmem.py +# https://github.com/django/django/blob/425b26092f/django/core/mail/backends/locmem.py # Credits of the original code go to the Django Software Foundation # and their contributors. @@ -9,6 +9,8 @@ Backend for test environment. """ +import copy + from newdle.vendor import django_mail from .base import BaseEmailBackend @@ -34,6 +36,6 @@ def send_messages(self, messages): msg_count = 0 for message in messages: # .message() triggers header validation message.message() - django_mail.outbox.append(message) + django_mail.outbox.append(copy.deepcopy(message)) msg_count += 1 return msg_count diff --git a/newdle/vendor/django_mail/backends/smtp.py b/newdle/vendor/django_mail/backends/smtp.py index ae21bd7e0..e6191ed36 100644 --- a/newdle/vendor/django_mail/backends/smtp.py +++ b/newdle/vendor/django_mail/backends/smtp.py @@ -1,7 +1,7 @@ # The code in here is taken almost verbatim from `django.core.mail.backends.smtp`, # which is licensed under the three-clause BSD license and is originally # available on the following URL: -# https://github.com/django/django/blob/stable/3.1.x/django/core/mail/backends/smtp.py +# https://github.com/django/django/blob/425b26092f/django/core/mail/backends/smtp.py # Credits of the original code go to the Django Software Foundation # and their contributors. @@ -11,6 +11,7 @@ import threading from flask import current_app +from werkzeug.utils import cached_property from ..encoding_utils import DEFAULT_CHARSET from ..message import sanitize_address @@ -67,8 +68,8 @@ def __init__( ) if self.use_ssl and self.use_tls: raise ValueError( - "EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set " - "one of those settings to True." + 'EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set ' + 'one of those settings to True.' ) self.connection = None self._lock = threading.RLock() @@ -77,6 +78,15 @@ def __init__( def connection_class(self): return smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP + @cached_property + def ssl_context(self): + if self.ssl_certfile or self.ssl_keyfile: + ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) + ssl_context.load_cert_chain(self.ssl_certfile, self.ssl_keyfile) + return ssl_context + else: + return ssl.create_default_context() + def open(self): """ Ensure an open connection to the email server. Return whether or not a @@ -93,9 +103,7 @@ def open(self): if self.timeout is not None: connection_params['timeout'] = self.timeout if self.use_ssl: - connection_params.update( - {'keyfile': self.ssl_keyfile, 'certfile': self.ssl_certfile} - ) + connection_params['context'] = self.ssl_context try: self.connection = self.connection_class( self.host, self.port, **connection_params @@ -104,9 +112,7 @@ def open(self): # TLS/SSL are mutually exclusive, so only attempt TLS over # non-secure connections. if not self.use_ssl and self.use_tls: - self.connection.starttls( - keyfile=self.ssl_keyfile, certfile=self.ssl_certfile - ) + self.connection.starttls(context=self.ssl_context) if self.username and self.password: self.connection.login(self.username, self.password) return True @@ -147,12 +153,14 @@ def send_messages(self, email_messages): # Trying to send would be pointless. return 0 num_sent = 0 - for message in email_messages: - sent = self._send(message) - if sent: - num_sent += 1 - if new_conn_created: - self.close() + try: + for message in email_messages: + sent = self._send(message) + if sent: + num_sent += 1 + finally: + if new_conn_created: + self.close() return num_sent def _send(self, email_message): diff --git a/newdle/vendor/django_mail/encoding_utils.py b/newdle/vendor/django_mail/encoding_utils.py index 8a18fd14c..f6e486ed1 100644 --- a/newdle/vendor/django_mail/encoding_utils.py +++ b/newdle/vendor/django_mail/encoding_utils.py @@ -1,33 +1,28 @@ # The code in here is taken almost verbatim from `django.utils.encoding`, # which is licensed under the three-clause BSD license and is originally # available on the following URL: -# https://github.com/django/django/blob/stable/3.1.x/django/utils/encoding.py +# https://github.com/django/django/blob/425b26092f/django/utils/encoding.py # Credits of the original code go to the Django Software Foundation # and their contributors. - import datetime from decimal import Decimal - +from types import NoneType DEFAULT_CHARSET = 'utf-8' class DjangoUnicodeDecodeError(UnicodeDecodeError): - def __init__(self, obj, *args): - self.obj = obj - super().__init__(*args) - def __str__(self): return '%s. You passed in %r (%s)' % ( super().__str__(), - self.obj, - type(self.obj), + self.object, + type(self.object), ) _PROTECTED_TYPES = ( - type(None), + NoneType, int, float, Decimal, @@ -41,7 +36,7 @@ def is_protected_type(obj): """Determine if the object instance is of a protected type. Objects of protected types are preserved as-is when passed to - force_text(strings_only=True). + force_str(strings_only=True). """ return isinstance(obj, _PROTECTED_TYPES) @@ -50,6 +45,7 @@ def force_str(s, encoding='utf-8', strings_only=False, errors='strict'): """ Similar to smart_str(), except that lazy instances are resolved to strings, rather than kept as lazy objects. + If strings_only is True, don't convert (some) non-string-like objects. """ # Handle the common case first for performance reasons. @@ -63,7 +59,7 @@ def force_str(s, encoding='utf-8', strings_only=False, errors='strict'): else: s = str(s) except UnicodeDecodeError as e: - raise DjangoUnicodeDecodeError(s, *e.args) + raise DjangoUnicodeDecodeError(*e.args) from None return s diff --git a/newdle/vendor/django_mail/message.py b/newdle/vendor/django_mail/message.py index 69b4a68db..f0b65cfcc 100644 --- a/newdle/vendor/django_mail/message.py +++ b/newdle/vendor/django_mail/message.py @@ -1,7 +1,7 @@ # The code in here is taken almost verbatim from `django.core.mail.message`, # which is licensed under the three-clause BSD license and is originally # available on the following URL: -# https://github.com/django/django/blob/stable/3.1.x/django/core/mail/message.py +# https://github.com/django/django/blob/425b26092f/django/core/mail/message.py # Credits of the original code go to the Django Software Foundation # and their contributors. @@ -24,7 +24,6 @@ from .encoding_utils import DEFAULT_CHARSET, force_str, punycode from .utils import DNS_NAME - # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from # some spam filters. utf8_charset = Charset.Charset('utf-8') @@ -43,7 +42,7 @@ class BadHeaderError(ValueError): pass -# Header names that contain structured address data (RFC #5322) +# Header names that contain structured address data (RFC 5322). ADDRESS_HEADERS = { 'from', 'sender', @@ -104,6 +103,8 @@ def sanitize_address(addr, encoding): domain = token.domain or '' else: nm, address = addr + if '@' not in address: + raise ValueError(f'Invalid address "{address}"') localpart, domain = address.rsplit('@', 1) address_parts = nm + localpart + domain @@ -173,7 +174,8 @@ def __setitem__(self, name, val): def set_payload(self, payload, charset=None): if charset == 'utf-8' and not isinstance(charset, Charset.Charset): has_long_lines = any( - len(line.encode()) > RFC5322_EMAIL_LINE_LENGTH_LIMIT + len(line.encode(errors='surrogateescape')) + > RFC5322_EMAIL_LINE_LENGTH_LIMIT for line in payload.splitlines() ) # Quoted-Printable encoding has the side effect of shortening long @@ -317,11 +319,15 @@ def attach(self, filename=None, content=None, mimetype=None): mimetype to DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content. """ if isinstance(filename, MIMEBase): - assert content is None - assert mimetype is None + if content is not None or mimetype is not None: + raise ValueError( + 'content and mimetype must not be given when a MIMEBase ' + 'instance is provided.' + ) self.attachments.append(filename) + elif content is None: + raise ValueError('content must be provided.') else: - assert content is not None mimetype = ( mimetype or mimetypes.guess_type(filename)[0] @@ -385,8 +391,8 @@ def _create_mime_attachment(self, content, mimetype): encoding = self.encoding or DEFAULT_CHARSET attachment = SafeMIMEText(content, subtype, encoding) elif basetype == 'message' and subtype == 'rfc822': - # Bug #18967: per RFC2046 s5.2.1, message/rfc822 attachments - # must not be base64 encoded. + # Bug #18967: Per RFC 2046 Section 5.2.1, message/rfc822 + # attachments must not be base64 encoded. if isinstance(content, EmailMessage): # convert content into an email.Message first content = content.message() @@ -475,8 +481,8 @@ def __init__( def attach_alternative(self, content, mimetype): """Attach an alternative content representation.""" - assert content is not None - assert mimetype is not None + if content is None or mimetype is None: + raise ValueError('Both content and mimetype must be provided.') self.alternatives.append((content, mimetype)) def _create_message(self, msg): diff --git a/newdle/vendor/django_mail/module_loading_utils.py b/newdle/vendor/django_mail/module_loading_utils.py index aa3466057..2f58475e9 100644 --- a/newdle/vendor/django_mail/module_loading_utils.py +++ b/newdle/vendor/django_mail/module_loading_utils.py @@ -1,7 +1,7 @@ # The code in here is taken almost verbatim from `django.utils.module_loading`, # which is licensed under the three-clause BSD license and is originally # available on the following URL: -# https://github.com/django/django/blob/stable/2.2.x/django/utils/module_loading.py +# https://github.com/django/django/blob/425b26092f/django/utils/module_loading.py # Credits of the original code go to the Django Software Foundation # and their contributors. diff --git a/newdle/vendor/django_mail/utils.py b/newdle/vendor/django_mail/utils.py index 948c42445..3665e818b 100644 --- a/newdle/vendor/django_mail/utils.py +++ b/newdle/vendor/django_mail/utils.py @@ -1,7 +1,7 @@ # The code in here is taken almost verbatim from `django.core.mail.utils`, # which is licensed under the three-clause BSD license and is originally # available on the following URL: -# https://github.com/django/django/blob/stable/3.1.x/django/core/mail/utils.py +# https://github.com/django/django/blob/425b26092f/django/core/mail/utils.py # Credits of the original code go to the Django Software Foundation # and their contributors. diff --git a/newdle/wsgi.py b/newdle/wsgi.py index 6dbd19dfb..83e1c552a 100644 --- a/newdle/wsgi.py +++ b/newdle/wsgi.py @@ -1,4 +1,3 @@ from newdle.core.app import create_app - app = create_app() diff --git a/package.json b/package.json index 97a550763..91b63857d 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,12 @@ "scripts": { "prettier": "prettier --write 'newdle/client/src/**/*.{js,jsx,json,scss,css}'", "prettier:check": "prettier --check 'newdle/client/src/**/*.{js,jsx,json,scss,css}'", - "isort": "isort setup.py conftest.py ci/ newdle/ tests/", - "isort:check": "npm run isort -- --diff --check-only", - "black": "black setup.py conftest.py ci/ newdle/ tests/", - "black:check": "npm run black -- --diff --check", + "ruff-format": "ruff format setup.py conftest.py ci/ newdle/ tests/", + "ruff-format:check": "npm run ruff-format -- --diff --check", "eslint": "cd newdle/client && npx eslint --fix --max-warnings 0 src/", "eslint:specific": "cd newdle/client && npx eslint --fix --max-warnings 0", "eslint:check": "cd newdle/client && npx eslint --max-warnings 0 src/", - "flake8": "flake8 conftest.py ci/ newdle/ tests/", + "ruff": "ruff check conftest.py ci/ newdle/ tests/", "pydeps:update": "pip-compile --strip-extras --resolver=backtracking -q -U && pip-compile --strip-extras --resolver=backtracking -q -U requirements.dev.in && pip-compile --strip-extras --resolver=backtracking -q -U requirements.exchange.in && pip-compile --strip-extras --resolver=backtracking -q -U requirements.cern.in", "pydeps:lock": "pip-compile --strip-extras --resolver=backtracking -q && pip-compile --strip-extras --resolver=backtracking -q requirements.dev.in && pip-compile --strip-extras --resolver=backtracking -q requirements.exchange.in && pip-compile --strip-extras --resolver=backtracking -q requirements.cern.in" }, @@ -25,8 +23,8 @@ "git add newdle/client/src/locales/" ], "{newdle,tests,ci}/**/*.py": [ - "isort", - "black" + "ruff check --fix", + "ruff format" ] }, "husky": { diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 209c6ec88..000000000 --- a/pyproject.toml +++ /dev/null @@ -1,21 +0,0 @@ -[tool.black] -target_version = ['py39'] -skip-string-normalization = true -exclude = ''' -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project [black defaults] - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ - | node_modules - | newdle/client/ -) -''' diff --git a/requirements.cern.txt b/requirements.cern.txt index 4acb3e97d..7c31c8e87 100644 --- a/requirements.cern.txt +++ b/requirements.cern.txt @@ -1,19 +1,19 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --strip-extras requirements.cern.in # -authlib==1.2.1 +authlib==1.3.0 # via # -c requirements.txt # flask-multipass -blinker==1.7.0 +blinker==1.8.2 # via # -c requirements.txt # flask # flask-multipass -certifi==2023.7.22 +certifi==2024.2.2 # via # -c requirements.txt # requests @@ -29,43 +29,38 @@ click==8.1.7 # via # -c requirements.txt # flask -cryptography==41.0.6 +cryptography==42.0.7 # via # -c requirements.txt # authlib -flask==3.0.0 +flask==3.0.3 # via # -c requirements.txt # flask-multipass -flask-multipass==0.5.1 +flask-multipass==0.5.4 # via # -c requirements.txt - # flask-multipass # flask-multipass-cern flask-multipass-cern==2.2.9 # via -r requirements.cern.in -idna==3.4 +idna==3.7 # via # -c requirements.txt # requests -importlib-metadata==6.8.0 - # via - # -c requirements.txt - # flask -itsdangerous==2.1.2 +itsdangerous==2.2.0 # via # -c requirements.txt # flask -jinja2==3.1.3 +jinja2==3.1.4 # via # -c requirements.txt # flask -markupsafe==2.1.3 +markupsafe==2.1.5 # via # -c requirements.txt # jinja2 # werkzeug -pycparser==2.21 +pycparser==2.22 # via # -c requirements.txt # cffi @@ -73,16 +68,12 @@ requests==2.31.0 # via # -c requirements.txt # flask-multipass -urllib3==2.1.0 +urllib3==2.2.1 # via # -c requirements.txt # flask-multipass-cern # requests -werkzeug==3.0.1 +werkzeug==3.0.3 # via # -c requirements.txt # flask -zipp==3.17.0 - # via - # -c requirements.txt - # importlib-metadata diff --git a/requirements.dev.in b/requirements.dev.in index 53ef1996f..46164ca63 100644 --- a/requirements.dev.in +++ b/requirements.dev.in @@ -1,13 +1,10 @@ -c requirements.txt -black -flake8 -flake8-quotes flask-shell-ipython ipython -isort pip-tools pytest pytest-cov pytest-mock pytest-snapshot +ruff wheel diff --git a/requirements.dev.txt b/requirements.dev.txt index 466be0df1..9a4659495 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,165 +1,117 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --strip-extras requirements.dev.in # asttokens==2.4.1 # via stack-data -black==23.11.0 - # via -r requirements.dev.in -blinker==1.7.0 +blinker==1.8.2 # via # -c requirements.txt # flask -build==1.0.3 +build==1.2.1 # via pip-tools click==8.1.7 # via # -c requirements.txt - # black # flask # flask-shell-ipython # pip-tools -coverage==7.3.2 - # via - # coverage - # pytest-cov +coverage==7.5.1 + # via pytest-cov decorator==5.1.1 # via ipython -exceptiongroup==1.1.3 - # via - # ipython - # pytest executing==2.0.1 # via stack-data -flake8==6.1.0 - # via - # -r requirements.dev.in - # flake8-quotes -flake8-quotes==3.3.2 - # via -r requirements.dev.in -flask==3.0.0 +flask==3.0.3 # via # -c requirements.txt # flask-shell-ipython flask-shell-ipython==0.5.1 # via -r requirements.dev.in -importlib-metadata==6.8.0 - # via - # -c requirements.txt - # build - # flask iniconfig==2.0.0 # via pytest -ipython==8.17.2 +ipython==8.24.0 # via # -r requirements.dev.in # flask-shell-ipython -isort==5.12.0 - # via -r requirements.dev.in -itsdangerous==2.1.2 +itsdangerous==2.2.0 # via # -c requirements.txt # flask jedi==0.19.1 # via ipython -jinja2==3.1.3 +jinja2==3.1.4 # via # -c requirements.txt # flask -markupsafe==2.1.3 +markupsafe==2.1.5 # via # -c requirements.txt # jinja2 # werkzeug -matplotlib-inline==0.1.6 +matplotlib-inline==0.1.7 # via ipython -mccabe==0.7.0 - # via flake8 -mypy-extensions==1.0.0 - # via black -packaging==23.2 +packaging==24.0 # via # -c requirements.txt - # black # build # pytest -parso==0.8.3 +parso==0.8.4 # via jedi -pathspec==0.11.2 - # via black -pexpect==4.8.0 +pexpect==4.9.0 # via ipython -pip-tools==7.3.0 +pip-tools==7.4.1 # via -r requirements.dev.in -platformdirs==4.0.0 - # via black -pluggy==1.3.0 +pluggy==1.5.0 # via pytest -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via ipython ptyprocess==0.7.0 # via pexpect pure-eval==0.2.2 # via stack-data -pycodestyle==2.11.1 - # via flake8 -pyflakes==3.1.0 - # via flake8 -pygments==2.16.1 +pygments==2.18.0 # via ipython -pyproject-hooks==1.0.0 - # via build -pytest==7.4.3 +pyproject-hooks==1.1.0 + # via + # build + # pip-tools +pytest==8.2.0 # via # -r requirements.dev.in # pytest-cov # pytest-mock # pytest-snapshot -pytest-cov==4.1.0 +pytest-cov==5.0.0 # via -r requirements.dev.in -pytest-mock==3.12.0 +pytest-mock==3.14.0 # via -r requirements.dev.in pytest-snapshot==0.9.0 # via -r requirements.dev.in +ruff==0.4.3 + # via -r requirements.dev.in six==1.16.0 # via # -c requirements.txt # asttokens stack-data==0.6.3 # via ipython -tomli==2.0.1 - # via - # black - # build - # coverage - # pip-tools - # pyproject-hooks - # pytest -traitlets==5.13.0 +traitlets==5.14.3 # via # ipython # matplotlib-inline -typing-extensions==4.8.0 - # via - # -c requirements.txt - # black - # ipython -wcwidth==0.2.10 +wcwidth==0.2.13 # via prompt-toolkit -werkzeug==3.0.1 +werkzeug==3.0.3 # via # -c requirements.txt # flask -wheel==0.41.3 +wheel==0.43.0 # via # -r requirements.dev.in # pip-tools -zipp==3.17.0 - # via - # -c requirements.txt - # importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements.exchange.txt b/requirements.exchange.txt index da4740499..8f5ae130a 100644 --- a/requirements.exchange.txt +++ b/requirements.exchange.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --strip-extras requirements.exchange.in # cached-property==1.5.2 # via exchangelib -certifi==2023.7.22 +certifi==2024.2.2 # via # -c requirements.txt # requests @@ -18,7 +18,7 @@ charset-normalizer==3.3.2 # via # -c requirements.txt # requests -cryptography==41.0.6 +cryptography==42.0.7 # via # -c requirements.txt # msal @@ -27,29 +27,29 @@ cryptography==41.0.6 # requests-ntlm defusedxml==0.7.1 # via exchangelib -dnspython==2.4.2 +dnspython==2.6.1 # via exchangelib -exchangelib==5.1.0 +exchangelib==5.3.0 # via -r requirements.exchange.in -idna==3.4 +idna==3.7 # via # -c requirements.txt # requests isodate==0.6.1 # via exchangelib -lxml==4.9.3 +lxml==5.2.1 # via exchangelib -msal==1.25.0 +msal==1.28.0 # via -r requirements.exchange.in oauthlib==3.2.2 # via # exchangelib # requests-oauthlib -pycparser==2.21 +pycparser==2.22 # via # -c requirements.txt # cffi -pygments==2.16.1 +pygments==2.18.0 # via exchangelib pyjwt==2.8.0 # via @@ -66,19 +66,19 @@ requests==2.31.0 # requests-oauthlib requests-ntlm==1.2.0 # via exchangelib -requests-oauthlib==1.3.1 +requests-oauthlib==2.0.0 # via exchangelib six==1.16.0 # via # -c requirements.txt # isodate -tzdata==2023.3 +tzdata==2024.1 # via exchangelib tzlocal==3.0 # via # -r requirements.exchange.in # exchangelib -urllib3==2.1.0 +urllib3==2.2.1 # via # -c requirements.txt # requests diff --git a/requirements.txt b/requirements.txt index e0744618b..348254ae4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,22 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --strip-extras # -alembic==1.12.1 +alembic==1.13.1 # via # -r requirements.in # flask-migrate -authlib==1.2.1 +authlib==1.3.0 # via flask-multipass -blinker==1.7.0 +blinker==1.8.2 # via # flask # flask-multipass cachelib==0.9.0 # via flask-caching -certifi==2023.7.22 +certifi==2024.2.2 # via requests cffi==1.16.0 # via cryptography @@ -26,11 +26,11 @@ click==8.1.7 # via # flask # flask-url-map-serializer -cryptography==41.0.6 +cryptography==42.0.7 # via authlib -faker==20.0.3 +faker==25.0.1 # via -r requirements.in -flask==3.0.0 +flask==3.0.3 # via # -r requirements.in # flask-caching @@ -39,13 +39,13 @@ flask==3.0.0 # flask-multipass # flask-sqlalchemy # flask-url-map-serializer -flask-caching==2.1.0 +flask-caching==2.3.0 # via -r requirements.in -flask-marshmallow==0.15.0 +flask-marshmallow==1.2.1 # via -r requirements.in -flask-migrate==4.0.5 +flask-migrate==4.0.7 # via -r requirements.in -flask-multipass==0.5.1 +flask-multipass==0.5.4 # via -r requirements.in flask-sqlalchemy==3.1.1 # via @@ -53,30 +53,28 @@ flask-sqlalchemy==3.1.1 # flask-migrate flask-url-map-serializer==0.0.1 # via -r requirements.in -greenlet==3.0.1 +greenlet==3.0.3 # via sqlalchemy -icalendar==5.0.11 +icalendar==5.0.12 # via -r requirements.in -idna==3.4 +idna==3.7 # via requests -importlib-metadata==6.8.0 - # via flask -itsdangerous==2.1.2 +itsdangerous==2.2.0 # via # -r requirements.in # flask -jinja2==3.1.3 +jinja2==3.1.4 # via # -r requirements.in # flask -mako==1.3.0 +mako==1.3.3 # via alembic -markupsafe==2.1.3 +markupsafe==2.1.5 # via # jinja2 # mako # werkzeug -marshmallow==3.20.1 +marshmallow==3.21.2 # via # -r requirements.in # flask-marshmallow @@ -85,25 +83,23 @@ marshmallow==3.20.1 # webargs marshmallow-enum==1.5.1 # via -r requirements.in -marshmallow-sqlalchemy==0.29.0 +marshmallow-sqlalchemy==1.0.0 # via -r requirements.in -packaging==23.2 +packaging==24.0 # via - # flask-marshmallow # marshmallow - # marshmallow-sqlalchemy # webargs psycopg2==2.9.9 # via -r requirements.in -pycparser==2.21 +pycparser==2.22 # via cffi -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via # faker # icalendar -python-dotenv==1.0.0 +python-dotenv==1.0.1 # via -r requirements.in -pytz==2023.3.post1 +pytz==2024.1 # via # -r requirements.in # icalendar @@ -113,25 +109,23 @@ requests==2.31.0 # flask-multipass six==1.16.0 # via python-dateutil -sqlalchemy==2.0.23 +sqlalchemy==2.0.30 # via # -r requirements.in # alembic # flask-sqlalchemy # marshmallow-sqlalchemy -typing-extensions==4.8.0 +typing-extensions==4.11.0 # via # alembic # sqlalchemy -urllib3==2.1.0 +urllib3==2.2.1 # via requests -webargs==8.3.0 +webargs==8.4.0 # via -r requirements.in -werkzeug==3.0.1 +werkzeug==3.0.3 # via # -r requirements.in # flask -xlsxwriter==3.1.9 +xlsxwriter==3.2.0 # via -r requirements.in -zipp==3.17.0 - # via importlib-metadata diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 000000000..a66f69c04 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,149 @@ +target-version = 'py312' + +[lint] +preview = true + +select = [ + 'E', # pycodestyle + 'F', # pyflakes + 'N', # pep8-naming + 'Q', # flake8-quotes + 'RUF', # ruff + 'UP', # pyupgrade + 'D', # pydocstyle + 'S', # flake8-bandit + 'C4', # flake8-comprehensions + 'INT', # flake8-gettext + 'LOG', # flake8-logging + 'B', # flake8-bugbear + 'A001', # flake8-builtins + 'COM', # flake8-commas + 'T10', # flake8-debugger + 'EXE', # flake8-executable + 'ISC', # flake8-implicit-str-concat + 'PIE', # flake8-pie + 'PT', # flake8-pytest-style + 'RSE', # flake8-raise + 'RET504', # flake8-return + 'SIM', # flake8-simplify + 'TID', # flake8-tidy-imports + 'PGH', # pygrep-hooks + 'PL', # pylint + 'TRY', # tryceratops + 'PERF', # perflint + 'FURB', # refurb + 'I', # isort +] +ignore = [ + 'E226', # allow omitting whitespace around arithmetic operators + 'E731', # allow assigning lambdas (it's useful for single-line functions defined inside other functions) + 'N818', # not all our exceptions are errors + 'RUF012', # ultra-noisy and dicts in classvars are very common + 'RUF015', # not always more readable, and we don't do it for huge lists + 'RUF022', # autofix messes up our formatting instead of just sorting + 'RUF027', # also triggers on i18n functions -> too noisy for now + 'UP038', # it looks kind of weird and it slower than a tuple + 'D205', # too many docstrings which have no summary line + 'D301', # https://github.com/astral-sh/ruff/issues/8696 + 'D1', # we have way too many missing docstrings :( + 'D412', # we do not use section, and in click docstrings those blank lines are useful + 'S101', # we use asserts outside tests, and do not run python with `-O` (also see B011) + 'S113', # enforcing timeouts would likely require config in some places - maybe later + 'S311', # false positives, it does not care about the context + 'S324', # all our md5/sha1 usages are for non-security purposes + 'S404', # useless, triggers on *all* subprocess imports + 'S403', # there's already a warning on using pickle, no need to have one for the import + 'S405', # we don't use lxml in unsafe ways + 'S603', # useless, triggers on *all* subprocess calls: https://github.com/astral-sh/ruff/issues/4045 + 'S607', # we trust the PATH to be sane + 'B011', # we don't run python with `-O` (also see S101) + 'B904', # possibly useful but too noisy + 'PIE807', # `lambda: []` is much clearer for `load_default` in schemas + 'PT004', # pretty weird + not a pytest convention: https://github.com/astral-sh/ruff/issues/8796 + 'PT005', # ^ likewise + 'PT011', # very noisy + 'PT015', # nice for tests but not so nice elsewhere + 'PT018', # ^ likewise + 'SIM102', # sometimes nested ifs are more readable + 'SIM103', # sometimes this is more readable (especially when checking multiple conditions) + 'SIM105', # try-except-pass is faster and people are used to it + 'SIM108', # noisy ternary + 'SIM114', # sometimes separate ifs are more readable (especially if they just return a bool) + 'SIM117', # nested context managers may be more readable + 'PLC0415', # local imports are there for a reason + 'PLC2701', # some private imports are needed + 'PLR09', # too-many- is just noisy + 'PLR0913', # very noisy + 'PLR2004', # extremely noisy and generally annoying + 'PLR6201', # sets are faster (by a factor of 10!) but it's noisy and we're in nanoseconds territory + 'PLR6301', # extremely noisy and generally annoying + 'PLW0108', # a lambda often makes it more clear what you actually want + 'PLW1510', # we often do not care about the status code of commands + 'PLW1514', # we expect UTF8 environments everywhere + 'PLW1641', # false positives with SA comparator classes + 'PLW2901', # noisy and reassigning to the loop var is usually intentional + 'TRY002', # super noisy, and those exceptions are pretty exceptional anyway + 'TRY003', # super noisy and also useless w/ werkzeugs http exceptions + 'TRY300', # kind of strange in many cases + 'TRY301', # sometimes doing that is actually useful + 'TRY400', # not all exceptions need exception logging + 'PERF203', # noisy, false positives, and not applicable for 3.11+ + 'FURB113', # less readable + 'FURB140', # less readable and actually slower in 3.12+ + 'PLW0603', # globals are fine here + 'COM812', # formatter conflict + 'ISC001', # formatter conflict +] + +extend-safe-fixes = [ + 'RUF005', # we typically don't deal with objects overriding `__add__` ir `__radd__` + 'C4', # they seem pretty safe + 'UP008', # ^ likewise + 'D200', # ^ likewise + 'D400', # ^ likewise + 'PT014', # duplicate test case parametrizations are never intentional + 'RSE102', # we do not use `raise func()` (with `func` returning the exception instance) + 'RET504', # looks pretty safe + 'SIM110', # ^ likewise + 'PERF102', # ^ likewise +] + +[format] +quote-style = 'single' +exclude = ['newdle/vendor/*.py'] + +[lint.flake8-builtins] +builtins-ignorelist = ['id', 'format', 'input', 'type', 'credits'] + +[lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false +parametrize-names-type = 'tuple' +parametrize-values-type = 'tuple' +parametrize-values-row-type = 'tuple' + +[lint.flake8-tidy-imports] +ban-relative-imports = 'all' + +[lint.flake8-quotes] +inline-quotes = 'single' +multiline-quotes = 'double' +docstring-quotes = 'double' +avoid-escape = true + +[lint.pep8-naming] +classmethod-decorators = [ + 'classmethod', + 'declared_attr', + 'expression', + 'comparator', +] + +[lint.pydocstyle] +convention = 'pep257' + +[lint.per-file-ignores] +# vendored code - don't care about most stuff in there +'newdle/vendor/*.py' = ['N', 'F841', 'UP', 'D', 'PIE', 'TID252', 'TRY'] +# allow long lines in migrations (only do that for raw SQL please) +'newdle/migrations/versions/*.py' = ['E501', 'D400'] diff --git a/setup.cfg b/setup.cfg index 09c5dd080..42d1d55ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,17 +13,4 @@ url = https://github.com/indico/newdle packages = find: zip_safe = false include_package_data = true -python_requires = ~=3.9 - - -[flake8] -exclude=newdle/client,__pycache__ -max-line-length=89 - -inline-quotes = single -multiline-quotes = single -docstring-quotes = double -avoid-escape = true - -per-file-ignores = - newdle/vendor/django_mail/**/*.py:Q +python_requires = ~=3.12 diff --git a/tests/models_test.py b/tests/models_test.py index 8204e94e5..d84c47308 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -83,7 +83,7 @@ def test_code_generation(db_session, monkeypatch): # simulate a situation in which there is a collision with the code # of an existing newdle - class _MockRandom(object): + class _MockRandom: def __init__(self): self.collision = True