Skip to content

Commit

Permalink
Merge pull request #598 from MAKENTNU/cleanup/python-3.9-and-3.10-syntax
Browse files Browse the repository at this point in the history
Refactor code to use Python 3.9 and 3.10 syntax
  • Loading branch information
ddabble committed Feb 26, 2023
2 parents 090a6a5 + 9c7f85f commit a70723a
Show file tree
Hide file tree
Showing 44 changed files with 204 additions and 209 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
# Retries installing files 3 times, as the GitHub Actions CI is occasionally unreliable
run: |
sudo apt update
sudo apt install python-dev libldap2-dev libsasl2-dev libssl-dev -o Acquire::Retries=3
sudo apt install python3-dev libldap2-dev libsasl2-dev libssl-dev -o Acquire::Retries=3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [ 3.8, 3.9, "3.10" ]
python-version: [ "3.10", "3.11" ]

steps:
- uses: actions/checkout@v3
Expand All @@ -25,7 +25,7 @@ jobs:
# Retries installing files 3 times, as the GitHub Actions CI is occasionally unreliable
run: |
sudo apt update
sudo apt install python-dev libldap2-dev libsasl2-dev libssl-dev -o Acquire::Retries=3
sudo apt install python3-dev libldap2-dev libsasl2-dev libssl-dev -o Acquire::Retries=3
- name: Install Python Dependencies
run: |
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ A summary of changes made to the codebase, grouped per deployment.

### Other changes

- Set minimum required Python version to 3.10
- Never-ending masses of code cleanup


Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM python:3.10
FROM python:3.11
ENV PYTHONUNBUFFERED 1
RUN apt update --fix-missing && apt install -y python-dev libldap2-dev libsasl2-dev libssl-dev gettext libgettextpo-dev
RUN apt update --fix-missing && apt install -y python3-dev libldap2-dev libsasl2-dev libssl-dev gettext libgettextpo-dev
RUN mkdir /web
WORKDIR /web
COPY requirements.txt /web/
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

### Prerequisites

