diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a00f93baf..91e0889e9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,7 @@ tests: - virtualenv-3.4 env - source env/bin/activate - cd src - - XDG_CACHE_HOME=/cache pip3 install -r requirements.txt -r requirements/dev.txt + - XDG_CACHE_HOME=/cache pip3 install -r requirements.txt -r requirements/dev.txt -r requirements/py34.txt - flake8 --ignore=E123,F403,F401,N802,C901,W503 . - isort -c -rc . - python3 manage.py check diff --git a/.travis.yml b/.travis.yml index f75905c33..4400f6d00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - "3.4" install: - pip install -U pip wheel - - pip install -q -r src/requirements.txt -r src/requirements/dev.txt + - pip install -q -r src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt - npm install -g less@2.5.0 before_script: - cd src diff --git a/deployment/docker/standalone/Dockerfile b/deployment/docker/standalone/Dockerfile index 1ecd4aef9..cd1a932d1 100644 --- a/deployment/docker/standalone/Dockerfile +++ b/deployment/docker/standalone/Dockerfile @@ -23,7 +23,8 @@ RUN git clone --recursive --depth 1 https://github.com/pretix/pretix.git /pretix WORKDIR /pretix/src RUN pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \ - -r requirements/memcached.txt -r requirements/celery.txt -r requirements/redis.txt gunicorn + -r requirements/memcached.txt -r requirements/celery.txt -r requirements/redis.txt \ + -r requirements/py34.txt gunicorn RUN make diff --git a/src/pretix/base/cache.py b/src/pretix/base/cache.py index c913a13bb..e61915f73 100644 --- a/src/pretix/base/cache.py +++ b/src/pretix/base/cache.py @@ -3,11 +3,12 @@ import time from django.core.cache import caches from django.db.models import Model +from typing import Dict, List class NamespacedCache: - def __init__(self, prefixkey, cache: str='default'): + def __init__(self, prefixkey: str, cache: str='default'): self.cache = caches[cache] self.prefixkey = prefixkey @@ -30,7 +31,7 @@ class NamespacedCache: def _strip_prefix(self, key: str) -> str: return key.split(":", 2 + self.prefixkey.count(":"))[-1] - def clear(self): + def clear(self) -> None: try: prefix = self.cache.incr(self.prefixkey, 1) except ValueError: @@ -43,14 +44,14 @@ class NamespacedCache: def get(self, key: str) -> str: return self.cache.get(self._prefix_key(key)) - def get_many(self, keys: "list[str]") -> "dict[str, str]": + def get_many(self, keys: List[str]) -> Dict[str, str]: values = self.cache.get_many([self._prefix_key(key) for key in keys]) newvalues = {} for k, v in values.items(): newvalues[self._strip_prefix(k)] = v return newvalues - def set_many(self, values: "dict[str, str]", timeout=3600): + def set_many(self, values: Dict[str, str], timeout=3600): newvalues = {} for k, v in values.items(): newvalues[self._prefix_key(k)] = v @@ -59,7 +60,7 @@ class NamespacedCache: def delete(self, key: str): # NOQA return self.cache.delete(self._prefix_key(key)) - def delete_many(self, keys: "list[str]"): # NOQA + def delete_many(self, keys: List[str]): # NOQA return self.cache.delete_many([self._prefix_key(key) for key in keys]) def incr(self, key: str, by: int=1): # NOQA @@ -86,6 +87,6 @@ class ObjectRelatedCache(NamespacedCache): times as you want. """ - def __init__(self, obj, cache: str='default'): + def __init__(self, obj: Model, cache: str='default'): assert isinstance(obj, Model) super().__init__('%s:%s' % (obj._meta.object_name, obj.pk), cache) diff --git a/src/pretix/base/exporter.py b/src/pretix/base/exporter.py index 1b081009d..2be7444d3 100644 --- a/src/pretix/base/exporter.py +++ b/src/pretix/base/exporter.py @@ -2,6 +2,7 @@ import json from django.core.serializers.json import DjangoJSONEncoder from django.dispatch import receiver +from typing import Tuple from pretix.base.signals import register_data_exporters @@ -60,7 +61,7 @@ class BaseExporter: """ return {} - def render(self, form_data: dict) -> tuple: + def render(self, form_data: dict) -> Tuple[str, str, str]: """ Render the exported file and return a tuple consisting of a filename, a file type and file content. diff --git a/src/pretix/base/forms/auth.py b/src/pretix/base/forms/auth.py index f3c96dfcf..d5ec586b5 100644 --- a/src/pretix/base/forms/auth.py +++ b/src/pretix/base/forms/auth.py @@ -43,7 +43,7 @@ class LoginForm(forms.Form): return self.cleaned_data - def confirm_login_allowed(self, user): + def confirm_login_allowed(self, user: User): """ Controls whether the given User may log in. This is a policy setting, independent of end-user authentication. This default behavior is to diff --git a/src/pretix/base/i18n.py b/src/pretix/base/i18n.py index fdca837d3..f4d82e71f 100644 --- a/src/pretix/base/i18n.py +++ b/src/pretix/base/i18n.py @@ -6,6 +6,7 @@ from django.conf import settings from django.db.models import SubfieldBase, TextField from django.utils import translation from django.utils.safestring import mark_safe +from typing import Dict, List class LazyI18nString: @@ -13,7 +14,7 @@ class LazyI18nString: This represents an internationalized string that is/was/will be stored in the database. """ - def __init__(self, data): + def __init__(self, data: Dict[str, str]): """ Input data should be a dictionary which maps language codes to content. """ @@ -26,7 +27,7 @@ class LazyI18nString: else: self.data = j - def __str__(self): + def __str__(self) -> str: """ Evaluate the given string with respect to the currently active locale. This will rather return you a string in a wrong language than give you an @@ -53,10 +54,10 @@ class LazyI18nString: else: return str(self.data) - def __repr__(self): + def __repr__(self) -> str: return '' % repr(self.data) - def __lt__(self, other): # NOQA + def __lt__(self, other) -> bool: # NOQA return str(self) < str(other) @@ -68,7 +69,7 @@ class I18nWidget(forms.MultiWidget): """ widget = forms.TextInput - def __init__(self, langcodes, field, attrs=None): + def __init__(self, langcodes: List[str], field: forms.Field, attrs=None): widgets = [] self.langcodes = langcodes self.enabled_langcodes = langcodes diff --git a/src/pretix/base/middleware.py b/src/pretix/base/middleware.py index 55b2756d0..580eaeadd 100644 --- a/src/pretix/base/middleware.py +++ b/src/pretix/base/middleware.py @@ -3,6 +3,7 @@ from collections import OrderedDict import pytz from django.conf import settings from django.core.urlresolvers import get_script_prefix +from django.http import HttpRequest, HttpResponse from django.utils import timezone, translation from django.utils.cache import patch_vary_headers from django.utils.translation import LANGUAGE_SESSION_KEY @@ -21,7 +22,7 @@ class LocaleMiddleware: for a request. """ - def process_request(self, request): + def process_request(self, request: HttpRequest): language = get_language_from_request(request) if hasattr(request, 'event') and not request.path.startswith(get_script_prefix() + 'control'): if language not in request.event.settings.locales: @@ -51,7 +52,7 @@ class LocaleMiddleware: else: timezone.deactivate() - def process_response(self, request, response): + def process_response(self, request: HttpRequest, response: HttpResponse): language = translation.get_language() patch_vary_headers(response, ('Accept-Language',)) if 'Content-Language' not in response: @@ -59,14 +60,14 @@ class LocaleMiddleware: return response -def get_language_from_user_settings(request) -> str: +def get_language_from_user_settings(request: HttpRequest) -> str: if request.user.is_authenticated(): lang_code = request.user.locale if lang_code in _supported and lang_code is not None and check_for_language(lang_code): return lang_code -def get_language_from_session_or_cookie(request) -> str: +def get_language_from_session_or_cookie(request: HttpRequest) -> str: if hasattr(request, 'session'): lang_code = request.session.get(LANGUAGE_SESSION_KEY) if lang_code in _supported and lang_code is not None and check_for_language(lang_code): @@ -79,7 +80,7 @@ def get_language_from_session_or_cookie(request) -> str: pass -def get_language_from_event(request) -> str: +def get_language_from_event(request: HttpRequest) -> str: if hasattr(request, 'event'): lang_code = request.event.settings.locale try: @@ -88,7 +89,7 @@ def get_language_from_event(request) -> str: pass -def get_language_from_browser(request) -> str: +def get_language_from_browser(request: HttpRequest) -> str: accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '') for accept_lang, unused in parse_accept_lang_header(accept): if accept_lang == '*': @@ -110,7 +111,7 @@ def get_default_language(): return settings.LANGUAGE_CODE -def get_language_from_request(request) -> str: +def get_language_from_request(request: HttpRequest) -> str: """ Analyzes the request to find what language the user wants the system to show. Only languages listed in settings.LANGUAGES are taken into account. diff --git a/src/pretix/base/migrations/0004_auto_20151024_0848.py b/src/pretix/base/migrations/0004_auto_20151024_0848.py index 5d12fd750..50d566d44 100644 --- a/src/pretix/base/migrations/0004_auto_20151024_0848.py +++ b/src/pretix/base/migrations/0004_auto_20151024_0848.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index b3e1ef6c8..aef87c5d6 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -12,13 +12,13 @@ class UserManager(BaseUserManager): model documentation to see what's so special about our user model. """ - def create_user(self, email, password=None, **kwargs): + def create_user(self, email: str, password: str=None, **kwargs): user = self.model(email=email, **kwargs) user.set_password(password) user.save() return user - def create_superuser(self, email, password=None): # NOQA + def create_superuser(self, email: str, password: str=None): # NOQA # Not used in the software but required by Django if password is None: raise Exception("You must provide a password") diff --git a/src/pretix/base/models/base.py b/src/pretix/base/models/base.py index 6302df8d5..9dfc0fd4e 100644 --- a/src/pretix/base/models/base.py +++ b/src/pretix/base/models/base.py @@ -1,5 +1,6 @@ import copy import uuid +from datetime import datetime import six from django.db import models @@ -12,7 +13,7 @@ class Versionable(BaseVersionable): class Meta: abstract = True - def clone_shallow(self, forced_version_date=None): + def clone_shallow(self, forced_version_date: datetime=None): """ This behaves like clone(), but misses all the Many2Many-relation-handling. This is a performance optimization for cases in which we have to handle the Many2Many relations @@ -63,7 +64,7 @@ class Versionable(BaseVersionable): }) -def cachedfile_name(instance, filename): +def cachedfile_name(instance, filename: str) -> str: return 'cachedfiles/%s.%s' % (instance.id, filename.split('.')[-1]) diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 0eeabbecd..e55b2575c 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -1,4 +1,6 @@ import sys +from datetime import datetime +from decimal import Decimal from itertools import product from django.db import models @@ -6,6 +8,7 @@ from django.db.models import Q, Case, Count, Sum, When from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ +from typing import List, Tuple, Union from versions.models import VersionedForeignKey, VersionedManyToManyField from pretix.base.i18n import I18nCharField, I18nTextField @@ -61,11 +64,11 @@ class ItemCategory(Versionable): def sortkey(self): return self.position, self.version_birth_date - def __lt__(self, other): + def __lt__(self, other) -> bool: return self.sortkey < other.sortkey -def itempicture_upload_to(instance, filename): +def itempicture_upload_to(instance, filename: str) -> str: return '%s/%s/item-%s.%s' % ( instance.event.organizer.slug, instance.event.slug, instance.identity, filename.split('.')[-1] @@ -170,7 +173,7 @@ class Item(Versionable): if self.event: self.event.get_cache().clear() - def get_all_variations(self, use_cache: bool=False) -> "list[VariationDict]": + def get_all_variations(self, use_cache: bool=False) -> List[VariationDict]: """ This method returns a list containing all variations of this item. The list contains one VariationDict per variation, where @@ -438,10 +441,10 @@ class PropertyValue(Versionable): self.prop.event.get_cache().clear() @property - def sortkey(self): + def sortkey(self) -> Tuple[int, datetime]: return self.position, self.version_birth_date - def __lt__(self, other): + def __lt__(self, other) -> bool: return self.sortkey < other.sortkey @@ -508,7 +511,7 @@ class ItemVariation(Versionable): if self.item: self.item.event.get_cache().clear() - def check_quotas(self): + def check_quotas(self) -> Tuple[int, int]: """ This method is used to determine whether this ItemVariation is currently available for sale in terms of quotas. @@ -518,7 +521,7 @@ class ItemVariation(Versionable): return min([q.availability() for q in self.quotas.all()], key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize)) - def to_variation_dict(self): + def to_variation_dict(self) -> VariationDict: """ :return: a :py:class:`VariationDict` representing this variation. """ @@ -528,7 +531,7 @@ class ItemVariation(Versionable): vd['variation'] = self return vd - def check_restrictions(self): + def check_restrictions(self) -> Union[bool, Decimal]: """ This method is used to determine whether this ItemVariation is restricted in sale by any restriction plugins. @@ -806,7 +809,7 @@ class Quota(Versionable): if self.event: self.event.get_cache().clear() - def availability(self): + def availability(self) -> Tuple[int, int]: """ This method is used to determine whether Items or ItemVariations belonging to this quota should currently be available for sale. @@ -864,7 +867,7 @@ class Quota(Versionable): return o @cached_property - def _position_lookup(self): + def _position_lookup(self) -> Q: return ( ( # Orders for items which do not have any variations Q(variation__isnull=True) diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index ccf24ef57..b9ff5c503 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -5,6 +5,7 @@ from datetime import datetime from django.db import models from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ +from typing import List, Union from versions.models import VersionedForeignKey from .base import CachedFile, Versionable @@ -133,7 +134,7 @@ class Order(Versionable): verbose_name_plural = _("Orders") ordering = ("-datetime",) - def str(self): + def __str__(self): return self.full_code @property @@ -160,7 +161,7 @@ class Order(Versionable): return @property - def can_modify_answers(self): + def can_modify_answers(self) -> bool: """ Is ``True`` if the user can change the question answers / attendee names that are related to the order. This checks order status and modification deadlines. It also @@ -187,7 +188,7 @@ class Order(Versionable): order.save() return order - def _can_be_paid(self): + def _can_be_paid(self) -> Union[bool, str]: error_messages = { 'late': _("The payment is too late to be accepted."), } @@ -202,7 +203,7 @@ class Order(Versionable): return self._is_still_available() - def _is_still_available(self): + def _is_still_available(self) -> Union[bool, str]: error_messages = { 'unavailable': _('Some of the ordered products were no longer available.'), } @@ -339,7 +340,7 @@ class OrderPosition(ObjectWithAnswers, Versionable): verbose_name_plural = _("Order positions") @classmethod - def transform_cart_positions(cls, cp: list, order) -> list: + def transform_cart_positions(cls, cp: List, order) -> list: ops = [] for cartpos in cp: op = OrderPosition( diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py index 61e86a8e3..e038093cb 100644 --- a/src/pretix/base/models/organizer.py +++ b/src/pretix/base/models/organizer.py @@ -45,7 +45,7 @@ class Organizer(Versionable): verbose_name_plural = _("Organizers") ordering = ("name",) - def __str__(self): + def __str__(self) -> str: return self.name def save(self, *args, **kwargs): @@ -97,7 +97,7 @@ class OrganizerPermission(Versionable): verbose_name = _("Organizer permission") verbose_name_plural = _("Organizer permissions") - def __str__(self): + def __str__(self) -> str: return _("%(name)s on %(object)s") % { 'name': str(self.user), 'object': str(self.organizer), diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 9ffcf5468..42af2f0a8 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -9,8 +9,9 @@ from django.forms import Form from django.http import HttpRequest from django.template.loader import get_template from django.utils.translation import ugettext_lazy as _ +from typing import Any, Dict -from pretix.base.models import CartPosition, Order, Quota +from pretix.base.models import CartPosition, Event, Order, Quota from pretix.base.settings import SettingsSandbox from pretix.base.signals import register_payment_providers @@ -20,7 +21,7 @@ class BasePaymentProvider: This is the base class for all payment providers. """ - def __init__(self, event): + def __init__(self, event: Event): self.event = event self.settings = SettingsSandbox('payment', self.identifier, event) @@ -193,7 +194,7 @@ class BasePaymentProvider: """ raise NotImplementedError() # NOQA - def checkout_prepare(self, request: HttpRequest, cart: dict) -> "bool|str": + def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> "bool|str": """ Will be called after the user selected this provider as his payment method. If you provided a form to the user to enter payment data, this method should @@ -395,7 +396,7 @@ class FreeOrderProvider(BasePaymentProvider): def identifier(self) -> str: return "free" - def checkout_confirm_render(self, request) -> str: + def checkout_confirm_render(self, request: HttpRequest) -> str: return _("No payment is required as this order only includes products which are free of charge.") def order_pending_render(self, request: HttpRequest, order: Order) -> str: diff --git a/src/pretix/base/plugins.py b/src/pretix/base/plugins.py index bba900d38..c01fd9636 100644 --- a/src/pretix/base/plugins.py +++ b/src/pretix/base/plugins.py @@ -1,6 +1,7 @@ from enum import Enum from django.apps import apps +from typing import List class PluginType(Enum): @@ -10,7 +11,7 @@ class PluginType(Enum): EXPORT = 4 -def get_all_plugins() -> "List[class]": +def get_all_plugins() -> List[type]: """ Returns the PretixPluginMeta classes of all plugins found in the installed Django apps. """ diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 400a59185..748d1c96b 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -1,9 +1,10 @@ -from datetime import timedelta +from datetime import datetime, timedelta from django.conf import settings from django.db.models import Q from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ +from typing import List, Optional, Tuple from pretix.base.models import ( CartPosition, Event, EventLock, Item, ItemVariation, Quota, @@ -29,7 +30,7 @@ error_messages = { } -def _extend_existing(event, cart_id, expiry): +def _extend_existing(event: Event, cart_id: str, expiry: datetime) -> None: # Extend this user's cart session to 30 minutes from now to ensure all items in the # cart expire at the same time # We can extend the reservation of items which are not yet expired without risk @@ -38,7 +39,7 @@ def _extend_existing(event, cart_id, expiry): ).update(expires=expiry) -def _re_add_expired_positions(items, event, cart_id): +def _re_add_expired_positions(items: List[CartPosition], event: Event, cart_id: str) -> List[CartPosition]: positions = set() # For items that are already expired, we have to delete and re-add them, as they might # be no longer available or prices might have changed. Sorry! @@ -51,20 +52,21 @@ def _re_add_expired_positions(items, event, cart_id): return positions -def _delete_expired(expired): +def _delete_expired(expired: List[CartPosition]) -> None: for cp in expired: if cp.version_end_date is None: cp.delete() -def _check_date(event): +def _check_date(event: Event) -> None: if event.presale_start and now() < event.presale_start: raise CartError(error_messages['not_started']) if event.presale_end and now() > event.presale_end: raise CartError(error_messages['ended']) -def _add_items(event, items, cart_id, expiry): +def _add_items(event: Event, items: List[Tuple[str, Optional[str], int]], + cart_id: str, expiry: datetime) -> Optional[str]: err = None # Fetch items from the database @@ -129,7 +131,7 @@ def _add_items(event, items, cart_id, expiry): return err -def _add_items_to_cart(event: Event, items: list, cart_id: str=None): +def _add_items_to_cart(event: Event, items: List[Tuple[str, Optional[str], int]], cart_id: str=None) -> None: with event.lock(): _check_date(event) existing = CartPosition.objects.current.filter(Q(cart_id=cart_id) & Q(event=event)).count() @@ -150,7 +152,7 @@ def _add_items_to_cart(event: Event, items: list, cart_id: str=None): raise CartError(err) -def add_items_to_cart(event: str, items: list, cart_id: str=None): +def add_items_to_cart(event: str, items: List[Tuple[str, Optional[str], int]], cart_id: str=None) -> None: """ Adds a list of items to a user's cart. :param event: The event ID in question @@ -160,12 +162,12 @@ def add_items_to_cart(event: str, items: list, cart_id: str=None): """ event = Event.objects.current.get(identity=event) try: - return _add_items_to_cart(event, items, cart_id) + _add_items_to_cart(event, items, cart_id) except EventLock.LockTimeoutException: raise CartError(error_messages['busy']) -def remove_items_from_cart(event: str, items: list, cart_id: str=None): +def remove_items_from_cart(event: str, items: List[Tuple[str, Optional[str], int]], cart_id: str=None) -> None: """ Removes a list of items from a user's cart. :param event: The event ID in question @@ -188,10 +190,10 @@ if settings.HAS_CELERY: from pretix.celery import app @app.task(bind=True, max_retries=5, default_retry_delay=2) - def add_items_to_cart_task(self, event: str, items: list, cart_id: str): + def add_items_to_cart_task(self, event: str, items: List[Tuple[str, Optional[str], int]], cart_id: str): event = Event.objects.current.get(identity=event) try: - return _add_items_to_cart(event, items, cart_id) + _add_items_to_cart(event, items, cart_id) except EventLock.LockTimeoutException: self.retry(exc=CartError(error_messages['busy'])) diff --git a/src/pretix/base/services/export.py b/src/pretix/base/services/export.py index 90807a5cf..cd846c410 100644 --- a/src/pretix/base/services/export.py +++ b/src/pretix/base/services/export.py @@ -1,11 +1,12 @@ from django.conf import settings from django.core.files.base import ContentFile +from typing import Any, Dict from pretix.base.models import CachedFile, Event, cachedfile_name from pretix.base.signals import register_data_exporters -def export(event, fileid, provider, form_data): +def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) -> None: event = Event.objects.current.get(identity=event) file = CachedFile.objects.get(id=fileid) responses = register_data_exporters.send(event) diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index a074df708..edd448da2 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -5,6 +5,7 @@ from django.core.mail import EmailMessage from django.template.loader import get_template from django.utils import translation from django.utils.translation import ugettext as _ +from typing import Any, Dict from pretix.base.i18n import LazyI18nString from pretix.base.models import Event @@ -12,7 +13,8 @@ from pretix.base.models import Event logger = logging.getLogger('pretix.base.mail') -def mail(email: str, subject: str, template: str, context: dict=None, event: Event=None, locale: str=None): +def mail(email: str, subject: str, template: str, + context: Dict[str, Any]=None, event: Event=None, locale: str=None): """ Sends out an email to a user. @@ -58,7 +60,7 @@ def mail(email: str, subject: str, template: str, context: dict=None, event: Eve translation.activate(_lng) -def mail_send(to, subject, body, sender): +def mail_send(to: str, subject: str, body: str, sender: str) -> bool: email = EmailMessage(subject, body, sender, to=to) try: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 60f6d38eb..557cf62bf 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -4,6 +4,7 @@ from django.conf import settings from django.db import transaction from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ +from typing import List from pretix.base.models import ( CartPosition, Event, EventLock, Order, OrderPosition, Quota, @@ -30,7 +31,7 @@ error_messages = { def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None, - force: bool=False): + force: bool=False) -> Order: """ Marks an order as paid. This clones the order object, sets the payment provider, info and date and returns the cloned order object. @@ -82,14 +83,14 @@ class OrderError(Exception): pass -def _check_date(event): +def _check_date(event: Event): if event.presale_start and now() < event.presale_start: raise OrderError(error_messages['not_started']) if event.presale_end and now() > event.presale_end: raise OrderError(error_messages['ended']) -def _check_positions(event: Event, dt: datetime, positions: list): +def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]): err = None _check_date(event) @@ -135,7 +136,7 @@ def _check_positions(event: Event, dt: datetime, positions: list): @transaction.atomic() -def _create_order(event: Event, email: str, positions: list, dt: datetime, +def _create_order(event: Event, email: str, positions: List[CartPosition], dt: datetime, payment_provider: BasePaymentProvider, locale: str=None): total = sum([c.price for c in positions]) payment_fee = payment_provider.calculate_fee(total) @@ -159,7 +160,7 @@ def _create_order(event: Event, email: str, positions: list, dt: datetime, return order -def _perform_order(event: str, payment_provider: str, position_ids: list, +def _perform_order(event: str, payment_provider: str, position_ids: List[str], email: str, locale: str): event = Event.objects.current.get(identity=event) responses = register_payment_providers.send(event) @@ -197,7 +198,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: list, return order.identity -def perform_order(event: str, payment_provider: str, positions: list, +def perform_order(event: str, payment_provider: str, positions: List[str], email: str=None, locale: str=None): try: return _perform_order(event, payment_provider, positions, email, locale) @@ -211,7 +212,7 @@ if settings.HAS_CELERY: from pretix.celery import app @app.task(bind=True, max_retries=5, default_retry_delay=2) - def perform_order_task(self, event: str, payment_provider: str, positions: list, + def perform_order_task(self, event: str, payment_provider: str, positions: List[str], email: str=None, locale: str=None): try: return _perform_order(event, payment_provider, positions, email, locale) diff --git a/src/pretix/base/services/stats.py b/src/pretix/base/services/stats.py index 414449e1f..b91c3d6d0 100644 --- a/src/pretix/base/services/stats.py +++ b/src/pretix/base/services/stats.py @@ -1,7 +1,10 @@ +from decimal import Decimal + from django.db.models import Count, Sum from django.utils.translation import ugettext_lazy as _ +from typing import Any, Dict, Iterable, List, Tuple -from pretix.base.models import ItemCategory, Order, OrderPosition +from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition from pretix.base.signals import register_payment_providers @@ -10,14 +13,14 @@ class DummyObject: class Dontsum: - def __init__(self, value): + def __init__(self, value: Any): self.value = value - def __str__(self): + def __str__(self) -> str: return str(self.value) -def tuplesum(tuples): +def tuplesum(tuples: Iterable[Tuple]) -> Tuple: def mysum(it): sit = [i for i in it if not isinstance(i, Dontsum)] return sum(sit) @@ -25,7 +28,7 @@ def tuplesum(tuples): return tuple(map(mysum, zip(*list(tuples)))) -def order_overview(event): +def order_overview(event: Event) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]: items = event.items.all().select_related( 'category', # for re-grouping ).prefetch_related( diff --git a/src/pretix/base/services/tickets.py b/src/pretix/base/services/tickets.py index 3f11e6963..aefd0dbd7 100644 --- a/src/pretix/base/services/tickets.py +++ b/src/pretix/base/services/tickets.py @@ -8,7 +8,7 @@ from pretix.base.models import CachedFile, CachedTicket, Order, cachedfile_name from pretix.base.signals import register_ticket_outputs -def generate(order, provider): +def generate(order: str, provider: str): order = Order.objects.current.select_related('event').get(identity=order) ct = CachedTicket.objects.get_or_create(order=order, provider=provider)[0] if not ct.cachedfile: diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 26554a90b..a47c46533 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -7,6 +7,7 @@ from django.conf import settings from django.core.files import File from django.core.files.storage import default_storage from django.db.models import Model +from typing import Any, Callable, Dict, Optional, TypeVar, Union from versions.models import Versionable DEFAULTS = { @@ -109,23 +110,23 @@ class SettingsProxy: you. It will return None for non-existing properties. """ - def __init__(self, obj, parent=None, type=None): + def __init__(self, obj: Model, parent: Optional[Model]=None, type=None): self._obj = obj self._parent = parent self._cached_obj = None self._type = type - def _cache(self): + def _cache(self) -> Dict[str, Any]: if self._cached_obj is None: self._cached_obj = {} for setting in self._obj.setting_objects.current.all(): self._cached_obj[setting.key] = setting return self._cached_obj - def _flush(self): + def _flush(self) -> None: self._cached_obj = None - def _unserialize(self, value, as_type): + def _unserialize(self, value: str, as_type: type) -> Any: if as_type is not None and isinstance(value, as_type): return value elif value is None: @@ -155,7 +156,7 @@ class SettingsProxy: return as_type.objects.get(pk=value) return value - def _serialize(self, value): + def _serialize(self, value: Any) -> str: if isinstance(value, str): return value elif isinstance(value, int) or isinstance(value, float) \ @@ -174,7 +175,7 @@ class SettingsProxy: raise TypeError('Unable to serialize %s into a setting.' % str(type(value))) - def get(self, key, default=None, as_type=None): + def get(self, key: str, default: Any=None, as_type: type=None): """ Get a setting specified by key 'key'. Normally, settings are strings, but if you put non-strings into the settings object, you can request unserialization @@ -199,21 +200,21 @@ class SettingsProxy: return self._unserialize(value, as_type) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: return self.get(key) - def __getattr__(self, key): + def __getattr__(self, key: str) -> Any: return self.get(key) - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: if key.startswith('_'): return super().__setattr__(key, value) self.set(key, value) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: self.set(key, value) - def set(self, key, value): + def set(self, key: str, value: Any) -> None: if key in self._cache(): s = self._cache()[key] s = s.clone() @@ -223,12 +224,12 @@ class SettingsProxy: s.save() self._cache()[key] = s - def __delattr__(self, key): + def __delattr__(self, key: str) -> None: if key.startswith('_'): return super().__delattr__(key) return self.__delitem__(key) - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: if key in self._cache(): self._cache()[key].delete() del self._cache()[key] @@ -240,36 +241,36 @@ class SettingsSandbox: prefixes for you. """ - def __init__(self, type, key, event): + def __init__(self, type: str, key: str, event: Versionable): self._event = event self._type = type self._key = key - def _convert_key(self, key): + def _convert_key(self, key: str) -> str: return '%s_%s_%s' % (self._type, self._key, key) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: self.set(key, value) - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: if key.startswith('_'): return super().__setattr__(key, value) self.set(key, value) - def __getattr__(self, item): + def __getattr__(self, item: str) -> Any: return self.get(item) - def __getitem__(self, item): + def __getitem__(self, item: str) -> Any: return self.get(item) - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: del self._event.settings[self._convert_key(key)] - def __delattr__(self, key): + def __delattr__(self, key: str) -> None: del self._event.settings[self._convert_key(key)] - def get(self, key, default=None, as_type=str): + def get(self, key: str, default: Any=None, as_type: type=str): return self._event.settings.get(self._convert_key(key), default=default, as_type=as_type) - def set(self, key, value): + def set(self, key: str, value: Any): self._event.settings.set(self._convert_key(key), value) diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index 2af0e7848..a40fa8707 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -1,6 +1,7 @@ import django.dispatch from django.apps import apps from django.dispatch.dispatcher import NO_RECEIVERS +from typing import Any, Callable, List, Tuple from .models import Event @@ -12,7 +13,7 @@ class EventPluginSignal(django.dispatch.Signal): Event. """ - def send(self, sender, **named): + def send(self, sender: Event, **named) -> List[Tuple[Callable, Any]]: """ Send signal from sender to all connected receivers that belong to plugins enabled for the given Event. diff --git a/src/pretix/base/ticketoutput.py b/src/pretix/base/ticketoutput.py index 53b78b430..7f4deadd5 100644 --- a/src/pretix/base/ticketoutput.py +++ b/src/pretix/base/ticketoutput.py @@ -3,8 +3,9 @@ from collections import OrderedDict from django import forms from django.http import HttpRequest from django.utils.translation import ugettext_lazy as _ +from typing import Tuple -from pretix.base.models import Order +from pretix.base.models import Event, Order from pretix.base.settings import SettingsSandbox @@ -13,7 +14,7 @@ class BaseTicketOutput: This is the base class for all ticket outputs. """ - def __init__(self, event): + def __init__(self, event: Event): self.event = event self.settings = SettingsSandbox('ticketoutput', self.identifier, event) @@ -28,7 +29,7 @@ class BaseTicketOutput: """ return self.settings.get('_enabled', as_type=bool) - def generate(self, order: Order) -> tuple: + def generate(self, order: Order) -> Tuple[str, str, str]: """ This method should generate the download file and return a tuple consisting of a filename, a file type and file content. diff --git a/src/pretix/base/types.py b/src/pretix/base/types.py index 2492efe3f..0913b88c3 100644 --- a/src/pretix/base/types.py +++ b/src/pretix/base/types.py @@ -1,3 +1,6 @@ +from typing import Iterable, List, Tuple + + class VariationDict(dict): """ A VariationDict object behaves exactle the same as the Python built-in @@ -7,7 +10,7 @@ class VariationDict(dict): """ IGNORE_KEYS = ('variation', 'key', 'available', 'price') - def relevant_items(self) -> "list[(str, PropertyValue)]": + def relevant_items(self) -> Iterable[Tuple]: """ Iterate over all items with numeric keys. @@ -16,7 +19,7 @@ class VariationDict(dict): """ return (i for i in self.items() if i[0] not in self.IGNORE_KEYS) - def relevant_values(self) -> "list[PropertyValue]": + def relevant_values(self) -> Iterable["PropertyValue"]: """ Iterate over all values with numeric keys. @@ -60,14 +63,14 @@ class VariationDict(dict): else: return super().__eq__(other) - def empty(self): + def empty(self) -> bool: """ Returns true, if this VariationDict does not contain any "real" data like references to PropertyValues, but only "metadata". """ return not next(self.relevant_items(), False) - def ordered_values(self) -> "list[ItemVariation]": + def ordered_values(self) -> List["ItemVariation"]: """ Returns a list of values ordered by their keys """ @@ -79,7 +82,7 @@ class VariationDict(dict): ) ] - def __str__(self): + def __str__(self) -> str: return " – ".join([str(v.value) for v in self.ordered_values()]) def copy(self) -> "VariationDict": diff --git a/src/pretix/base/views/cachedfiles.py b/src/pretix/base/views/cachedfiles.py index 31684f786..2f9735646 100644 --- a/src/pretix/base/views/cachedfiles.py +++ b/src/pretix/base/views/cachedfiles.py @@ -1,4 +1,4 @@ -from django.http import HttpResponse +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.utils.functional import cached_property from django.views.generic import TemplateView @@ -10,10 +10,10 @@ class DownloadView(TemplateView): template_name = "pretixbase/cachedfiles/pending.html" @cached_property - def object(self): + def object(self) -> CachedFile: return get_object_or_404(CachedFile, id=self.kwargs['id']) - def get(self, request, *args, **kwargs): + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: if 'ajax' in request.GET: return HttpResponse('1' if self.object.file else '0') elif self.object.file: diff --git a/src/requirements/py34.txt b/src/requirements/py34.txt new file mode 100644 index 000000000..094af98f6 --- /dev/null +++ b/src/requirements/py34.txt @@ -0,0 +1,2 @@ +typing +