* Python 3.8+ (latest stable version preferred)
* Python 3.10+ (latest stable version preferred)
* Having cloned this repository to your machine
* For most purposes, check out [the `dev` branch](https://github.com/MAKENTNU/web/tree/dev), as it's the base branch for all development:
```shell
Expand All @@ -35,7 +35,7 @@
[the "Configure a virtual environment" guide](https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html#python_create_virtual_env)
* Otherwise, `cd` to the project folder, and run:
```shell
[newest installed Python command, like python3.10] -m venv ../venv
[newest installed Python command, like python3.11] -m venv ../venv
```
* Activate the virtual environment:
* If using PyCharm, this should be done automatically when opening a terminal tab inside the IDE
Expand Down
20 changes: 11 additions & 9 deletions card/modelfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ def formfield(self, **kwargs):
})

def get_prep_value(self, value):
if isinstance(value, CardNumber):
return str(value.number)
elif isinstance(value, str):
value = value.strip()
# Only try to remove an EM prefix if the string is not just whitespace
if value:
return value.split()[-1] # Remove possible EM prefix
# `value` is either None or not an acceptable value
return None
match value:
case CardNumber():
return str(value.number)
case str():
value = value.strip()
# Only try to remove an EM prefix if the string is not just whitespace
if value:
return value.split()[-1] # Remove possible EM prefix
case _:
# `value` is either None or not an acceptable value
return None

def from_db_value(self, value, expression, connection):
if value:
Expand Down
2 changes: 1 addition & 1 deletion checkin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def get_context_data(self, **kwargs):
return context


@dataclass
@dataclass(kw_only=True)
# `[...]DataClass` might have been a better name, but `[...]Struct` is shorter
class CompletedCourseMessageStruct:
completed: bool
Expand Down
52 changes: 27 additions & 25 deletions contentbox/tests/test_urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import importlib
from dataclasses import dataclass
from collections.abc import Callable
from dataclasses import dataclass, field
from functools import cached_property
from http import HTTPStatus
from typing import Callable, List, Protocol, Tuple
from typing import Protocol
from urllib.parse import urlparse

from django.test import Client, TestCase
Expand Down Expand Up @@ -31,7 +32,7 @@ class ContentBoxAssertionStruct:
reverse_func: ReverseCallable
viewname: str
client: Client
should_be_bleached: bool
should_be_bleached: bool = field(kw_only=True)

@cached_property
def url(self) -> str:
Expand All @@ -56,22 +57,22 @@ def setUp(self):
)

self.content_box_assertion_structs = (
ContentBoxAssertionStruct(reverse_main, 'about', self.main_client, True),
ContentBoxAssertionStruct(reverse_main, 'contact', self.main_client, True),
ContentBoxAssertionStruct(reverse_main, 'apply', self.main_client, True),
ContentBoxAssertionStruct(reverse_main, 'cookies', self.main_client, True),
ContentBoxAssertionStruct(reverse_main, 'privacypolicy', self.main_client, True),

ContentBoxAssertionStruct(reverse_main, 'makerspace', self.main_client, True),
ContentBoxAssertionStruct(reverse_main, 'rules', self.main_client, True),

ContentBoxAssertionStruct(reverse_internal, 'home', self.internal_client, False),
ContentBoxAssertionStruct(reverse_internal, 'dev-board', self.internal_client, False),
ContentBoxAssertionStruct(reverse_internal, 'event-board', self.internal_client, True),
ContentBoxAssertionStruct(reverse_internal, 'mentor-board', self.internal_client, True),
ContentBoxAssertionStruct(reverse_internal, 'pr-board', self.internal_client, True),

ContentBoxAssertionStruct(reverse_internal, 'make-history', self.internal_client, True),
ContentBoxAssertionStruct(reverse_main, 'about', self.main_client, should_be_bleached=True),
ContentBoxAssertionStruct(reverse_main, 'contact', self.main_client, should_be_bleached=True),
ContentBoxAssertionStruct(reverse_main, 'apply', self.main_client, should_be_bleached=True),
ContentBoxAssertionStruct(reverse_main, 'cookies', self.main_client, should_be_bleached=True),
ContentBoxAssertionStruct(reverse_main, 'privacypolicy', self.main_client, should_be_bleached=True),

ContentBoxAssertionStruct(reverse_main, 'makerspace', self.main_client, should_be_bleached=True),
ContentBoxAssertionStruct(reverse_main, 'rules', self.main_client, should_be_bleached=True),

ContentBoxAssertionStruct(reverse_internal, 'home', self.internal_client, should_be_bleached=False),
ContentBoxAssertionStruct(reverse_internal, 'dev-board', self.internal_client, should_be_bleached=False),
ContentBoxAssertionStruct(reverse_internal, 'event-board', self.internal_client, should_be_bleached=True),
ContentBoxAssertionStruct(reverse_internal, 'mentor-board', self.internal_client, should_be_bleached=True),
ContentBoxAssertionStruct(reverse_internal, 'pr-board', self.internal_client, should_be_bleached=True),

ContentBoxAssertionStruct(reverse_internal, 'make-history', self.internal_client, should_be_bleached=True),
)

def get_content_box_from_url(self, url: str, client: Client) -> ContentBox:
Expand All @@ -91,7 +92,7 @@ def test_content_box_url_count_is_as_expected(self):

# Code based on https://stackoverflow.com/a/19162337
@staticmethod
def get_all_content_box_url_patterns() -> List[URLPattern]:
def get_all_content_box_url_patterns() -> list[URLPattern]:
patterns = []

def check_urls(urlpatterns, prefix=''):
Expand Down Expand Up @@ -255,14 +256,15 @@ def escaped(s: str):
self.assert_content_is_bleached_expectedly_when_posted(original_content__bleached_content__tuples,
struct.should_be_bleached, struct.url, content_box, struct.client)

def assert_content_is_bleached_expectedly_when_posted(self, original_content__bleached_content__tuples: List[Tuple[str, str]],
def assert_content_is_bleached_expectedly_when_posted(self, original_content__bleached_content__tuples: list[tuple[str, str]],
should_be_bleached: bool, url: str, content_box: ContentBox, client: Client):
change_url = reverse('contentbox_edit', args=[content_box.pk])
for original_content, bleached_content in original_content__bleached_content__tuples:
response = client.post(change_url, {
**{subwidget_name: subwidget_name.title() for subwidget_name in MultiLingualTextEdit.get_subwidget_names('title')},
**{subwidget_name: original_content for subwidget_name in MultiLingualTextEdit.get_subwidget_names('content')},
})
response = client.post(
change_url,
{subwidget_name: subwidget_name.title() for subwidget_name in MultiLingualTextEdit.get_subwidget_names('title')}
| {subwidget_name: original_content for subwidget_name in MultiLingualTextEdit.get_subwidget_names('content')},
)
# `url` contains a relative scheme and a host, so only compare the paths
self.assertRedirects(response, urlparse(url).path)
content_box.refresh_from_db()
Expand Down
12 changes: 6 additions & 6 deletions contentbox/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Optional, Type
from typing import Type
from urllib.parse import urlparse

from django.contrib.auth.models import Permission
Expand Down Expand Up @@ -67,10 +67,10 @@ def test_edit_page_contains_correct_error_messages(self):
self.client.force_login(user)

def assert_response_contains_error_message(posted_content: str, error: bool):
data = {
**{subwidget_name: subwidget_name.title() for subwidget_name in MultiLingualTextEdit.get_subwidget_names('title')},
**{subwidget_name: posted_content for subwidget_name in MultiLingualTextEdit.get_subwidget_names('content')},
}
data = (
{subwidget_name: subwidget_name.title() for subwidget_name in MultiLingualTextEdit.get_subwidget_names('title')}
| {subwidget_name: posted_content for subwidget_name in MultiLingualTextEdit.get_subwidget_names('content')}
)
response = self.client.post(self.edit_url1, data=data)
# The form will redirect if valid, and stay on the same page if not
self.assertEqual(response.status_code, HTTPStatus.OK if error else HTTPStatus.FOUND)
Expand All @@ -84,7 +84,7 @@ def test_edit_view_has_expected_form_class(self):
user.add_perms('contentbox.change_contentbox')
self.client.force_login(user)

def assert_edit_page_response_with(*, status_code: int, form: Optional[Type[BaseForm]]):
def assert_edit_page_response_with(*, status_code: int, form: Type[BaseForm] | None):
response = self.client.get(self.edit_url1)
self.assertEqual(response.status_code, status_code)
if form:
Expand Down
14 changes: 6 additions & 8 deletions dataporten/ldap_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"""
Note: querying NTNU's LDAP server requires connection to NTNU's VPN.
"""
from typing import Dict, List, Tuple

import ldap

from users.models import User
Expand All @@ -23,7 +21,7 @@
STANDARD_USER_DETAILS_FIELDS = ('username', 'email', 'full_name')


def ldap_search(search_field: str, search_value: str) -> List[Tuple[str, Dict[str, List[bytes]]]]:
def ldap_search(search_field: str, search_value: str) -> list[tuple[str, dict[str, list[bytes]]]]:
"""
Searches the LDAP server given by LDAP_HOST with the filter ``search_field=search_value``.
Expand All @@ -36,7 +34,7 @@ def ldap_search(search_field: str, search_value: str) -> List[Tuple[str, Dict[st
return ldap_obj.search_s(LDAP_BASE, ldap.SCOPE_SUBTREE, query)


def get_ldap_field(ldap_data: List[Tuple[str, Dict[str, List[bytes]]]], field: str) -> str:
def get_ldap_field(ldap_data: list[tuple[str, dict[str, list[bytes]]]], field: str) -> str:
"""
Retrieves the value of a field in ``ldap_data``.
Expand All @@ -47,7 +45,7 @@ def get_ldap_field(ldap_data: List[Tuple[str, Dict[str, List[bytes]]]], field: s
return ldap_data[0][1].get(LDAP_FIELDS[field], [b''])[0].decode()


def get_user_details_from_ldap(search_field: str, search_value: str) -> Dict[str, str]:
def get_user_details_from_ldap(search_field: str, search_value: str) -> dict[str, str]:
"""
Retrieves all relevant user details from LDAP.
Searches the LDAP server given by LDAP_HOST with the filter ``search_field=search_value``.
Expand All @@ -60,7 +58,7 @@ def get_user_details_from_ldap(search_field: str, search_value: str) -> Dict[str
return {}


def _get_user_details_from_user_field(field_name: str, field_value: str, use_cached: bool) -> Dict[str, str]:
def _get_user_details_from_user_field(field_name: str, field_value: str, use_cached: bool) -> dict[str, str]:
user_details = {}
if use_cached:
user = User.objects.filter(**{field_name: field_value}).first()
Expand Down Expand Up @@ -93,7 +91,7 @@ def _get_user_details_from_user_field(field_name: str, field_value: str, use_cac
return user_details


def get_user_details_from_username(username: str, use_cached=True) -> Dict[str, str]:
def get_user_details_from_username(username: str, use_cached=True) -> dict[str, str]:
"""
Retrieves details for user given by username, either from database or LDAP server.
Expand All @@ -104,7 +102,7 @@ def get_user_details_from_username(username: str, use_cached=True) -> Dict[str,
return _get_user_details_from_user_field('username', username, use_cached)


def get_user_details_from_email(email: str, use_cached=True) -> Dict[str, str]:
def get_user_details_from_email(email: str, use_cached=True) -> dict[str, str]:
"""
Retrieves details for user given by email, either from database or LDAP server.
Expand Down
3 changes: 1 addition & 2 deletions dataporten/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import uuid
from typing import Tuple

from django.contrib.auth import get_user
from django.http import HttpRequest
Expand Down Expand Up @@ -99,7 +98,7 @@ def assert_original_user1_values():
)

@staticmethod
def create_social_user(username, email_username, first_and_last_name: Tuple[str, str],
def create_social_user(username, email_username, first_and_last_name: tuple[str, str],
*, ldap_full_name, social_data_fullname):
first_name, last_name = first_and_last_name
user = User.objects.create_user(
Expand Down
30 changes: 16 additions & 14 deletions internal/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,26 +131,28 @@ def clean(self):
return cleaned_data

member = self.instance
if status_action == self.StatusAction.UNDO_QUIT and not member.quit:
raise forms.ValidationError(
_("Member's “quit” status was not undone, as the member did not have the status “quit”."),
code='warning_message',
)
elif status_action == self.StatusAction.UNDO_RETIRE and not member.retired:
raise forms.ValidationError(
_("Member's retirement was not undone, as the member did not have the status “retired”."),
code='warning_message',
)
match status_action:
case self.StatusAction.UNDO_QUIT if not member.quit:
raise forms.ValidationError(
_("Member's “quit” status was not undone, as the member did not have the status “quit”."),
code='warning_message',
)
case self.StatusAction.UNDO_RETIRE if not member.retired:
raise forms.ValidationError(
_("Member's retirement was not undone, as the member did not have the status “retired”."),
code='warning_message',
)
return cleaned_data

def save(self, commit=True):
member = super().save(commit=False)
status_action = self.cleaned_data['status_action']

if status_action == self.StatusAction.UNDO_QUIT:
member.set_quit(False)
elif status_action == self.StatusAction.UNDO_RETIRE:
member.set_retirement(False)
match status_action:
case self.StatusAction.UNDO_QUIT:
member.set_quit(False)
case self.StatusAction.UNDO_RETIRE:
member.set_retirement(False)
member.save()
return member

Expand Down
13 changes: 7 additions & 6 deletions internal/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ def member_update_user_groups(instance: Member, action, pk_set=None, **kwargs):
"""
if action in {'pre_add', 'pre_remove'}:
committees = Committee.objects.filter(pk__in=pk_set)
if action == 'pre_add':
for committee in committees:
committee.group.user_set.add(instance.user)
elif action == 'pre_remove':
for committee in committees:
committee.group.user_set.remove(instance.user)
match action:
case 'pre_add':
for committee in committees:
committee.group.user_set.add(instance.user)
case 'pre_remove':
for committee in committees:
committee.group.user_set.remove(instance.user)


def connect():
Expand Down
18 changes: 9 additions & 9 deletions internal/tests/test_urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from http import HTTPStatus
from typing import Set
from unittest import TestCase as StandardTestCase

from django.conf import settings
Expand Down Expand Up @@ -58,14 +57,15 @@ def setUp(self):

@staticmethod
def generic_request(client: Client, method: str, path: str, data: dict = None):
if method == 'GET':
return client.get(path)
elif method == 'POST':
return client.post(path, data)
else:
raise ValueError(f'Method "{method}" not supported')

def _test_url_permissions(self, method: str, path: str, data: dict = None, *, allowed_clients: Set[Client], expected_redirect_path: str = None):
match method:
case 'GET':
return client.get(path)
case 'POST':
return client.post(path, data)
case _:
raise ValueError(f'Method "{method}" not supported')

def _test_url_permissions(self, method: str, path: str, data: dict = None, *, allowed_clients: set[Client], expected_redirect_path: str = None):
disallowed_clients = self.all_clients - allowed_clients
for client in disallowed_clients:
response = self.generic_request(client, method, path)
Expand Down
2 changes: 1 addition & 1 deletion internal/validators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from typing import Collection
from collections.abc import Collection

from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator, RegexValidator
Expand Down

0 comments on commit a70723a

Please sign in to comment.