diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 98931c989..a2d036117 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,12 +3,12 @@ repos: - repo: local hooks: - - id: pylint - name: pylint - entry: pylint + - id: ruff + name: ruff + entry: ruff language: system types: [python] - args: ["-rn", "-sn", "--fail-under=5"] + args: ["check"] - id: prettier name: prettier entry: prettier --config frontend/.prettierrc.json --write --ignore-unknown diff --git a/modoboa/admin/api/v1/serializers.py b/modoboa/admin/api/v1/serializers.py index c2cd1b7f4..bd840b952 100644 --- a/modoboa/admin/api/v1/serializers.py +++ b/modoboa/admin/api/v1/serializers.py @@ -2,7 +2,6 @@ from typing import List -from django.conf import settings from django.contrib.auth import password_validation from django.core.exceptions import ValidationError from django.shortcuts import get_object_or_404 @@ -179,7 +178,7 @@ class DomainAliasSerializer(serializers.ModelSerializer): object_type="domain_aliases", ) except lib_exceptions.ModoboaException as inst: - raise serializers.ValidationError({"domain": force_str(inst)}) + raise serializers.ValidationError({"domain": force_str(inst)}) from None domain_alias.save(creator=creator) return domain_alias @@ -312,7 +311,7 @@ class AccountPasswordSerializer(serializers.ModelSerializer): try: password_validation.validate_password(value, self.instance) except ValidationError as exc: - raise serializers.ValidationError(exc.messages[0]) + raise serializers.ValidationError(exc.messages[0]) from None return value def update(self, instance, validated_data): @@ -364,7 +363,7 @@ class WritableAccountSerializer(AccountSerializer): try: validators.UTF8EmailValidator()(username) except ValidationError as err: - raise ValidationError({"username": err.message}) + raise ValidationError({"username": err.message}) from None mailbox = data.get("mailbox") if mailbox is None: if not self.instance: @@ -397,7 +396,9 @@ class WritableAccountSerializer(AccountSerializer): object_type="mailboxes", ) except lib_exceptions.ModoboaException as inst: - raise serializers.ValidationError({"mailbox": force_str(inst)}) + raise serializers.ValidationError( + {"mailbox": force_str(inst)} + ) from None condition = not data.get("random_password") and ( data.get("password") or not self.partial ) @@ -409,7 +410,9 @@ class WritableAccountSerializer(AccountSerializer): data["password"], self.instance ) except ValidationError as exc: - raise serializers.ValidationError({"password": exc.messages[0]}) + raise serializers.ValidationError( + {"password": exc.messages[0]} + ) from None elif not self.instance: raise serializers.ValidationError( {"password": _("This field is required.")} @@ -524,9 +527,9 @@ class AliasSerializer(serializers.ModelSerializer): value, self.context["request"].user, instance=self.instance ) except ValidationError as err: - raise serializers.ValidationError(err) + raise serializers.ValidationError(err) from None except AliasExists: - raise serializers.ValidationError(_("This alias already exists")) + raise serializers.ValidationError(_("This alias already exists")) from None return value.lower() def create(self, validated_data): diff --git a/modoboa/admin/api/v1/tests.py b/modoboa/admin/api/v1/tests.py index 69d3b0715..d443116de 100644 --- a/modoboa/admin/api/v1/tests.py +++ b/modoboa/admin/api/v1/tests.py @@ -161,7 +161,7 @@ class DomainAliasAPITestCase(ModoAPITestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(DomainAliasAPITestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() cls.dom_alias1 = factories.DomainAliasFactory( name="dalias1.com", target__name="test.com" @@ -185,7 +185,7 @@ class DomainAliasAPITestCase(ModoAPITestCase): self.assertEqual(response.data["name"], "dalias1.com") url = reverse("v1:domain_alias-list") - response = self.client.get("{}?domain=test.com".format(url)) + response = self.client.get(f"{url}?domain=test.com") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) @@ -246,7 +246,7 @@ class AccountAPITestCase(ModoAPITestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(AccountAPITestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() cls.da_token = Token.objects.create( user=core_models.User.objects.get(username="admin@test.com") @@ -271,17 +271,17 @@ class AccountAPITestCase(ModoAPITestCase): response = response.json() self.assertEqual(len(response), 5) - response = self.client.get("{}?domain=test.com".format(url)) + response = self.client.get(f"{url}?domain=test.com") self.assertEqual(response.status_code, 200) response = response.json() self.assertEqual(len(response), 2) - response = self.client.get("{}?domain=pouet.com".format(url)) + response = self.client.get(f"{url}?domain=pouet.com") self.assertEqual(response.status_code, 200) response = response.json() self.assertEqual(len(response), 0) - response = self.client.get("{}?search=user@test.com".format(url)) + response = self.client.get(f"{url}?search=user@test.com") self.assertEqual(response.status_code, 200) response = response.json() self.assertEqual(len(response), 1) @@ -608,7 +608,7 @@ class AliasAPITestCase(ModoAPITestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(AliasAPITestCase, cls).setUpTestData() + super().setUpTestData() cls.localconfig.parameters.set_value("enable_admin_limits", False, app="limits") cls.localconfig.save() factories.populate_database() @@ -624,7 +624,7 @@ class AliasAPITestCase(ModoAPITestCase): response = response.json() self.assertEqual(len(response), 3) - response = self.client.get("{}?domain=test.com".format(url)) + response = self.client.get(f"{url}?domain=test.com") self.assertEqual(response.status_code, 200) response = response.json() self.assertEqual(len(response), 3) @@ -744,7 +744,7 @@ class SenderAddressAPITestCase(ModoAPITestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(SenderAddressAPITestCase, cls).setUpTestData() + super().setUpTestData() cls.localconfig.parameters.set_value("enable_admin_limits", False, app="limits") cls.localconfig.save() factories.populate_database() diff --git a/modoboa/admin/api/v1/viewsets.py b/modoboa/admin/api/v1/viewsets.py index f6ed5804f..c97a2512c 100644 --- a/modoboa/admin/api/v1/viewsets.py +++ b/modoboa/admin/api/v1/viewsets.py @@ -135,7 +135,7 @@ class AccountViewSet( try: user = core_models.User.objects.get(pk=pk) except core_models.User.DoesNotExist: - raise http.Http404 + raise http.Http404 from None serializer = self.get_serializer(user, data=request.data) if serializer.is_valid(): serializer.save() diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py index b64860118..c38f4ab77 100644 --- a/modoboa/admin/api/v2/serializers.py +++ b/modoboa/admin/api/v2/serializers.py @@ -94,7 +94,7 @@ class DomainSerializer(v1_serializers.DomainSerializer): return domain # 1. Create a domain administrator - username = "%s@%s" % (domain_admin["username"], domain.name) + username = "{}@{}".format(domain_admin["username"], domain.name) try: da = core_models.User.objects.get(username=username) except core_models.User.DoesNotExist: @@ -136,7 +136,7 @@ class DomainSerializer(v1_serializers.DomainSerializer): core_signals.can_create_object.send( self.__class__, context=user, klass=models.Alias ) - address = "postmaster@{}".format(domain.name) + address = f"postmaster@{domain.name}" alias = models.Alias.objects.create( address=address, domain=domain, enabled=True ) @@ -240,13 +240,11 @@ class AdminGlobalParametersSerializer(serializers.Serializer): if not value: return value try: - ip_addresses = [ - ipaddress.ip_network(v.strip()) for v in value.split() if v.strip() - ] + [ipaddress.ip_network(v.strip()) for v in value.split() if v.strip()] except ValueError: raise serializers.ValidationError( _("This field only allows valid IP addresses (or networks)") - ) + ) from None return value def validate_auto_create_domain_and_mailbox(self, value): @@ -366,9 +364,7 @@ class AccountMeSerializer(v1_serializers.AccountSerializer): permissions = ( user.user_permissions.all() | Permission.objects.filter(group__user=user) ).select_related("content_type") - return [ - "{}.{}".format(p.content_type.app_label, p.codename) for p in permissions - ] + return [f"{p.content_type.app_label}.{p.codename}" for p in permissions] class AccountSerializer(v1_serializers.AccountSerializer): @@ -483,7 +479,7 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): try: validators.UTF8EmailValidator()(username) except ValidationError as err: - raise ValidationError({"username": err.message}) + raise ValidationError({"username": err.message}) from None mailbox = data.get("mailbox") if mailbox is None: if not self.instance: @@ -505,7 +501,7 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): object_type="mailboxes", ) except lib_exceptions.ModoboaException as inst: - raise serializers.ValidationError({"mailbox": str(inst)}) + raise serializers.ValidationError({"mailbox": str(inst)}) from None if len(self.address) > 64: raise serializers.ValidationError( { @@ -522,7 +518,9 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): data["password"], self.instance ) except ValidationError as exc: - raise serializers.ValidationError({"password": exc.messages[0]}) + raise serializers.ValidationError( + {"password": exc.messages[0]} + ) from None elif not self.instance: raise serializers.ValidationError( {"password": _("This field is required.")} diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index c75292c96..8f27d0269 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -212,7 +212,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): dpath = fpath if dpath: try: - code, version = exec_cmd("%s --version" % dpath) + code, version = exec_cmd(f"{dpath} --version") except OSError: hide_fields = True else: @@ -255,18 +255,16 @@ class AdminParametersForm(param_forms.AdminParametersForm): if not value: return value try: - ip_addresses = [ - ipaddress.ip_network(v.strip()) for v in value.split() if v.strip() - ] + [ipaddress.ip_network(v.strip()) for v in value.split() if v.strip()] except ValueError: raise forms.ValidationError( gettext_lazy("This field only allows valid IP addresses (or networks)") - ) + ) from None return value def clean(self): """Check MX options.""" - cleaned_data = super(AdminParametersForm, self).clean() + cleaned_data = super().clean() condition = ( cleaned_data.get("enable_mx_checks") and cleaned_data.get("domains_must_have_authorized_mx") diff --git a/modoboa/admin/factories.py b/modoboa/admin/factories.py index 2d59ff863..359c82f9a 100644 --- a/modoboa/admin/factories.py +++ b/modoboa/admin/factories.py @@ -15,7 +15,7 @@ class DomainFactory(PermissionFactory): model = models.Domain django_get_or_create = ("name",) - name = factory.Sequence(lambda n: "domain{}.test".format(n)) + name = factory.Sequence(lambda n: f"domain{n}.test") type = "domain" # NOQA:A003 quota = 0 default_mailbox_quota = 10 diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index cbfcffc52..f43c1979a 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -71,7 +71,7 @@ class AccountFormGeneral(forms.ModelForm): labels = {"is_active": gettext_lazy("Enabled")} def __init__(self, user, *args, **kwargs): - super(AccountFormGeneral, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields = OrderedDict( (key, self.fields[key]) for key in [ @@ -163,7 +163,7 @@ class AccountFormGeneral(forms.ModelForm): def clean(self): """Check master user mode.""" - super(AccountFormGeneral, self).clean() + super().clean() if self.errors: return self.cleaned_data condition = ( @@ -200,7 +200,7 @@ class AccountFormGeneral(forms.ModelForm): return self.cleaned_data def save(self, commit=True): - account = super(AccountFormGeneral, self).save(commit=False) + account = super().save(commit=False) if self.user == account and not self.cleaned_data["is_active"]: raise lib_exceptions.PermDeniedException( _("You can't disable your own account") @@ -292,12 +292,12 @@ class AccountFormMail(forms.Form, DynamicForm): self.fields["email"].required = True qset = self.mb.aliasrecipient_set.filter(alias__internal=False) for cpt, ralias in enumerate(qset): - name = "aliases_{}".format(cpt + 1) + name = f"aliases_{cpt + 1}" self._create_field( lib_fields.UTF8AndEmptyUserEmailField, name, ralias.alias.address ) for cpt, saddress in enumerate(self.mb.senderaddress_set.all()): - name = "senderaddress_{}".format(cpt + 1) + name = f"senderaddress_{cpt + 1}" self._create_field( lib_fields.UTF8AndEmptyUserEmailField, name, saddress.address ) @@ -336,14 +336,14 @@ class AccountFormMail(forms.Form, DynamicForm): try: self.domain = models.Domain.objects.get(name=domname) except models.Domain.DoesNotExist: - raise forms.ValidationError(_("Domain does not exist")) + raise forms.ValidationError(_("Domain does not exist")) from None if not self.mb: try: core_signals.can_create_object.send( sender=self.__class__, context=self.domain, object_type="mailboxes" ) except lib_exceptions.ModoboaException as inst: - raise forms.ValidationError(inst) + raise forms.ValidationError(inst) from None return email def clean_quota(self): @@ -355,7 +355,7 @@ class AccountFormMail(forms.Form, DynamicForm): Check if quota is >= 0 only when the domain value is not used. """ - cleaned_data = super(AccountFormMail, self).clean() + cleaned_data = super().clean() use_default_domain_quota = cleaned_data["quota_act"] condition = ( not use_default_domain_quota @@ -510,13 +510,13 @@ class AccountPermissionsForm(forms.Form, DynamicForm): self.account = kwargs["instance"] del kwargs["instance"] - super(AccountPermissionsForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if not hasattr(self, "account") or self.account is None: return qset = models.Domain.objects.get_for_admin(self.account) for pos, dom in enumerate(qset): - name = "domains_%d" % (pos + 1) + name = f"domains_{pos + 1}" self._create_field(lib_fields.DomainNameField, name, dom.name) if len(args) and isinstance(args[0], QueryDict): self._load_from_qdict(args[0], "domains", lib_fields.DomainNameField) @@ -580,7 +580,7 @@ class AccountForm(TabForms): cbargs["account"] = kwargs["instances"]["general"] results = signals.extra_account_forms.send(sender=self.__class__, **cbargs) self.forms += reduce(lambda a, b: a + b, [result[1] for result in results]) - super(AccountForm, self).__init__(request, *args, **kwargs) + super().__init__(request, *args, **kwargs) def extra_context(self, context): account = self.instances["general"] @@ -604,8 +604,8 @@ class AccountForm(TabForms): if form["id"] == "general": return True - if hasattr(self, "check_%s" % form["id"]): - if not getattr(self, "check_%s" % form["id"])(self.account): + if hasattr(self, "check_{}".format(form["id"])): + if not getattr(self, "check_{}".format(form["id"]))(self.account): return False return True @@ -620,9 +620,9 @@ class AccountForm(TabForms): def is_valid(self): """Two steps validation.""" self.instances["general"].oldgroup = self.instances["general"].role - if super(AccountForm, self).is_valid(mandatory_only=True): + if super().is_valid(mandatory_only=True): self.account = self.forms[0]["instance"].save() - return super(AccountForm, self).is_valid(optional_only=True) + return super().is_valid(optional_only=True) return False def save(self): @@ -646,7 +646,7 @@ class AccountWizard(WizardForm): """Account creation wizard.""" def __init__(self, request): - super(AccountWizard, self).__init__(request) + super().__init__(request) self.add_step( WizardStep( "general", AccountFormGeneral, _("General"), new_args=[request.user] diff --git a/modoboa/admin/forms/alias.py b/modoboa/admin/forms/alias.py index 33794bfdb..ab95bf1a9 100644 --- a/modoboa/admin/forms/alias.py +++ b/modoboa/admin/forms/alias.py @@ -49,7 +49,7 @@ class AliasForm(forms.ModelForm, DynamicForm): def __init__(self, user, *args, **kwargs): self.user = user - super(AliasForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields = OrderedDict( (key, self.fields[key]) for key in [ @@ -80,7 +80,7 @@ class AliasForm(forms.ModelForm, DynamicForm): self.fields["enabled"].widget.attrs["disabled"] = "disabled" cpt = 1 for rcpt in alias.aliasrecipient_set.filter(alias__internal=False): - name = "recipients_%d" % cpt + name = f"recipients_{cpt}" self._create_field(forms.EmailField, name, rcpt.address, 2) cpt += 1 @@ -92,7 +92,7 @@ class AliasForm(forms.ModelForm, DynamicForm): try: domain = Domain.objects.get(name=domname) except Domain.DoesNotExist: - raise forms.ValidationError(_("Domain does not exist")) + raise forms.ValidationError(_("Domain does not exist")) from None if not self.user.can_access(domain): raise forms.ValidationError(_("You don't have access to this domain")) if not self.instance.pk: @@ -101,12 +101,12 @@ class AliasForm(forms.ModelForm, DynamicForm): sender=self.__class__, context=domain, object_type="mailbox_aliases" ) except lib_exceptions.ModoboaException as inst: - raise forms.ValidationError(inst) + raise forms.ValidationError(inst) from None return self.cleaned_data["address"].lower() def clean(self): """Check it there is at least one recipient.""" - super(AliasForm, self).clean() + super().clean() random_address = self.cleaned_data.get("random_address") if not random_address: if not self.cleaned_data.get("address"): @@ -121,11 +121,9 @@ class AliasForm(forms.ModelForm, DynamicForm): def save(self, commit=True): """Custom save method.""" - alias = super(AliasForm, self).save(commit=False) + alias = super().save(commit=False) if self.cleaned_data.get("random_address"): - alias.address = "{}@{}".format( - Alias.generate_random_address(), alias.domain - ) + alias.address = f"{Alias.generate_random_address()}@{alias.domain}" else: local_part, domname = split_mailbox(self.cleaned_data["address"]) alias.domain = Domain.objects.get(name=domname) diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index e5e028527..85eb1ce2b 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -80,7 +80,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if "instance" in kwargs: self.old_dkim_key_length = kwargs["instance"].dkim_key_length self.oldname = kwargs["instance"].name - super(DomainFormGeneral, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) params = dict(param_tools.get_global_parameters("admin")) self.fields["quota"].initial = params["default_domain_quota"] if params["default_domain_message_limit"] is not None: @@ -97,7 +97,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): elif "instance" in kwargs: d = kwargs["instance"] for pos, dalias in enumerate(d.domainalias_set.all()): - name = "aliases_%d" % (pos + 1) + name = f"aliases_{pos + 1}" self._create_field(forms.CharField, name, dalias.name, 3) def clean_name(self): @@ -148,7 +148,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): The validation way is not very smart... """ - cleaned_data = super(DomainFormGeneral, self).clean() + cleaned_data = super().clean() if self._errors: return cleaned_data condition = ( @@ -203,7 +203,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): quotas update. """ - d = super(DomainFormGeneral, self).save(commit=False) + d = super().save(commit=False) core_signals.can_create_object.send( sender=self.__class__, context=user, klass=Domain, instance=d ) @@ -280,7 +280,7 @@ class DomainFormOptions(forms.Form): ) def __init__(self, user, *args, **kwargs): - super(DomainFormOptions, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) results = core_signals.user_can_set_role.send( sender=self.__class__, user=user, role="DomainAdmins" ) @@ -296,7 +296,7 @@ class DomainFormOptions(forms.Form): def clean(self): """Check required values.""" - cleaned_data = super(DomainFormOptions, self).clean() + cleaned_data = super().clean() if cleaned_data.get("create_dom_admin"): if not cleaned_data.get("dom_admin_username"): self.add_error("dom_admin_username", _("This field is required.")) @@ -311,7 +311,7 @@ class DomainFormOptions(forms.Form): return user = kwargs.pop("user") domain = kwargs.pop("domain") - username = "%s@%s" % (self.cleaned_data["dom_admin_username"], domain.name) + username = f"{self.cleaned_data['dom_admin_username']}@{domain.name}" try: da = User.objects.get(username=username) except User.DoesNotExist: @@ -349,7 +349,7 @@ class DomainFormOptions(forms.Form): core_signals.can_create_object.send( self.__class__, context=user, klass=Alias ) - address = "postmaster@{}".format(domain.name) + address = f"postmaster@{domain.name}" alias = Alias.objects.create( address=address, domain=domain, enabled=True ) @@ -386,7 +386,7 @@ class DomainForm(TabForms): self.forms += reduce(lambda a, b: a + b, [result[1] for result in results]) if not self.forms: self.active_id = "admins" - super(DomainForm, self).__init__(request, *args, **kwargs) + super().__init__(request, *args, **kwargs) def extra_context(self, context): """Add information to template context.""" @@ -409,7 +409,7 @@ class DomainForm(TabForms): if "general" in self.instances: instance = self.instances["general"] instance.oldname = instance.name - return super(DomainForm, self).is_valid() + return super().is_valid() def save(self): """Custom save method. @@ -437,7 +437,7 @@ class DomainWizard(WizardForm): """Domain creation wizard.""" def __init__(self, request): - super(DomainWizard, self).__init__(request) + super().__init__(request) self.add_step( WizardStep( "general", diff --git a/modoboa/admin/forms/export.py b/modoboa/admin/forms/export.py index ba0863664..6d9c9d925 100644 --- a/modoboa/admin/forms/export.py +++ b/modoboa/admin/forms/export.py @@ -13,7 +13,7 @@ class ExportDataForm(forms.Form): ) def __init__(self, *args, **kwargs): - super(ExportDataForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields["sepchar"].widget.attrs = {"class": "col-md-1 form-control"} def clean_sepchar(self): diff --git a/modoboa/admin/forms/import_.py b/modoboa/admin/forms/import_.py index a038d1e35..86f53dbaf 100644 --- a/modoboa/admin/forms/import_.py +++ b/modoboa/admin/forms/import_.py @@ -21,7 +21,7 @@ class ImportDataForm(forms.Form): ) def __init__(self, *args, **kwargs): - super(ImportDataForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields["sepchar"].widget.attrs = {"class": "col-md-1 form-control"} def clean_sepchar(self): diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py index f2c7a686e..ba87d4507 100644 --- a/modoboa/admin/handlers.py +++ b/modoboa/admin/handlers.py @@ -27,13 +27,9 @@ def update_domain_mxs_and_mailboxes(sender, instance, **kwargs): ) if instance.old_mail_homes is None: return - qset = models.Quota.objects.filter( - username__contains="@{}".format(instance.oldname) - ) + qset = models.Quota.objects.filter(username__contains=f"@{instance.oldname}") for q in qset: - username = q.username.replace( - "@{}".format(instance.oldname), "@{}".format(instance.name) - ) + username = q.username.replace(f"@{instance.oldname}", f"@{instance.name}") models.Quota.objects.create( username=username, bytes=q.bytes, messages=q.messages ) @@ -62,17 +58,17 @@ def create_alias_for_domainalias(sender, instance, **kwargs): if not kwargs.get("created"): return alias = models.Alias.objects.create( - address="@{}".format(instance.name), enabled=True, internal=True + address=f"@{instance.name}", enabled=True, internal=True ) models.AliasRecipient.objects.create( - address="@{}".format(instance.target.name), alias=alias + address=f"@{instance.target.name}", alias=alias ) @receiver(signals.post_delete, sender=models.DomainAlias) def remove_alias_for_domainalias(sender, instance, **kwargs): """Remove the alias associated to domain alias.""" - models.Alias.objects.filter(address="@{}".format(instance.name)).delete() + models.Alias.objects.filter(address=f"@{instance.name}").delete() @receiver(signals.post_save, sender=models.Mailbox) @@ -256,7 +252,7 @@ def import_account_mailbox(sender, user, account, row, **kwargs): raise exceptions.BadRequest( _("Account import failed (%s): wrong quota value") % account.username - ) + ) from None use_domain_quota = True if not quota else False mb = models.Mailbox( address=mailbox, diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index bfbb07a8e..9715c06c7 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -205,7 +205,7 @@ def get_authoritative_server(domain, resolver=None): return str(first_answer.target) if hasattr(first_answer, "address"): return str(first_answer.address) - except dns.resolver.NoAnswer as e: + except dns.resolver.NoAnswer: dnsname = dnsname.parent() else: dnsname = dnsname.parent() @@ -253,7 +253,7 @@ def get_dns_records(name, typ, resolver=None): exc_info=e, ) except dns.name.NameTooLong as e: - logger.error(_("DNS name is too long: %s" % name), exc_info=e) + logger.error(_("DNS name is too long: {}".format(name)), exc_info=e) else: return dns_answers return None @@ -359,7 +359,7 @@ def import_data(user, file_object, options: dict): raise Conflict( _("Object already exists: %s") % options["sepchar"].join(row[:2]) - ) + ) from None cpt += 1 msg = _("%d objects imported successfully") % cpt return True, msg diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index bfa527c76..f6274f484 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -25,7 +25,7 @@ class Command(BaseCommand): help = "Handles rename and delete operations on mailboxes" # NOQA:A003 def __init__(self, *args, **kwargs): - super(Command, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.logger = logging.getLogger("modoboa.admin") def add_arguments(self, parser): @@ -44,9 +44,9 @@ class Command(BaseCommand): if not os.path.exists(dirname): try: os.makedirs(dirname) - except os.error as e: - raise OperationError(str(e)) - code, output = exec_cmd("mv %s %s" % (operation.argument, new_mail_home)) + except OSError as e: + raise OperationError(str(e)) from None + code, output = exec_cmd(f"mv {operation.argument} {new_mail_home}") if code: raise OperationError(output) @@ -57,7 +57,7 @@ class Command(BaseCommand): def onerror(function, path, excinfo): """Handle errors.""" - self.logger.critical("delete failed (reason: {})".format(excinfo)) + self.logger.critical(f"delete failed (reason: {excinfo})") shutil.rmtree(operation.argument, False, onerror) @@ -71,7 +71,7 @@ class Command(BaseCommand): with open(path) as fp: pid = fp.read().strip() code, output = exec_cmd( - "grep handle_mailbox_operations /proc/%s/cmdline" % pid + f"grep handle_mailbox_operations /proc/{pid}/cmdline" ) if not code: return False @@ -88,7 +88,7 @@ class Command(BaseCommand): return for ope in MailboxOperation.objects.all(): try: - f = getattr(self, "%s_mailbox" % ope.type) + f = getattr(self, f"{ope.type}_mailbox") except AttributeError: continue try: diff --git a/modoboa/admin/management/commands/subcommands/_import.py b/modoboa/admin/management/commands/subcommands/_import.py index 0be4e2575..0f2c39b62 100644 --- a/modoboa/admin/management/commands/subcommands/_import.py +++ b/modoboa/admin/management/commands/subcommands/_import.py @@ -1,13 +1,11 @@ """Django management command to import admin objects.""" import csv -import io import os import progressbar from chardet.universaldetector import UniversalDetector -from django.core.exceptions import ValidationError from django.core.management.base import BaseCommand, CommandError from django.db import transaction from django.utils.translation import gettext as _ @@ -50,12 +48,12 @@ class ImportCommand(BaseCommand): if not os.path.isfile(filename): raise CommandError("File not found") - num_lines = sum(1 for line in io.open(filename, encoding=encoding) if line) + num_lines = sum(1 for line in open(filename, encoding=encoding) if line) pbar = progressbar.ProgressBar( widgets=[progressbar.Percentage(), progressbar.Bar(), progressbar.ETA()], maxval=num_lines, ).start() - with io.open(filename, encoding=encoding, newline="") as f: + with open(filename, encoding=encoding, newline="") as f: reader = csv.reader(f, delimiter=options["sepchar"]) i = 0 for row in reader: @@ -77,7 +75,7 @@ class ImportCommand(BaseCommand): "Object already exists at line {}: {}".format( i + 1, options["sepchar"].join(row[:2]) ) - ) + ) from None i += 1 pbar.update(i) @@ -102,7 +100,7 @@ class ImportCommand(BaseCommand): ) ) detector = UniversalDetector() - with io.open(filename, "rb") as fp: + with open(filename, "rb") as fp: for line in fp: detector.feed(line) if detector.done: diff --git a/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py b/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py index 9cac1a2b8..e8790a120 100644 --- a/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py +++ b/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py @@ -19,7 +19,7 @@ class ManageDKIMKeys(BaseCommand): def create_new_dkim_key(self, domain): """Create a new DKIM key.""" storage_dir = param_tools.get_global_parameter("dkim_keys_storage_dir") - pkey_path = os.path.join(storage_dir, "{}.pem".format(domain.name)) + pkey_path = os.path.join(storage_dir, f"{domain.name}.pem") alarm_qset = domain.alarms.filter(internal_name=DKIM_WRITE_ERROR) if not os.access(storage_dir, os.W_OK): @@ -43,26 +43,20 @@ class ManageDKIMKeys(BaseCommand): if domain.dkim_key_length else self.default_key_length ) - code, output = sysutils.exec_cmd( - "openssl genrsa -out {} {}".format(pkey_path, key_size) - ) + code, output = sysutils.exec_cmd(f"openssl genrsa -out {pkey_path} {key_size}") if code: print( - "Failed to generate DKIM private key for domain {}: {}".format( - domain.name, smart_str(output) - ) + f"Failed to generate DKIM private key for domain {domain.name}: {smart_str(output)}" ) domain.alarms.create( title=_("Failed to generate DKIM private key"), internal_name=DKIM_ERROR ) return domain.dkim_private_key_path = pkey_path - code, output = sysutils.exec_cmd("openssl rsa -in {} -pubout".format(pkey_path)) + code, output = sysutils.exec_cmd(f"openssl rsa -in {pkey_path} -pubout") if code: print( - "Failed to generate DKIM public key for domain {}: {}".format( - domain.name, smart_str(output) - ) + f"Failed to generate DKIM public key for domain {domain.name}: {smart_str(output)}" ) domain.alarms.create( title=_("Failed to generate DKIM public key"), internal_name=DKIM_ERROR diff --git a/modoboa/admin/management/commands/subcommands/_mx.py b/modoboa/admin/management/commands/subcommands/_mx.py index a2d365da1..ff1d9e890 100644 --- a/modoboa/admin/management/commands/subcommands/_mx.py +++ b/modoboa/admin/management/commands/subcommands/_mx.py @@ -87,7 +87,7 @@ class CheckMXRecords(BaseCommand): else: delim = "." if ip.version == 4 else ":" reverse = delim.join(ip.exploded.split(delim)[::-1]) - pattern = "{}.{}.".format(reverse, provider) + pattern = f"{reverse}.{provider}." try: result = socket.gethostbyname(pattern) # result from dnsbl is in ipv4 format @@ -128,7 +128,7 @@ class CheckMXRecords(BaseCommand): dnsbl_result.save() if not dnsbl_result.status and result: trigger = True - alarm_name = "domain_mx_in_dnsbl_{}".format(provider) + alarm_name = f"domain_mx_in_dnsbl_{provider}" if trigger: title = _("MX {} listed by DNSBL provider {}").format(mx.name, provider) domain.alarms.create( @@ -165,7 +165,7 @@ class CheckMXRecords(BaseCommand): """ alerts = [] check = False - mxs = [(mx, ipaddress.ip_address("%s" % mx.address)) for mx in mx_list] + mxs = [(mx, ipaddress.ip_address(str(mx.address))) for mx in mx_list] valid_mxs = self.valid_mxs if not mxs: alarm, created = domain.alarms.get_or_create( @@ -187,7 +187,7 @@ class CheckMXRecords(BaseCommand): mx.managed = check = True mx.save() if check is False: - mx_names = ["{0.name} ({0.address})".format(mx) for mx in mx_list] + mx_names = [f"{mx.name} ({mx.address})" for mx in mx_list] alarm, created = domain.alarms.get_or_create( internal_name="domain_invalid_mx", status=constants.ALARM_OPENED, diff --git a/modoboa/admin/management/commands/subcommands/_repair.py b/modoboa/admin/management/commands/subcommands/_repair.py index 27a5f2cf6..90c42b91d 100644 --- a/modoboa/admin/management/commands/subcommands/_repair.py +++ b/modoboa/admin/management/commands/subcommands/_repair.py @@ -76,18 +76,16 @@ def sometimes_mailbox_have_no_alias(**options): ) if created: alias_created += 1 - log("Alias {0} created".format(alias), **options) + log(f"Alias {alias} created", **options) recipient, created = models.AliasRecipient.objects.get_or_create( alias=alias, address=instance.full_address, r_mailbox=instance ) if created: recipient_created += 1 - log("AliasRecipient {0} created".format(recipient), **options) + log(f"AliasRecipient {recipient} created", **options) if alias_created or recipient_created: log( - "{0} alias created. {1} alias recipient created".format( - alias_created, recipient_created - ), + f"{alias_created} alias created. {recipient_created} alias recipient created", **options, ) @@ -129,5 +127,5 @@ class Repair(BaseCommand): for func in known_problems: title = func.__doc__.strip() log("", **options) - log("Checking for... {}...".format(title), **options) + log(f"Checking for... {title}...", **options) func(**options) diff --git a/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py b/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py index 13d831adb..900f543ea 100644 --- a/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py +++ b/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations def rename_and_clean(apps, schema_editor): diff --git a/modoboa/admin/models/alarm.py b/modoboa/admin/models/alarm.py index 15267a609..e7580161c 100644 --- a/modoboa/admin/models/alarm.py +++ b/modoboa/admin/models/alarm.py @@ -48,9 +48,7 @@ class Alarm(models.Model): ordering = ["created"] def __str__(self): - return "[{}] {} - {}".format( - self.created, self.domain, self.get_status_display() - ) + return f"[{self.created}] {self.domain} - {self.get_status_display()}" def close(self): """Close this alarm.""" diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py index 9d870a9fb..1fb857ee8 100644 --- a/modoboa/admin/models/alias.py +++ b/modoboa/admin/models/alias.py @@ -54,7 +54,7 @@ def validate_alias_address( object_type="mailbox_aliases", ) except ModoboaException as inst: - raise ValidationError(str(inst)) + raise ValidationError(str(inst)) from None return local_part, domain @@ -132,7 +132,7 @@ class Alias(AdminObject): return "---" rcpts = self.recipients if rcpts_count > 1: - return "%s, ..." % rcpts[0] + return f"{rcpts[0]}, ..." return rcpts[0] @property @@ -150,7 +150,7 @@ class Alias(AdminObject): def post_create(self, creator): from modoboa.lib.permissions import grant_access_to_object - super(Alias, self).post_create(creator) + super().post_create(creator) if creator.is_superuser: for admin in self.domain.admins: grant_access_to_object(admin, self) @@ -174,7 +174,7 @@ class Alias(AdminObject): address, return_extension=True ) if domname is None: - raise BadRequest("%s %s" % (_("Invalid address"), address)) + raise BadRequest("{} {}".format(_("Invalid address"), address)) domain = Domain.objects.filter(name=domname).first() kwargs = {"address": address, "alias": self} if (domain is not None) and ( @@ -189,7 +189,7 @@ class Alias(AdminObject): rcpt = Mailbox.objects.filter(domain=domain, address=local_part).first() if rcpt is None: rcpt = Alias.objects.filter( - address="%s@%s" % (local_part, domname) + address=f"{local_part}@{domname}" ).first() if rcpt is None: raise NotFound( @@ -306,4 +306,4 @@ class AliasRecipient(models.Model): def __str__(self): """Return alias and recipient.""" - return smart_str("{} -> {}".format(self.alias.address, self.address)) + return smart_str(f"{self.alias.address} -> {self.address}") diff --git a/modoboa/admin/models/base.py b/modoboa/admin/models/base.py index 0fc6ef7d3..06b71029b 100644 --- a/modoboa/admin/models/base.py +++ b/modoboa/admin/models/base.py @@ -62,10 +62,10 @@ class AdminObject(models.Model): def save(self, *args, **kwargs): creator = kwargs.pop("creator", None) - super(AdminObject, self).save(*args, **kwargs) + super().save(*args, **kwargs) if creator is not None: self.post_create(creator) def delete(self): ungrant_access_to_object(self) - super(AdminObject, self).delete() + super().delete() diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 6a84c2f1a..6680c97f9 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -271,22 +271,20 @@ class Domain(mixins.MessageLimitMixin, AdminObject): @property def bind_format_dkim_public_key(self): """Return DKIM public key using Bind format.""" - record = "v=DKIM1;k=rsa;p=%s" % self.dkim_public_key + record = f"v=DKIM1;k=rsa;p={self.dkim_public_key}" # TXT records longer than 255 characters need split into chunks # split record into 74 character chunks (+2 for indent, +2 for quotes(") # == 78 characters) to make more readable in text editors. split_record = [] while record: if len(record) > 74: - split_record.append(' "%s"' % record[:74]) + split_record.append(f' "{record[:74]}"') record = record[74:] else: - split_record.append(' "%s"' % record) + split_record.append(f' "{record}"') break record = "\n".join(split_record) - return "{}._domainkey.{}. IN TXT (\n{})".format( - self.dkim_key_selector, self.name, record - ) + return f"{self.dkim_key_selector}._domainkey.{self.name}. IN TXT (\n{record})" def add_admin(self, account): """Add a new administrator to this domain. @@ -345,7 +343,7 @@ class Domain(mixins.MessageLimitMixin, AdminObject): if param_tools.get_global_parameter("auto_account_removal"): User.objects.filter(mailbox__domain=self).delete() elif self.mailbox_set.count(): - Quota.objects.filter(username__contains="@%s" % self.name).delete() + Quota.objects.filter(username__contains=f"@{self.name}").delete() ungrant_access_to_objects(self.mailbox_set.all()) super().delete() @@ -380,7 +378,9 @@ class Domain(mixins.MessageLimitMixin, AdminObject): try: self.quota = int(row[2].strip()) except ValueError: - raise BadRequest(_("{}: invalid quota value for domain").format(self.name)) + raise BadRequest( + _("{}: invalid quota value for domain").format(self.name) + ) from None try: self.default_mailbox_quota = int(row[3].strip()) except ValueError: @@ -388,7 +388,7 @@ class Domain(mixins.MessageLimitMixin, AdminObject): _("{}: invalid default mailbox quota value for domain").format( self.name ) - ) + ) from None if self.quota != 0 and self.default_mailbox_quota > self.quota: raise BadRequest( _( diff --git a/modoboa/admin/models/domain_alias.py b/modoboa/admin/models/domain_alias.py index 32a00b7f9..218795fbc 100644 --- a/modoboa/admin/models/domain_alias.py +++ b/modoboa/admin/models/domain_alias.py @@ -76,7 +76,7 @@ class DomainAlias(AdminObject): try: self.target = Domain.objects.get(name=domname) except Domain.DoesNotExist: - raise BadRequest(_("Unknown domain %s") % domname) + raise BadRequest(_("Unknown domain %s") % domname) from None core_signals.can_create_object.send( sender="import", context=self.target, object_type="domain_aliases" ) diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index e87d76502..f1acaeb3c 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -25,7 +25,7 @@ class QuotaManager(models.Manager): def get_domain_usage(self, domain): """Return current usage for domain.""" - qset = self.get_queryset().filter(username__endswith="@{}".format(domain.name)) + qset = self.get_queryset().filter(username__endswith=f"@{domain.name}") result = qset.aggregate(usage=models.Sum("bytes")).get("usage", 0) if result is None: result = 0 @@ -114,7 +114,7 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): return smart_str(self.full_address) def __full_address(self, localpart): - return "%s@%s" % (localpart, self.domain.name) + return f"{localpart}@{self.domain.name}" @property def full_address(self): @@ -147,7 +147,7 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): if not admin_params.get("handle_mailboxes"): return None if self.__mail_home is None: - code, output = doveadm_cmd("user -f home %s" % self.full_address) + code, output = doveadm_cmd(f"user -f home {self.full_address}") if code: raise lib_exceptions.InternalError( _("Failed to retrieve mailbox location (%s)") % output @@ -384,5 +384,5 @@ class MailboxOperation(models.Model): def __str__(self): if self.type == "rename": - return "Rename %s -> %s" % (self.argument, self.mailbox.mail_home) - return "Delete %s" % self.argument + return f"Rename {self.argument} -> {self.mailbox.mail_home}" + return f"Delete {self.argument}" diff --git a/modoboa/admin/models/mxrecord.py b/modoboa/admin/models/mxrecord.py index e0d630c5c..1eae36025 100644 --- a/modoboa/admin/models/mxrecord.py +++ b/modoboa/admin/models/mxrecord.py @@ -45,7 +45,7 @@ class MXRecordManager(models.Manager): record = self.get_queryset().create( domain=domain, name="{}".format(mx_addr.strip(".")), - address="{}".format(mx_ip_addr), + address=f"{mx_ip_addr}", updated=now + delta, ) yield record @@ -68,7 +68,7 @@ class MXRecord(models.Model): return bool(param_tools.get_global_parameter("valid_mxs").strip()) def __str__(self): - return "{0.name} ({0.address}) for {0.domain} ".format(self) + return f"{self.name} ({self.address}) for {self.domain} " class DNSBLQuerySet(models.QuerySet): diff --git a/modoboa/admin/postfix_maps.py b/modoboa/admin/postfix_maps.py index 1378997f9..1e865d683 100644 --- a/modoboa/admin/postfix_maps.py +++ b/modoboa/admin/postfix_maps.py @@ -1,7 +1,7 @@ """Map file definitions for postfix.""" -class DomainsMap(object): +class DomainsMap: """Map to list all domains.""" filename = "sql-domains.cf" @@ -18,7 +18,7 @@ class DomainsMap(object): ) -class DomainsAliasesMap(object): +class DomainsAliasesMap: """Map to list all domain aliases.""" filename = "sql-domain-aliases.cf" @@ -39,7 +39,7 @@ class DomainsAliasesMap(object): ) -class AliasesMap(object): +class AliasesMap: """A map to list all mailbox aliases.""" filename = "sql-aliases.cf" @@ -63,7 +63,7 @@ class AliasesMap(object): ) -class MaintainMap(object): +class MaintainMap: """Map files to list non available mailboxes.""" filename = "sql-maintain.cf" @@ -90,7 +90,7 @@ class MaintainMap(object): ) -class SenderLoginMap(object): +class SenderLoginMap: """Map file to list authorized sender addresses for a given account: * Its mailbox * Its aliases diff --git a/modoboa/admin/templatetags/admin_tags.py b/modoboa/admin/templatetags/admin_tags.py index 0da358765..43c5c9757 100644 --- a/modoboa/admin/templatetags/admin_tags.py +++ b/modoboa/admin/templatetags/admin_tags.py @@ -151,7 +151,7 @@ def domain_actions(user, domain): actions = [ { "name": "listidentities", - "url": "{0}#list/?searchquery=@{1}".format( + "url": "{}#list/?searchquery=@{}".format( reverse("admin:identity_list"), domain.name ), "title": _("View the domain's identities"), @@ -268,7 +268,7 @@ def domain_aliases(domain): return "---" res = "" for alias in domain.aliases.all(): - res += "%s
" % alias.name + res += f"{alias.name}
" return mark_safe(res) @@ -286,7 +286,7 @@ def identity_modify_link(identity, active_tab="default"): linkdef = {"label": identity.identity, "modal": True} if identity.__class__.__name__ == "User": linkdef["url"] = reverse("admin:account_change", args=[identity.id]) - linkdef["url"] += "?active_tab=%s" % active_tab + linkdef["url"] += f"?active_tab={active_tab}" linkdef["modalcb"] = "admin.editaccount_cb" else: linkdef["url"] = reverse("admin:alias_change", args=[identity.id]) @@ -299,7 +299,7 @@ def domadmin_actions(daid, domid): actions = [ { "name": "removeperm", - "url": "{0}?domid={1}&daid={2}".format( + "url": "{}?domid={}&daid={}".format( reverse("admin:permission_remove"), domid, daid ), "img": "fa fa-trash", diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index 561079b02..79d44a694 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -17,7 +17,7 @@ class AuthenticationTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(AuthenticationTestCase, cls).setUpTestData() + super().setUpTestData() cls.mb = factories.MailboxFactory( domain__name="test.com", address="user", @@ -42,7 +42,7 @@ class AccountTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(AccountTestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() def test_crud(self): @@ -576,14 +576,14 @@ class AccountTestCase(ModoTestCase): self.assertIn("user@test.com", response["rows"]) old_rows = response["rows"] - response = self.ajax_get("{}?sort_order=-quota_value__bytes".format(url)) + response = self.ajax_get(f"{url}?sort_order=-quota_value__bytes") self.assertNotEqual(old_rows, response["rows"]) old_rows = response["rows"] - response = self.ajax_get("{}?sort_order=-quota_usage".format(url)) + response = self.ajax_get(f"{url}?sort_order=-quota_usage") self.assertEqual(old_rows, response["rows"]) - response = self.ajax_get("{}?sort_order=-unknown".format(url), status=400) + response = self.ajax_get(f"{url}?sort_order=-unknown", status=400) @skipIf(NO_LDAP, "No ldap module installed") @@ -612,10 +612,10 @@ class PermissionsTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(PermissionsTestCase, cls).setUpTestData() + super().setUpTestData() parameters = {} for name, _definition in limits_utils.get_user_limit_templates(): - parameters["deflt_user_{0}_limit".format(name)] = 2 + parameters[f"deflt_user_{name}_limit"] = 2 cls.localconfig.parameters.set_values(parameters, app="limits") cls.localconfig.save() factories.populate_database() @@ -625,7 +625,7 @@ class PermissionsTestCase(ModoTestCase): def setUp(self): """Initiate test context.""" - super(PermissionsTestCase, self).setUp() + super().setUp() self.values = { "username": self.user.username, "role": "DomainAdmins", diff --git a/modoboa/admin/tests/test_alarms.py b/modoboa/admin/tests/test_alarms.py index 07691f3f0..e81e6bdc6 100644 --- a/modoboa/admin/tests/test_alarms.py +++ b/modoboa/admin/tests/test_alarms.py @@ -1,7 +1,5 @@ """Alarm related tests.""" -from django.urls import reverse - from modoboa.lib.tests import ModoTestCase from ..factories import populate_database from ..models import Alarm, Domain diff --git a/modoboa/admin/tests/test_alias.py b/modoboa/admin/tests/test_alias.py index dafa6b2cd..f2984bdd1 100644 --- a/modoboa/admin/tests/test_alias.py +++ b/modoboa/admin/tests/test_alias.py @@ -13,7 +13,7 @@ class AliasTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(AliasTestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() def test_alias(self): @@ -173,7 +173,7 @@ class AliasTestCase(ModoTestCase): self.ajax_post(reverse("admin:alias_change", args=[fwd.id]), values) self.assertEqual(fwd.recipients_count, 1) - self.ajax_delete(reverse("admin:alias_delete") + "?selection=%d" % fwd.id, {}) + self.ajax_delete(reverse("admin:alias_delete") + f"?selection={fwd.id}", {}) self.assertRaises( Alias.DoesNotExist, Alias.objects.get, address="forward2@test.com" ) diff --git a/modoboa/admin/tests/test_domain.py b/modoboa/admin/tests/test_domain.py index f0b7fcd40..1b3859276 100644 --- a/modoboa/admin/tests/test_domain.py +++ b/modoboa/admin/tests/test_domain.py @@ -32,7 +32,7 @@ class DomainTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(DomainTestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() def test_create(self): @@ -364,10 +364,10 @@ class DomainTestCase(ModoTestCase): self.assertIn("test.com", response["rows"]) old_rows = response["rows"] - response = self.ajax_get("{}?sort_order=-name".format(url)) + response = self.ajax_get(f"{url}?sort_order=-name") self.assertNotEqual(old_rows, response["rows"]) - response = self.ajax_get("{}?sort_order=allocated_quota".format(url)) + response = self.ajax_get(f"{url}?sort_order=allocated_quota") self.assertNotEqual(old_rows, response["rows"]) def test_domain_logs_list_view(self): @@ -386,10 +386,10 @@ class DomainTestCase(ModoTestCase): self.assertIn("ID1", response["rows"]) old_rows = response["rows"] - response = self.ajax_get("{}?sort_order=-sender".format(url)) + response = self.ajax_get(f"{url}?sort_order=-sender") self.assertNotEqual(old_rows, response["rows"]) - response = self.ajax_get("{}?searchquery=ID1".format(url)) + response = self.ajax_get(f"{url}?searchquery=ID1") self.assertNotEqual(old_rows, response["rows"]) admin = User.objects.get(username="admin@test.com") @@ -421,7 +421,7 @@ class DomainTestCase(ModoTestCase): response = self.ajax_get(url) self.assertIn("handle_mailboxes", response) self.assertIn("listalarms", response["rows"]) - response = self.ajax_get("{}?objtype=quota".format(url)) + response = self.ajax_get(f"{url}?objtype=quota") self.assertIn("progress-bar", response["rows"]) @@ -431,7 +431,7 @@ class DKIMTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(DKIMTestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() def setUp(self): diff --git a/modoboa/admin/tests/test_domain_alias.py b/modoboa/admin/tests/test_domain_alias.py index 6cf4f07cf..883e0a061 100644 --- a/modoboa/admin/tests/test_domain_alias.py +++ b/modoboa/admin/tests/test_domain_alias.py @@ -10,7 +10,7 @@ class DomainAliasTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(DomainAliasTestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() cls.dom = Domain.objects.get(name="test.com") @@ -21,15 +21,13 @@ class DomainAliasTestCase(ModoTestCase): domal.target = dom domal.save() self.assertEqual(dom.domainalias_count, 1) - self.assertTrue(Alias.objects.filter(address="@{}".format(domal.name)).exists()) + self.assertTrue(Alias.objects.filter(address=f"@{domal.name}").exists()) domal.name = "domalias.org" domal.save() domal.delete() - self.assertFalse( - Alias.objects.filter(address="@{}".format(domal.name)).exists() - ) + self.assertFalse(Alias.objects.filter(address=f"@{domal.name}").exists()) def test_form(self): dom = Domain.objects.get(name="test.com") diff --git a/modoboa/admin/tests/test_export.py b/modoboa/admin/tests/test_export.py index 0de2728e0..ae4c8df90 100644 --- a/modoboa/admin/tests/test_export.py +++ b/modoboa/admin/tests/test_export.py @@ -17,7 +17,7 @@ class ExportTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(ExportTestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() def __export_domains(self, domfilter=""): @@ -31,7 +31,7 @@ class ExportTestCase(ModoTestCase): def __export_identities(self, idtfilter="", grpfilter=""): self.client.get( reverse("admin:_identity_list") - + "?grpfilter=%s&idtfilter=%s" % (grpfilter, idtfilter) + + f"?grpfilter={grpfilter}&idtfilter={idtfilter}" ) return self.client.post( reverse("admin:identity_export"), {"filename": "test.csv"} diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index 5a72ba6d5..ad09b2807 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -21,7 +21,7 @@ class ImportTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(ImportTestCase, cls).setUpTestData() + super().setUpTestData() cls.localconfig.parameters.set_value("enable_admin_limits", False, app="limits") cls.localconfig.save() factories.populate_database() @@ -326,9 +326,7 @@ alias;user@test.com;True;admin@test.com f = ContentFile( """domain; dómªin1.com; 1000; 100; True dómain; dómªin2.com; 1000; 100; True -""".encode( - "utf8" - ), +""".encode(), name="dómains.csv", ) self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) diff --git a/modoboa/admin/tests/test_mailbox_operations.py b/modoboa/admin/tests/test_mailbox_operations.py index 99b32a1cf..84a45a0de 100644 --- a/modoboa/admin/tests/test_mailbox_operations.py +++ b/modoboa/admin/tests/test_mailbox_operations.py @@ -13,21 +13,21 @@ from modoboa.lib.tests import ModoTestCase from .. import factories, models -@override_settings(DOVECOT_LOOKUP_PATH=["{}/dovecot".format(os.path.dirname(__file__))]) +@override_settings(DOVECOT_LOOKUP_PATH=[f"{os.path.dirname(__file__)}/dovecot"]) class MailboxOperationTestCase(ModoTestCase): """Test management command.""" @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(MailboxOperationTestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() def setUp(self): """Initiate test env.""" - super(MailboxOperationTestCase, self).setUp() + super().setUp() self.workdir = tempfile.mkdtemp() - path = "{}/test.com/admin".format(self.workdir) + path = f"{self.workdir}/test.com/admin" os.makedirs(path) self.set_global_parameter("handle_mailboxes", True) self.set_global_parameter("enable_admin_limits", False, app="limits") @@ -39,7 +39,7 @@ class MailboxOperationTestCase(ModoTestCase): @mock.patch("modoboa.admin.models.Mailbox.mail_home") def test_delete_account(self, mail_home_mock): """Check delete operation.""" - path = "{}/test.com/admin".format(self.workdir) + path = f"{self.workdir}/test.com/admin" mail_home_mock.__get__ = mock.Mock(return_value=path) mb = models.Mailbox.objects.select_related("user").get( address="admin", domain__name="test.com" @@ -52,7 +52,7 @@ class MailboxOperationTestCase(ModoTestCase): @mock.patch("modoboa.admin.models.Mailbox.mail_home") def test_rename_account(self, mail_home_mock): """Check rename operation.""" - path = "{}/test.com/admin".format(self.workdir) + path = f"{self.workdir}/test.com/admin" mail_home_mock.__get__ = mock.Mock(return_value=path) mb = models.Mailbox.objects.select_related("user").get( address="admin", domain__name="test.com" @@ -67,7 +67,7 @@ class MailboxOperationTestCase(ModoTestCase): "content": "content", } self.ajax_post(reverse("admin:account_change", args=[mb.user.pk]), values) - path = "{}/test.com/admin2".format(self.workdir) + path = f"{self.workdir}/test.com/admin2" mail_home_mock.__get__ = mock.Mock(return_value=path) call_command("handle_mailbox_operations") self.assertFalse(models.MailboxOperation.objects.exists()) @@ -76,7 +76,7 @@ class MailboxOperationTestCase(ModoTestCase): @mock.patch("modoboa.admin.models.Mailbox.mail_home") def test_delete_domain(self, mail_home_mock): """Check delete operation.""" - path = "{}/test.com/admin".format(self.workdir) + path = f"{self.workdir}/test.com/admin" mail_home_mock.__get__ = mock.Mock(return_value=path) domain = models.Domain.objects.get(name="test.com") self.ajax_post(reverse("admin:domain_delete", args=[domain.pk])) diff --git a/modoboa/admin/tests/test_mx.py b/modoboa/admin/tests/test_mx.py index 12bbfd3ce..a2e967b2c 100644 --- a/modoboa/admin/tests/test_mx.py +++ b/modoboa/admin/tests/test_mx.py @@ -23,7 +23,7 @@ class MXTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create some data.""" - super(MXTestCase, cls).setUpTestData() + super().setUpTestData() cls.domain = factories.DomainFactory(name="modoboa.org") # should not exist cls.bad_domain = factories.DomainFactory(name="does-not-exist.example.com") @@ -110,7 +110,7 @@ class MXTestCase(ModoTestCase): mb = factories.MailboxFactory( address="admin", domain=domain, - user__username="admin@{}".format(domain.name), + user__username=f"admin@{domain.name}", user__groups=("DomainAdmins",), ) domain.add_admin(mb.user) @@ -297,7 +297,7 @@ class DNSChecksTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create some data.""" - super(DNSChecksTestCase, cls).setUpTestData() + super().setUpTestData() cls.domain = factories.DomainFactory(name="dns-checks.com") @mock.patch("gevent.socket.gethostbyname") diff --git a/modoboa/admin/tests/test_password_schemes.py b/modoboa/admin/tests/test_password_schemes.py index de48eda3f..7a5d56fdc 100644 --- a/modoboa/admin/tests/test_password_schemes.py +++ b/modoboa/admin/tests/test_password_schemes.py @@ -10,7 +10,7 @@ class PasswordSchemesTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(PasswordSchemesTestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() def _create_account(self): diff --git a/modoboa/admin/tests/test_repair.py b/modoboa/admin/tests/test_repair.py index 6a6a1e6bb..3a23901c3 100644 --- a/modoboa/admin/tests/test_repair.py +++ b/modoboa/admin/tests/test_repair.py @@ -13,7 +13,7 @@ class RepairTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create some data.""" - super(RepairTestCase, cls).setUpTestData() + super().setUpTestData() factories.populate_database() def test_management_command(self): diff --git a/modoboa/admin/tests/test_user.py b/modoboa/admin/tests/test_user.py index f4614aab4..b5096ff92 100644 --- a/modoboa/admin/tests/test_user.py +++ b/modoboa/admin/tests/test_user.py @@ -12,7 +12,7 @@ class ForwardTestCase(ModoTestCase): """User forward test cases.""" def setUp(self): - super(ForwardTestCase, self).setUp() + super().setUp() populate_database() def test_set_forward(self): diff --git a/modoboa/admin/tests/utils.py b/modoboa/admin/tests/utils.py index f1b9eeaf5..f9056655e 100644 --- a/modoboa/admin/tests/utils.py +++ b/modoboa/admin/tests/utils.py @@ -11,13 +11,13 @@ from dns.rdtypes.ANY.TXT import TXT from dns.resolver import NXDOMAIN, NoAnswer, NoNameservers, Timeout -class RRset(object): +class RRset: def __init__(self): self.address = "192.0.2.1" -class RRsetInvalid(object): +class RRsetInvalid: def __init__(self): self.address = "000.0.0.0" diff --git a/modoboa/admin/views/alias.py b/modoboa/admin/views/alias.py index ac491811d..8b3e5c109 100644 --- a/modoboa/admin/views/alias.py +++ b/modoboa/admin/views/alias.py @@ -27,7 +27,7 @@ def _validate_alias(request, form, successmsg, callback=None): try: alias = form.save() except IntegrityError: - raise Conflict(_("Alias with this name already exists")) + raise Conflict(_("Alias with this name already exists")) from None if callback: callback(request.user, alias) return render_to_json_response(successmsg) @@ -110,13 +110,13 @@ class AliasDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailView): def has_permission(self): """Check object-level access.""" - result = super(AliasDetailView, self).has_permission() + result = super().has_permission() if not result: return result return self.request.user.can_access(self.get_object()) def get_context_data(self, **kwargs): """Add information to context.""" - context = super(AliasDetailView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context["selection"] = "identities" return context diff --git a/modoboa/admin/views/dns.py b/modoboa/admin/views/dns.py index cfdde03eb..778f24571 100644 --- a/modoboa/admin/views/dns.py +++ b/modoboa/admin/views/dns.py @@ -16,15 +16,11 @@ class MXDomainDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailView def get_queryset(self): """Add some prefetching.""" - return ( - super(MXDomainDetailView, self) - .get_queryset() - .prefetch_related("mxrecord_set") - ) + return super().get_queryset().prefetch_related("mxrecord_set") def get_context_data(self, **kwargs): """Add extra variables.""" - context = super(MXDomainDetailView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context.update({"title": _("MX records of {}").format(self.object.name)}) return context @@ -38,14 +34,10 @@ class DNSBLDomainDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailV def get_queryset(self): """Add some prefetching.""" - return ( - super(DNSBLDomainDetailView, self) - .get_queryset() - .prefetch_related("dnsblresult_set") - ) + return super().get_queryset().prefetch_related("dnsblresult_set") def get_context_data(self, **kwargs): """Add extra variables.""" - context = super(DNSBLDomainDetailView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context.update({"title": _("DNSBL summary for {}").format(self.object.name)}) return context diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py index b16980e7a..06b050cc2 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -150,10 +150,10 @@ def list_quotas(request): domains = Domain.objects.get_for_admin(request.user) domains = domains.exclude(quota=0) if sort_order in ["name", "quota"]: - domains = domains.order_by("{}{}".format(sort_dir, sort_order)) + domains = domains.order_by(f"{sort_dir}{sort_order}") elif sort_order == "allocated_quota": domains = domains.annotate(allocated_quota=Sum("mailbox__quota")).order_by( - "{}{}".format(sort_dir, sort_order) + f"{sort_dir}{sort_order}" ) page = get_listing_page(domains, request.GET.get("page", 1)) context = { @@ -182,7 +182,7 @@ def list_logs(request): ) else: logs = ml_models.Maillog.objects.all() - logs = logs.order_by("{}{}".format(sort_dir, sort_order)) + logs = logs.order_by(f"{sort_dir}{sort_order}") if search: logs = logs.filter( Q(sender__icontains=search) @@ -266,7 +266,7 @@ class DomainDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailView): def get_context_data(self, **kwargs): """Include extra widgets.""" - context = super(DomainDetailView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) result = signals.extra_domain_dashboard_widgets.send( self.__class__, user=self.request.user, domain=self.object ) diff --git a/modoboa/admin/views/export.py b/modoboa/admin/views/export.py index ccb9ec6cc..d390187bd 100644 --- a/modoboa/admin/views/export.py +++ b/modoboa/admin/views/export.py @@ -27,7 +27,7 @@ def _export(content, filename): resp = HttpResponse(content) resp["Content-Type"] = "text/csv" resp["Content-Length"] = len(content) - resp["Content-Disposition"] = 'attachment; filename="{}"'.format(filename) + resp["Content-Disposition"] = f'attachment; filename="{filename}"' return resp diff --git a/modoboa/admin/views/identity.py b/modoboa/admin/views/identity.py index 09aa11a8f..985c8b528 100644 --- a/modoboa/admin/views/identity.py +++ b/modoboa/admin/views/identity.py @@ -75,7 +75,7 @@ def list_quotas(request): ) mboxes = mboxes.exclude(quota=0) if sort_order in ["address", "quota"]: - mboxes = mboxes.order_by("%s%s" % (sort_dir, sort_order)) + mboxes = mboxes.order_by(f"{sort_dir}{sort_order}") elif sort_order in ("quota_value__bytes", "quota_usage"): db_type = db_type() if db_type == "mysql": @@ -85,9 +85,9 @@ def list_quotas(request): if sort_order == "quota_value__bytes": mboxes = mboxes.extra( select={"quota_value__bytes": "admin_quota.bytes"}, - where=["admin_quota.username=%s" % where], + where=[f"admin_quota.username={where}"], tables=["admin_quota", "admin_domain"], - order_by=["%s%s" % (sort_dir, sort_order)], + order_by=[f"{sort_dir}{sort_order}"], ) else: if db_type == "postgres": @@ -102,9 +102,9 @@ def list_quotas(request): ) mboxes = mboxes.extra( select={"quota_usage": select}, - where=["admin_quota.username=%s" % where], + where=[f"admin_quota.username={where}"], tables=["admin_quota", "admin_domain"], - order_by=["%s%s" % (sort_dir, sort_order)], + order_by=[f"{sort_dir}{sort_order}"], ) else: raise BadRequest(_("Invalid request")) @@ -199,7 +199,7 @@ def remove_permission(request): account = User.objects.get(pk=daid) domain = Domain.objects.get(pk=domid) except (User.DoesNotExist, Domain.DoesNotExist): - raise BadRequest(_("Invalid request")) + raise BadRequest(_("Invalid request")) from None if not request.user.can_access(account) or not request.user.can_access(domain): raise PermDeniedException domain.remove_admin(account) @@ -215,14 +215,14 @@ class AccountDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailView) def has_permission(self): """Check object-level access.""" - result = super(AccountDetailView, self).has_permission() + result = super().has_permission() if not result: return result return self.request.user.can_access(self.get_object()) def get_context_data(self, **kwargs): """Add information to context.""" - context = super(AccountDetailView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) del context["user"] result = signals.extra_account_dashboard_widgets.send( self.__class__, user=self.request.user, account=self.object diff --git a/modoboa/admin/views/import_.py b/modoboa/admin/views/import_.py index 80744e10d..fd7f184d4 100644 --- a/modoboa/admin/views/import_.py +++ b/modoboa/admin/views/import_.py @@ -1,8 +1,5 @@ """Import related views.""" -import csv -import io - from reversion import revisions as reversion from django.contrib.auth.decorators import ( @@ -10,15 +7,11 @@ from django.contrib.auth.decorators import ( permission_required, user_passes_test, ) -from django.db import transaction from django.shortcuts import render from django.urls import reverse -from django.utils.encoding import smart_str from django.utils.translation import gettext as _ -from modoboa.lib.exceptions import Conflict, ModoboaException from .. import lib -from .. import signals from ..forms import ImportDataForm, ImportIdentitiesForm diff --git a/modoboa/calendars/apps.py b/modoboa/calendars/apps.py index 8b847e87e..ffbace58f 100644 --- a/modoboa/calendars/apps.py +++ b/modoboa/calendars/apps.py @@ -10,7 +10,7 @@ class CalendarsConfig(AppConfig): verbose_name = "Modoboa calendars" def ready(self): - from modoboa.calendars import handlers + from modoboa.calendars import handlers # noqa from modoboa.calendars.app_settings import load_settings load_settings() diff --git a/modoboa/calendars/backends/__init__.py b/modoboa/calendars/backends/__init__.py index 8d463a517..b18191dfc 100644 --- a/modoboa/calendars/backends/__init__.py +++ b/modoboa/calendars/backends/__init__.py @@ -2,10 +2,8 @@ from importlib import import_module -from modoboa.lib.cryptutils import decrypt - -class CalendarBackend(object): +class CalendarBackend: """Base backend class.""" def __init__(self, calendar=None): @@ -31,11 +29,10 @@ class CalendarBackend(object): def get_backend(name, *args, **kwargs): """Return a backend instance.""" - module = import_module("modoboa.calendars.backends.{}".format(name)) - return getattr(module, "{}Backend".format(name.capitalize()))(*args, **kwargs) + module = import_module(f"modoboa.calendars.backends.{name}") + return getattr(module, f"{name.capitalize()}Backend")(*args, **kwargs) def get_backend_from_request(name, request, calendar=None): """Return a backend instance from a request.""" - # password = decrypt(request.session["password"]) return get_backend(name, request.user.email, request.auth, calendar=calendar) diff --git a/modoboa/calendars/backends/caldav_.py b/modoboa/calendars/backends/caldav_.py index 73df9b374..7bc513ebe 100644 --- a/modoboa/calendars/backends/caldav_.py +++ b/modoboa/calendars/backends/caldav_.py @@ -94,7 +94,7 @@ class Caldav_Backend(CalendarBackend): def update_event(self, uid, original_data): """Update an existing event.""" data = dict(original_data) - url = "{}/{}.ics".format(self.remote_cal.url.geturl(), uid) + url = f"{self.remote_cal.url.geturl()}/{uid}.ics" cal = self.remote_cal.event_by_url(url) orig_evt = cal.vobject_instance.vevent if "title" in data: @@ -125,7 +125,7 @@ class Caldav_Backend(CalendarBackend): # Calendar has been changed, remove old event first. self.remote_cal.client.delete(url) remote_cal = Calendar(self.client, data["calendar"].encoded_path) - url = "{}/{}.ics".format(remote_cal.url.geturl(), uid) + url = f"{remote_cal.url.geturl()}/{uid}.ics" else: remote_cal = self.remote_cal remote_cal.add_event(cal.instance) @@ -133,7 +133,7 @@ class Caldav_Backend(CalendarBackend): def get_event(self, uid): """Retrieve and event using its uid.""" - url = "{}/{}.ics".format(self.remote_cal.url.geturl(), uid) + url = f"{self.remote_cal.url.geturl()}/{uid}.ics" event = self.remote_cal.event_by_url(url) return self._serialize_event(event) @@ -147,7 +147,7 @@ class Caldav_Backend(CalendarBackend): def delete_event(self, uid): """Delete an event using its uid.""" - url = "{}/{}.ics".format(self.remote_cal.url.geturl(), uid) + url = f"{self.remote_cal.url.geturl()}/{uid}.ics" self.remote_cal.client.delete(url) def import_events(self, fp): diff --git a/modoboa/calendars/factories.py b/modoboa/calendars/factories.py index b8b0b32f5..ac3b37e46 100644 --- a/modoboa/calendars/factories.py +++ b/modoboa/calendars/factories.py @@ -12,7 +12,7 @@ class UserCalendarFactory(factory.django.DjangoModelFactory): class Meta: model = models.UserCalendar - name = factory.Sequence(lambda n: "User calendar %s" % n) + name = factory.Sequence(lambda n: f"User calendar {n}") mailbox = factory.SubFactory(admin_factories.MailboxFactory) @@ -21,7 +21,7 @@ class SharedCalendarFactory(factory.django.DjangoModelFactory): class Meta: model = models.SharedCalendar - name = factory.Sequence(lambda n: "Shared calendar %s" % n) + name = factory.Sequence(lambda n: f"Shared calendar {n}") domain = factory.SubFactory(admin_factories.DomainFactory) diff --git a/modoboa/calendars/handlers.py b/modoboa/calendars/handlers.py index 7fb8bcf54..0dbd20a56 100644 --- a/modoboa/calendars/handlers.py +++ b/modoboa/calendars/handlers.py @@ -48,7 +48,7 @@ def set_user_calendar_path(sender, instance, **kwargs): """Set path at creation.""" if instance.pk: return - instance._path = "{}/{}".format(instance.mailbox.full_address, instance.name) + instance._path = f"{instance.mailbox.full_address}/{instance.name}" if not instance.access_token: instance.access_token = token_hex(16) @@ -58,6 +58,6 @@ def set_shared_calendar_path(sender, instance, **kwargs): """Set path at creation.""" if instance.pk: return - instance._path = "{}/{}".format(instance.domain.name, instance.name) + instance._path = f"{instance.domain.name}/{instance.name}" if not instance.access_token: instance.access_token = token_hex(16) diff --git a/modoboa/calendars/management/commands/generate_rights.py b/modoboa/calendars/management/commands/generate_rights.py index b2413a1d3..b1b3a6dbf 100644 --- a/modoboa/calendars/management/commands/generate_rights.py +++ b/modoboa/calendars/management/commands/generate_rights.py @@ -30,21 +30,20 @@ class Command(BaseCommand): def _generate_acr(self, name, user, collection, perm="rw", comment=None): """Write a new access control rule to the config file.""" if comment is not None: - self._cfgfile.write("\n# %s" % comment) + self._cfgfile.write(f"\n# {comment}") self._cfgfile.write( - """ -[%s] -user = %s -collection = %s -permissions = %s + f""" +[{name}] +user = {user} +collection = {collection} +permissions = {perm} """ - % (name, user, collection, perm) ) def _user_access_rules(self): """Create user access rules.""" for acr in models.AccessRule.objects.select_related().all(): - section = "%s-to-%s-acr" % (acr.mailbox, acr.calendar) + section = f"{acr.mailbox}-to-{acr.calendar}-acr" permission = "" if acr.read: permission += "r" @@ -61,7 +60,7 @@ permissions = %s """Create token access rules.""" for model in [models.UserCalendar, models.SharedCalendar]: for calendar in model.objects.all(): - section = "token-{}-access".format(calendar._path) + section = f"token-{calendar._path}-access" self._generate_acr( section, calendar.access_token, @@ -73,15 +72,15 @@ permissions = %s def _super_admin_rules(self): """Generate access rules for super administrators.""" for sa in User.objects.filter(is_superuser=True): - section = "sa-%s-acr" % sa.username + section = f"sa-{sa.username}-acr" self._generate_acr(section, sa.username, ".*") def _domain_admin_rules(self): """Generate access rules for domain adminstrators.""" for da in User.objects.filter(groups__name="DomainAdmins"): for domain in Domain.objects.get_for_admin(da): - section = "da-%s-to-%s-acr" % (da.email, domain.name) - self._generate_acr(section, da.email, "%s/user/.*" % domain.name) + section = f"da-{da.email}-to-{domain.name}-acr" + self._generate_acr(section, da.email, f"{domain.name}/user/.*") def _generate_file(self, target): """ @@ -89,11 +88,10 @@ permissions = %s """ self._cfgfile = open(target, "w") self._cfgfile.write( - """# Rights management file for Radicale -# This file was generated by Modoboa on %s + f"""# Rights management file for Radicale +# This file was generated by Modoboa on {datetime.datetime.today()} # DO NOT EDIT MANUALLY! """ - % datetime.datetime.today() ) allow_calendars_administration = param_tools.get_global_parameter( diff --git a/modoboa/calendars/mocks.py b/modoboa/calendars/mocks.py index bc17cd742..c9dc21952 100644 --- a/modoboa/calendars/mocks.py +++ b/modoboa/calendars/mocks.py @@ -31,7 +31,7 @@ END:VCALENDAR """ -class Url(object): +class Url: def __init__(self, path): self.path = path @@ -85,5 +85,5 @@ class DAVClientMock: def proppatch(self, url, body, dummy=None): return Response(200) - def request(self, url, method="GET", body="", headers={}): + def request(self, url, method="GET", body="", headers=None): return Response(200) diff --git a/modoboa/calendars/models.py b/modoboa/calendars/models.py index f2ee8de1d..35cd954ac 100644 --- a/modoboa/calendars/models.py +++ b/modoboa/calendars/models.py @@ -53,7 +53,7 @@ class Calendar(models.Model): @property def share_url(self): """Return calendar share url.""" - return "{}?token={}".format(self.full_url, self.access_token) + return f"{self.full_url}?token={self.access_token}" @property def encoded_url(self): @@ -162,6 +162,7 @@ class AccessRule(models.Model): access = "r" if self.read else "" access += "w" if self.write else "" return smart_str( - "%s access rule to %s -> %s" - % (self.mailbox, self.calendar, access if access else "no access") + "{} access rule to {} -> {}".format( + self.mailbox, self.calendar, access if access else "no access" + ) ) diff --git a/modoboa/calendars/tests.py b/modoboa/calendars/tests.py index 55a85e92c..1b817e747 100644 --- a/modoboa/calendars/tests.py +++ b/modoboa/calendars/tests.py @@ -1,5 +1,3 @@ -# coding: utf-8 - """Radicale extension unit tests.""" import os @@ -24,13 +22,13 @@ from . import models from . import mocks -class TestDataMixin(object): +class TestDataMixin: """Create some data.""" @classmethod def setUpTestData(cls): """Create test data.""" - super(TestDataMixin, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() cls.account = core_models.User.objects.get(username="user@test.com") cls.calendar = factories.UserCalendarFactory(mailbox=cls.account.mailbox) @@ -60,12 +58,12 @@ class AccessRuleTestCase(ModoTestCase): @classmethod def setUpTestData(cls): """Create test data.""" - super(AccessRuleTestCase, cls).setUpTestData() + super().setUpTestData() populate_database() def setUp(self): """Initialize tests.""" - super(AccessRuleTestCase, self).setUp() + super().setUp() self.rights_file_path = tempfile.mktemp() self.set_global_parameter( "rights_file_path", self.rights_file_path, app="calendars" @@ -394,12 +392,12 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): self.client.post(reverse("core:login"), data) # FIXME: drf nested routers does not handle reverse() properly - url = "/api/v2/user-calendars/{}/events/{}/".format(self.calendar.pk, 1234) + url = f"/api/v2/user-calendars/{self.calendar.pk}/events/{1234}/" response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertTrue(response.json()["title"].startswith("Bastille")) - url = "/api/v2/user-calendars/{}/events/".format(self.calendar.pk) + url = f"/api/v2/user-calendars/{self.calendar.pk}/events/" url = "{}?start={}&end={}".format(url, "20060712T182145Z", "20070712T182145Z") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -411,12 +409,12 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): self.client.post(reverse("core:login"), data) # FIXME: drf nested routers does not handle reverse() properly - url = "/api/v2/shared-calendars/{}/events/{}/".format(self.scalendar.pk, 1234) + url = f"/api/v2/shared-calendars/{self.scalendar.pk}/events/{1234}/" response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertTrue(response.json()["title"].startswith("Bastille")) - url = "/api/v2/shared-calendars/{}/events/".format(self.scalendar.pk) + url = f"/api/v2/shared-calendars/{self.scalendar.pk}/events/" url = "{}?start={}&end={}".format(url, "20060712T182145Z", "20070712T182145Z") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -427,7 +425,7 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): data = {"username": "user@test.com", "password": "toto"} self.client.post(reverse("core:login"), data) - url = "/api/v2/user-calendars/{}/events/".format(self.calendar.pk) + url = f"/api/v2/user-calendars/{self.calendar.pk}/events/" data = { "title": "Test event", "start": "2018-03-27T00:00:00Z", @@ -441,7 +439,7 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): self.assertEqual(response.status_code, 201) self.assertIn("id", response.json()) - url = "/api/v2/shared-calendars/{}/events/".format(self.scalendar.pk) + url = f"/api/v2/shared-calendars/{self.scalendar.pk}/events/" data = { "title": "Test event 2", "start": "2018-03-27T00:00:00Z", @@ -460,7 +458,7 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): data = {"username": "user@test.com", "password": "toto"} self.client.post(reverse("core:login"), data) - url = "/api/v2/user-calendars/{}/events/1234/".format(self.calendar.pk) + url = f"/api/v2/user-calendars/{self.calendar.pk}/events/1234/" data = { "title": "Test event", "start": "2018-03-27T00:00:00Z", @@ -483,7 +481,7 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): data = {"username": "user@test.com", "password": "toto"} self.client.post(reverse("core:login"), data) - url = "/api/v2/user-calendars/{}/events/1234/".format(self.calendar.pk) + url = f"/api/v2/user-calendars/{self.calendar.pk}/events/1234/" data = { "start": "2018-03-27T00:00:00Z", "end": "2018-03-28T00:00:00Z", @@ -506,7 +504,7 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): "calendar": self.scalendar.pk, "new_calendar_type": "shared", } - url = "/api/v2/user-calendars/{}/events/1234/".format(self.calendar.pk) + url = f"/api/v2/user-calendars/{self.calendar.pk}/events/1234/" response = self.client.put(url, data=data, format="json") self.assertEqual(response.status_code, 200) @@ -515,7 +513,7 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): data = {"username": "user@test.com", "password": "toto"} self.client.post(reverse("core:login"), data) - url = "/api/v2/shared-calendars/{}/events/1234/".format(self.scalendar.pk) + url = f"/api/v2/shared-calendars/{self.scalendar.pk}/events/1234/" response = self.client.delete(url) self.assertEqual(response.status_code, 200) @@ -546,7 +544,7 @@ class AttendeeViewSetTestCase(ModoAPITestCase): @classmethod def setUpTestData(cls): - super(AttendeeViewSetTestCase, cls).setUpTestData() + super().setUpTestData() populate_database() cls.account = core_models.User.objects.get(username="user@test.com") @@ -567,7 +565,7 @@ class MailboxViewSetTestCase(ModoAPITestCase): @classmethod def setUpTestData(cls): - super(MailboxViewSetTestCase, cls).setUpTestData() + super().setUpTestData() populate_database() cls.account = core_models.User.objects.get(username="user@test.com") diff --git a/modoboa/contacts/apps.py b/modoboa/contacts/apps.py index fb3cab3ff..8f002beb8 100644 --- a/modoboa/contacts/apps.py +++ b/modoboa/contacts/apps.py @@ -7,7 +7,7 @@ class ModoboaContactsConfig(AppConfig): name = "modoboa.contacts" def ready(self): - from . import handlers + from . import handlers # noqa from modoboa.contacts.app_settings import load_settings load_settings() diff --git a/modoboa/contacts/factories.py b/modoboa/contacts/factories.py index a0bfe9aab..4141d294a 100644 --- a/modoboa/contacts/factories.py +++ b/modoboa/contacts/factories.py @@ -51,9 +51,7 @@ class ContactFactory(factory.django.DjangoModelFactory): first_name = "Homer" last_name = "Simpson" - display_name = factory.LazyAttribute( - lambda c: "{}{}".format(c.first_name, c.last_name) - ) + display_name = factory.LazyAttribute(lambda c: f"{c.first_name}{c.last_name}") @factory.post_generation def categories(self, create, extracted, **dummy_kwargs): diff --git a/modoboa/contacts/lib/carddav.py b/modoboa/contacts/lib/carddav.py index a4126bd7c..07f7c6e48 100644 --- a/modoboa/contacts/lib/carddav.py +++ b/modoboa/contacts/lib/carddav.py @@ -53,9 +53,9 @@ nsmap = { def ns(prefix, tag=None): - name = "{%s}" % nsmap[prefix] + name = f"{{{nsmap[prefix]}}}" if tag is not None: - name = "%s%s" % (name, tag) + name = f"{name}{tag}" return name @@ -91,7 +91,7 @@ class SyncCollectionQuery(elements.base.BaseElement): tag = ns("D", "sync-collection") -class PyCardDAV(object): +class PyCardDAV: """class for interacting with a CardDAV server Since PyCardDAV relies heavily on Requests [1] its SSL verification is also diff --git a/modoboa/contacts/management/commands/import_contacts.py b/modoboa/contacts/management/commands/import_contacts.py index f1fffc527..d13aa822b 100644 --- a/modoboa/contacts/management/commands/import_contacts.py +++ b/modoboa/contacts/management/commands/import_contacts.py @@ -44,7 +44,7 @@ class Command(BaseCommand): ).first() if not addressbook: raise CommandError( - "Address Book for email '%s' not found" % options["email"] + "Address Book for email '{}' not found".format(options["email"]) ) try: import_csv_file( @@ -55,5 +55,5 @@ class Command(BaseCommand): options.get("carddav_password"), ) except RuntimeError as err: - raise CommandError(err) + raise CommandError(err) from None self.stdout.write(self.style.SUCCESS("File was imported successfuly")) diff --git a/modoboa/contacts/mocks.py b/modoboa/contacts/mocks.py index f777ec4f6..00a590c48 100644 --- a/modoboa/contacts/mocks.py +++ b/modoboa/contacts/mocks.py @@ -98,10 +98,10 @@ def report_mock(url, request): def get_mock(url, request): """Simulate a GET request.""" uid = url.path.split("/")[-1] - content = """ + content = f""" BEGIN:VCARD VERSION:3.0 -UID:{} +UID:{uid} N:Gump;Forrest FN:Forrest Gump ORG:Bubba Gump Shrimp Co. @@ -111,7 +111,5 @@ TEL;TYPE=HOME;VOICE:(404) 555-1212 ADR;TYPE=HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America EMAIL;TYPE=PREF,INTERNET:forrestgump@example.com END:VCARD -""".format( - uid - ) +""" return {"status_code": 200, "content": content} diff --git a/modoboa/contacts/models.py b/modoboa/contacts/models.py index 16b6d90e0..375859835 100644 --- a/modoboa/contacts/models.py +++ b/modoboa/contacts/models.py @@ -82,18 +82,16 @@ class Contact(models.Model): """Set uid for new object.""" super().__init__(*args, **kwargs) if not self.pk: - self.uid = "{}.vcf".format(uuid.uuid4()) + self.uid = f"{uuid.uuid4()}.vcf" @property def url(self): - return "{}/{}.vcf".format(self.addressbook.url, self.uid) + return f"{self.addressbook.url}/{self.uid}.vcf" def to_vcard(self): """Convert this contact to a vCard.""" card = vobject.vCard() - card.add("prodid").value = "-//Modoboa//Contacts plugin {}//EN".format( - __version__ - ) + card.add("prodid").value = f"-//Modoboa//Contacts plugin {__version__}//EN" card.add("uid").value = self.uid card.add("n").value = vobject.vcard.Name( family=self.last_name, given=self.first_name @@ -193,4 +191,4 @@ class PhoneNumber(models.Model): db_table = "modoboa_contacts_phonenumber" def __str__(self): - return "{}: {}".format(self.type, self.number) + return f"{self.type}: {self.number}" diff --git a/modoboa/contacts/serializers.py b/modoboa/contacts/serializers.py index 77dec85a5..f1922791a 100644 --- a/modoboa/contacts/serializers.py +++ b/modoboa/contacts/serializers.py @@ -38,7 +38,7 @@ class EmailAddressWithNameSerializer(serializers.ModelSerializer): """Return display name.""" if obj.contact.display_name: return obj.contact.display_name - return "{} {}".format(obj.contact.first_name, obj.contact.last_name) + return f"{obj.contact.first_name} {obj.contact.last_name}" class PhoneNumberSerializer(serializers.ModelSerializer): diff --git a/modoboa/contacts/tests.py b/modoboa/contacts/tests.py index ce87e273b..9225399ce 100644 --- a/modoboa/contacts/tests.py +++ b/modoboa/contacts/tests.py @@ -1,4 +1,3 @@ -# coding: utf-8 """Contacts backend tests.""" import os @@ -25,7 +24,7 @@ class TestDataMixin: @classmethod def setUpTestData(cls): """Create test data.""" - super(TestDataMixin, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() cls.user = core_models.User.objects.get(username="user@test.com") cls.addressbook = cls.user.addressbook_set.first() @@ -172,14 +171,14 @@ class ContactViewSetTestCase(TestDataMixin, ModoAPITestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 3) - response = self.client.get("{}?search=homer".format(url)) + response = self.client.get(f"{url}?search=homer") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) def test_filter_by_category(self): """Check filtering.""" url = reverse("api:contact-list") - response = self.client.get("{}?category={}".format(url, self.category.name)) + response = self.client.get(f"{url}?category={self.category.name}") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) @@ -300,15 +299,15 @@ class EmailAddressViewSetTestCase(TestDataMixin, ModoAPITestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 3) - response = self.client.get("{}?search=homer".format(url)) + response = self.client.get(f"{url}?search=homer") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) - response = self.client.get("{}?search=Marge".format(url)) + response = self.client.get(f"{url}?search=Marge") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) - response = self.client.get("{}?search=Simpson".format(url)) + response = self.client.get(f"{url}?search=Simpson") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 3) @@ -339,7 +338,7 @@ class ImportTestCase(TestDataMixin, ModoTestCase): def test_import_from_outlook_auto(self): management.call_command("import_contacts", "user@test.com", self.path) address = models.EmailAddress.objects.get(address="toto@titi.com") - phone = models.PhoneNumber.objects.get(number="12345678") + models.PhoneNumber.objects.get(number="12345678") self.assertEqual(address.contact.first_name, "Toto Tata") self.assertEqual(address.contact.addressbook.user.email, "user@test.com") self.assertEqual(address.contact.address, "Street 1 Street 2") @@ -352,7 +351,7 @@ class ImportTestCase(TestDataMixin, ModoTestCase): backend="outlook", ) address = models.EmailAddress.objects.get(address="toto@titi.com") - phone = models.PhoneNumber.objects.get(number="12345678") + models.PhoneNumber.objects.get(number="12345678") self.assertEqual(address.contact.first_name, "Toto Tata") self.assertEqual(address.contact.addressbook.user.email, "user@test.com") self.assertEqual(address.contact.address, "Street 1 Street 2") @@ -366,7 +365,7 @@ class ImportTestCase(TestDataMixin, ModoTestCase): carddav_password="Toto1234", ) address = models.EmailAddress.objects.get(address="toto@titi.com") - phone = models.PhoneNumber.objects.get(number="12345678") + models.PhoneNumber.objects.get(number="12345678") self.assertEqual(address.contact.first_name, "Toto Tata") self.assertEqual(address.contact.addressbook.user.email, "user@test.com") self.assertEqual(address.contact.address, "Street 1 Street 2") diff --git a/modoboa/core/api/v1/viewsets.py b/modoboa/core/api/v1/viewsets.py index 34478de7e..2f4dad928 100644 --- a/modoboa/core/api/v1/viewsets.py +++ b/modoboa/core/api/v1/viewsets.py @@ -29,7 +29,7 @@ class AccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): def tfa_setup(self, request): """Initiate TFA setup.""" instance, created = TOTPDevice.objects.get_or_create( - user=request.user, defaults={"name": "{} TOTP device".format(request.user)} + user=request.user, defaults={"name": f"{request.user} TOTP device"} ) return response.Response() @@ -43,9 +43,9 @@ class AccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): serializer.is_valid(raise_exception=True) # create static device for recovery purposes device = StaticDevice.objects.create( - user=request.user, name="{} static device".format(request.user) + user=request.user, name=f"{request.user} static device" ) - for cpt in range(10): + for _cpt in range(10): token = StaticToken.random_token() device.token_set.create(token=token) django_otp.login(self.request, request.user.totpdevice_set.first()) @@ -82,7 +82,7 @@ class AccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): if device is None: return response.Response({"error": _("2FA is not enabled")}, status=403) device.token_set.all().delete() - for cpt in range(10): + for _cpt in range(10): token = StaticToken.random_token() device.token_set.create(token=token) return response.Response( diff --git a/modoboa/core/api/v2/serializers.py b/modoboa/core/api/v2/serializers.py index a25196ecd..80527b975 100644 --- a/modoboa/core/api/v2/serializers.py +++ b/modoboa/core/api/v2/serializers.py @@ -180,21 +180,21 @@ class CoreGlobalParametersSerializer(serializers.Serializer): try: value % {"user": "toto"} except (KeyError, ValueError): - raise serializers.ValidationError(_("Invalid syntax")) + raise serializers.ValidationError(_("Invalid syntax")) from None return value def validate_ldap_sync_account_dn_template(self, value): try: value % {"user": "toto"} except (KeyError, ValueError): - raise serializers.ValidationError(_("Invalid syntax")) + raise serializers.ValidationError(_("Invalid syntax")) from None return value def validate_ldap_search_filter(self, value): try: value % {"user": "toto"} except (KeyError, ValueError, TypeError): - raise serializers.ValidationError(_("Invalid syntax")) + raise serializers.ValidationError(_("Invalid syntax")) from None return value def validate_rounds_number(self, value): @@ -416,7 +416,7 @@ class PasswordRecoverySmsConfirmSerializer(serializers.Serializer): except KeyError: raise CustomValidationError( {"reason": "totp secret not set in session"}, 403 - ) + ) from None if not oath.accept_totp(self.context["totp_secret"], data["sms_totp"])[0]: raise CustomValidationError({"reason": "Wrong totp"}, 400) @@ -445,7 +445,7 @@ class PasswordRecoverySmsResendSerializer(serializers.Serializer): except KeyError: raise CustomValidationError( {"reason": "totp secret not set in session"}, status.HTTP_403_FORBIDDEN - ) + ) from None # Attempt to get user, will raise an error if pk is not valid self.context["user"] = User.objects.filter( @@ -501,7 +501,7 @@ class PasswordRecoveryConfirmSerializer(serializers.Serializer): try: password_validation.validate_password(data["new_password1"], user) except djangoValidationError as e: - raise PasswordRequirementsFailure(e.messages) + raise PasswordRequirementsFailure(e.messages) from None self.context["user"] = user return super().validate(data) diff --git a/modoboa/core/api/v2/views.py b/modoboa/core/api/v2/views.py index 40b2dce4f..5e2a6c6f0 100644 --- a/modoboa/core/api/v2/views.py +++ b/modoboa/core/api/v2/views.py @@ -25,11 +25,11 @@ from . import serializers logger = logging.getLogger("modoboa.auth") -def delete_cache_key(class_target, throttles, request): +def delete_cache_key(class_target, throttles: list, request) -> None: """Attempt to delete cache key from throttling on login/password reset success.""" for throttle in throttles: - if type(throttle) == class_target: + if isinstance(throttle, class_target): throttle.reset_cache(request) return diff --git a/modoboa/core/api/v2/viewsets.py b/modoboa/core/api/v2/viewsets.py index ecde8ee79..dc4e150f3 100644 --- a/modoboa/core/api/v2/viewsets.py +++ b/modoboa/core/api/v2/viewsets.py @@ -32,12 +32,12 @@ def create_static_tokens(request): device = request.user.staticdevice_set.first() if device is None: device = StaticDevice.objects.create( - user=request.user, name="{} static device".format(request.user) + user=request.user, name=f"{request.user} static device" ) elif device is not None: return None tokens = [] - for cpt in range(10): + for _cpt in range(10): token = StaticToken.random_token() device.token_set.create(token=token) tokens.append(token) diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 5fdb42c7f..a63f8112a 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -512,7 +512,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): } def __init__(self, *args, **kwargs): - super(GeneralParametersForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields["default_top_redirection"].choices = enabled_applications() self._add_visibilty_rules(sms_backends.get_all_backend_visibility_rules()) self.fields["password_scheme"].choices = get_password_scheme() @@ -532,7 +532,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): try: tpl % {"user": "toto"} except (KeyError, ValueError): - raise forms.ValidationError(_("Invalid syntax")) + raise forms.ValidationError(_("Invalid syntax")) from None return tpl def clean_ldap_sync_account_dn_template(self): @@ -540,7 +540,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): try: tpl % {"user": "toto"} except (KeyError, ValueError): - raise forms.ValidationError(_("Invalid syntax")) + raise forms.ValidationError(_("Invalid syntax")) from None return tpl def clean_ldap_search_filter(self): @@ -548,7 +548,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): try: ldap_filter % {"user": "toto"} except (KeyError, ValueError, TypeError): - raise forms.ValidationError(_("Invalid syntax")) + raise forms.ValidationError(_("Invalid syntax")) from None return ldap_filter def clean_rounds_number(self): @@ -613,10 +613,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): {"first_name": "givenName", "email": "mail", "last_name": "sn"}, ) ldap_uri = "ldaps://" if values["ldap_secured"] == "ssl" else "ldap://" - ldap_uri += "%s:%s" % ( - values[backend.srv_address_setting_name], - values[backend.srv_port_setting_name], - ) + ldap_uri += f"{values[backend.srv_address_setting_name]}:{values[backend.srv_port_setting_name]}" setattr(settings, backend.setting_fullname("SERVER_URI"), ldap_uri) if values["ldap_secured"] == "starttls": setattr(settings, backend.setting_fullname("START_TLS"), True) @@ -680,7 +677,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): Doing so, we can use the django_auth_ldap module. """ try: - import ldap + import ldap # noqa ldap_available = True except ImportError: diff --git a/modoboa/core/commands/__init__.py b/modoboa/core/commands/__init__.py index b935d655f..049ee7120 100644 --- a/modoboa/core/commands/__init__.py +++ b/modoboa/core/commands/__init__.py @@ -7,7 +7,7 @@ from django.template import Context, Template from django.utils.encoding import smart_str -class Command(object): +class Command: """Base command class. A valid administrative command must inherit from this class. @@ -25,7 +25,7 @@ class Command(object): {"BACKEND": ("django.template.backends.django.DjangoTemplates")} ] ) - self._templates_dir = "%s/templates" % os.path.dirname(__file__) + self._templates_dir = f"{os.path.dirname(__file__)}/templates" def _render_template(self, tplfile, env): """Render an HTML template.""" @@ -70,7 +70,7 @@ def scan_for_commands(dirname=""): else: cmdclassname = cmdname.capitalize() try: - cmdclass = getattr(cmdmod, "%sCommand" % cmdclassname) + cmdclass = getattr(cmdmod, f"{cmdclassname}Command") except AttributeError: continue result[cmdname] = cmdclass @@ -83,9 +83,10 @@ def handle_command_line(): parser = argparse.ArgumentParser( description="A set of utilities to ease the installation of Modoboa.", epilog="""Available commands: -%s -""" - % "\n".join(["\t%s" % c for c in sorted(commands)]), +{} +""".format( + "\n".join([f"\t{c}" for c in sorted(commands)]) + ), ) parser.add_argument( "--verbose", action="store_true", help="Activate verbose output" @@ -94,7 +95,7 @@ def handle_command_line(): (args, remaining) = parser.parse_known_args() if args.command not in commands: - print("Unknown command '%s'" % args.command, file=sys.stderr) + print(f"Unknown command '{args.command}'", file=sys.stderr) sys.exit(1) commands[args.command](commands, verbose=args.verbose).run(remaining) diff --git a/modoboa/core/commands/deploy.py b/modoboa/core/commands/deploy.py index 0a3efc0c5..4d3fa9ba0 100644 --- a/modoboa/core/commands/deploy.py +++ b/modoboa/core/commands/deploy.py @@ -114,10 +114,10 @@ class DeployCommand(Command): if p.returncode: if output: print( - "\n".join([l.decode() for l in output if l is not None]), + "\n".join([line.decode() for line in output if line is not None]), file=sys.stderr, ) - print("%s failed, check your configuration" % cmd, file=sys.stderr) + print(f"{cmd} failed, check your configuration", file=sys.stderr) def ask_db_info(self, name="default"): """Prompt the user for database information @@ -127,7 +127,7 @@ class DeployCommand(Command): :param name: the connection name """ - print("Configuring database connection: %s" % name) + print(f"Configuring database connection: {name}") info = { "conn_name": name, "ENGINE": input("Database type (mysql, postgres or sqlite3): "), @@ -137,7 +137,7 @@ class DeployCommand(Command): if info["ENGINE"] == "sqlite3": info["ENGINE"] = "django.db.backends.sqlite3" - info["NAME"] = "%s.db" % name + info["NAME"] = f"{name}.db" return info if info["ENGINE"] == "postgres": info["ENGINE"] = "django.db.backends.postgresql" @@ -146,7 +146,7 @@ class DeployCommand(Command): info["ENGINE"] = "django.db.backends.mysql" default_port = 3306 info["HOST"] = input("Database host (default: 'localhost'): ") - info["PORT"] = input("Database port (default: '%s'): " % default_port) + info["PORT"] = input(f"Database port (default: '{default_port}'): ") # leave port setting empty, if default value is supplied and # leave it to django if info["PORT"] == default_port: @@ -181,7 +181,7 @@ class DeployCommand(Command): for extension in extensions: module = __import__(extension[1], locals(), globals(), []) basedir = os.path.dirname(module.__file__) - if not os.path.exists("{0}/settings.py".format(basedir)): + if not os.path.exists(f"{basedir}/settings.py"): continue extra_settings.append(extension[1]) return extra_settings @@ -189,7 +189,7 @@ class DeployCommand(Command): def handle(self, parsed_args): django.setup() management.call_command("startproject", parsed_args.name, verbosity=False) - path = "%(name)s/%(name)s" % {"name": parsed_args.name} + path = f"{parsed_args.name}/{parsed_args.name}" sys.path.append(parsed_args.name) conn_tpl = Template(DBCONN_TPL) @@ -240,7 +240,7 @@ class DeployCommand(Command): mod = __import__(parsed_args.name, globals(), locals(), [smart_str("settings")]) tpl = self._render_template( - "%s/settings.py.tpl" % self._templates_dir, + f"{self._templates_dir}/settings.py.tpl", { "db_connections": connections, "secret_key": management.utils.get_random_secret_key(), @@ -254,15 +254,15 @@ class DeployCommand(Command): "extra_settings": extra_settings, }, ) - with open("%s/settings.py" % path, "w") as fp: + with open(f"{path}/settings.py", "w") as fp: fp.write(tpl) generate_rsa_private_key(parsed_args.name) - shutil.copyfile("%s/urls.py.tpl" % self._templates_dir, "%s/urls.py" % path) - os.mkdir("%s/media" % parsed_args.name) + shutil.copyfile(f"{self._templates_dir}/urls.py.tpl", f"{path}/urls.py") + os.mkdir(f"{parsed_args.name}/media") - if isfile("%s/settings.pyc" % path): - os.unlink("%s/settings.pyc" % path) + if isfile(f"{path}/settings.pyc"): + os.unlink(f"{path}/settings.pyc") self._exec_django_command("migrate", parsed_args.name, "--noinput") self._exec_django_command( "load_initial_data", diff --git a/modoboa/core/commands/help.py b/modoboa/core/commands/help.py index 2c7049b8f..bd5514711 100644 --- a/modoboa/core/commands/help.py +++ b/modoboa/core/commands/help.py @@ -8,14 +8,14 @@ class HelpCommand(Command): help = "Display the help message associated to a specific command" # NOQA:A003 def __init__(self, *args, **kwargs): - super(HelpCommand, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._parser.add_argument("name", type=str, help="A command name") def handle(self, parsed_args): if parsed_args.name not in self._commands: - print("Unknown command: %s" % parsed_args.name, file=sys.stderr) + print(f"Unknown command: {parsed_args.name}", file=sys.stderr) sys.exit(1) cmd = self._commands[parsed_args.name](self._commands) - cmd._parser.prog = "modoboa-admin.py %s" % parsed_args.name + cmd._parser.prog = f"modoboa-admin.py {parsed_args.name}" cmd._parser.description = cmd.help cmd._parser.print_help() diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index 2dfb607a6..55551fc49 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -124,7 +124,7 @@ class ExtensionsPool: root = "" pattern = "{}.urls_api" else: - root = r"^{}/".format(ext.get_url()) + root = rf"^{ext.get_url()}/" pattern = "{}.urls" try: result.append(re_path(root, include(pattern.format(ext_name)))) diff --git a/modoboa/core/factories.py b/modoboa/core/factories.py index 7c77d9feb..bad1e77c7 100644 --- a/modoboa/core/factories.py +++ b/modoboa/core/factories.py @@ -1,7 +1,5 @@ """Factories for core application.""" -import base64 - import factory from django.contrib.auth.models import Group @@ -12,7 +10,7 @@ from . import models class PermissionFactory(factory.django.DjangoModelFactory): """A base factory to handle permissions.""" - class Meta(object): + class Meta: abstract = True @factory.post_generation @@ -25,7 +23,7 @@ class PermissionFactory(factory.django.DjangoModelFactory): class GroupFactory(factory.django.DjangoModelFactory): """A factory to create Group instances.""" - class Meta(object): + class Meta: model = Group name = "DefaultGroup" @@ -34,7 +32,7 @@ class GroupFactory(factory.django.DjangoModelFactory): class UserFactory(PermissionFactory): """A factory to create User instances.""" - class Meta(object): + class Meta: model = models.User django_get_or_create = ("username",) @@ -54,7 +52,7 @@ class UserFactory(PermissionFactory): class LogFactory(factory.django.DjangoModelFactory): """Factory for Log.""" - class Meta(object): + class Meta: model = models.Log logger = "modoboa.admin" diff --git a/modoboa/core/forms.py b/modoboa/core/forms.py index fdc80c42c..25a646304 100644 --- a/modoboa/core/forms.py +++ b/modoboa/core/forms.py @@ -133,7 +133,7 @@ class APIAccessForm(forms.Form): def __init__(self, *args, **kwargs): """Initialize form.""" user = kwargs.pop("user") - super(APIAccessForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields["enable_api_access"].initial = hasattr(user, "auth_token") diff --git a/modoboa/core/management/commands/clean_inactive_accounts.py b/modoboa/core/management/commands/clean_inactive_accounts.py index fbfed94b9..dfddce767 100644 --- a/modoboa/core/management/commands/clean_inactive_accounts.py +++ b/modoboa/core/management/commands/clean_inactive_accounts.py @@ -72,7 +72,7 @@ class Command(BaseCommand): return action = "delete" if options["delete"] else "disable" if not options["silent"]: - answer = input("Do you want to {} those accounts? (y/N) ".format(action)) + answer = input(f"Do you want to {action} those accounts? (y/N) ") if not answer.lower().startswith("y"): return if action == "disable": diff --git a/modoboa/core/management/commands/cleanlogs.py b/modoboa/core/management/commands/cleanlogs.py index b7332a6e5..f44d6dabd 100644 --- a/modoboa/core/management/commands/cleanlogs.py +++ b/modoboa/core/management/commands/cleanlogs.py @@ -38,7 +38,7 @@ class Command(BaseCommand): self.verbose = options["verbose"] log_maximum_age = param_tools.get_global_parameter("log_maximum_age") - self.__vprint("Deleting logs older than %d days..." % log_maximum_age) + self.__vprint(f"Deleting logs older than {log_maximum_age} days...") limit = timezone.now() - datetime.timedelta(log_maximum_age) Log.objects.filter(date_created__lt=limit).delete() self.__vprint("Done.") diff --git a/modoboa/core/management/commands/generate_postfix_maps.py b/modoboa/core/management/commands/generate_postfix_maps.py index 5677d2700..6e0321867 100644 --- a/modoboa/core/management/commands/generate_postfix_maps.py +++ b/modoboa/core/management/commands/generate_postfix_maps.py @@ -126,9 +126,7 @@ query = {{ query|safe }} if os.path.exists(fullpath) and not force_overwrite: if not self.__check_file(fullpath): print( - "Cannot upgrade '{}' map because it has been modified.".format( - mapobject.filename - ) + f"Cannot upgrade '{mapobject.filename}' map because it has been modified." ) return self.__checksums[mapobject.filename] mapcontent = utils.parse_map_file(fullpath) diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index 2285ab8c3..1fa35758a 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -91,7 +91,7 @@ class Command(BaseCommand): extension.load_initial_data() except Exception as e: self.stderr.write( - "Unable to load initial data for '{}' ({}).".format(extname, str(e)) + f"Unable to load initial data for '{extname}' ({str(e)})." ) else: signals.initial_data_loaded.send(sender=self.__class__, extname=extname) @@ -141,7 +141,7 @@ class Command(BaseCommand): base_frontend_dir = os.path.join( os.path.dirname(__file__), "../../../frontend_dist/" ) - frontend_target_dir = "{}/frontend".format(settings.BASE_DIR) + frontend_target_dir = f"{settings.BASE_DIR}/frontend" if os.path.exists(base_frontend_dir): shutil.rmtree(frontend_target_dir, ignore_errors=True) shutil.copytree(base_frontend_dir, frontend_target_dir) diff --git a/modoboa/core/migrations/0002_delete_extension.py b/modoboa/core/migrations/0002_delete_extension.py index c43cf011f..49ac4aa32 100644 --- a/modoboa/core/migrations/0002_delete_extension.py +++ b/modoboa/core/migrations/0002_delete_extension.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 8f35eb108..351fbb904 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -373,15 +373,17 @@ class User(AbstractUser): if len(row) < 8 or not row[7].strip(): raise BadRequest( _( - "The simple user '%s' must have a valid email address" - % self.username + "The simple user '{}' must have a valid email address".format( + self.username + ) ) ) if self.username != row[7].strip(): raise BadRequest( _( - "username and email fields must not differ for '%s'" - % self.username + "username and email fields must not differ for '{}'".format( + self.username + ) ) ) @@ -517,4 +519,4 @@ class ExtensionUpdateHistory(models.Model): unique_together = [("extension", "version")] def __str__(self): - return "{}: {}".format(self.extension, self.name) + return f"{self.extension}: {self.name}" diff --git a/modoboa/core/password_hashers/__init__.py b/modoboa/core/password_hashers/__init__.py index 379732224..b5c1ce167 100644 --- a/modoboa/core/password_hashers/__init__.py +++ b/modoboa/core/password_hashers/__init__.py @@ -28,7 +28,7 @@ def get_password_hasher(scheme: str) -> type[PasswordHasher]: """ try: scheme = scheme.replace("-", "") - hasher = globals()["%sHasher" % scheme.upper()] + hasher = globals()[f"{scheme.upper()}Hasher"] except KeyError: hasher = PLAINHasher return hasher diff --git a/modoboa/core/password_hashers/advanced.py b/modoboa/core/password_hashers/advanced.py index 8b44d0367..12366b9b1 100644 --- a/modoboa/core/password_hashers/advanced.py +++ b/modoboa/core/password_hashers/advanced.py @@ -148,7 +148,7 @@ if argon2_hasher is not None: def __init__( self, ): - super(ARGON2IDHasher, self).__init__() + super().__init__() parameters = dict() diff --git a/modoboa/core/password_hashers/base.py b/modoboa/core/password_hashers/base.py index e1ff5056d..76314dfa4 100644 --- a/modoboa/core/password_hashers/base.py +++ b/modoboa/core/password_hashers/base.py @@ -80,7 +80,7 @@ class PasswordHasher(metaclass=MetaHasher): :return: encrypted password """ pwhash = self._b64encode(self._encrypt(force_str(clearvalue))) - return "%s%s" % (self.scheme, force_str(pwhash)) + return f"{self.scheme}{force_str(pwhash)}" def verify(self, clearvalue, hashed_value): """Verify a password against a hashed value. diff --git a/modoboa/core/password_hashers/utils.py b/modoboa/core/password_hashers/utils.py index 25b1155b4..a317f9104 100644 --- a/modoboa/core/password_hashers/utils.py +++ b/modoboa/core/password_hashers/utils.py @@ -58,7 +58,7 @@ def get_dovecot_schemes(): internal_name=DOVEADM_PASS_SCHEME_ALARM, ) - return ["{{{}}}".format(smart_str(scheme)) for scheme in schemes.split()], status + return [f"{{{smart_str(scheme)}}}" for scheme in schemes.split()], status def cache_available_password_hasher(bypass_cache=False): diff --git a/modoboa/core/password_validation.py b/modoboa/core/password_validation.py index e21fc3f06..7bf2d8b3a 100644 --- a/modoboa/core/password_validation.py +++ b/modoboa/core/password_validation.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext as _, ngettext -class ComplexityValidator(object): +class ComplexityValidator: """Check password contains at least a few things.""" def __init__(self, upper=1, lower=1, digits=1, specials=1): diff --git a/modoboa/core/sms_backends/__init__.py b/modoboa/core/sms_backends/__init__.py index f3abe9648..4f8ebb532 100644 --- a/modoboa/core/sms_backends/__init__.py +++ b/modoboa/core/sms_backends/__init__.py @@ -24,9 +24,9 @@ class SMSBackend: def get_backend_class(name): """Return class for given backend.""" - backend_module = import_module("modoboa.core.sms_backends.{}".format(name)) + backend_module = import_module(f"modoboa.core.sms_backends.{name}") try: - backend_class = getattr(backend_module, "{}Backend".format(name.upper())) + backend_class = getattr(backend_module, f"{name.upper()}Backend") except AttributeError: return None else: diff --git a/modoboa/core/sms_backends/ovh.py b/modoboa/core/sms_backends/ovh.py index a1c260ebe..0b04da74c 100644 --- a/modoboa/core/sms_backends/ovh.py +++ b/modoboa/core/sms_backends/ovh.py @@ -148,7 +148,7 @@ class OVHBackend(SMSBackend): """Send a new SMS to given recipients.""" services = self.client.get("/sms") result = self.client.post( - "/sms/{}/jobs".format(services[0]), + f"/sms/{services[0]}/jobs", message=text, receivers=recipients, priority="high", diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py index e7285fcf3..714a6043d 100644 --- a/modoboa/core/templatetags/core_tags.py +++ b/modoboa/core/templatetags/core_tags.py @@ -181,7 +181,7 @@ def colorize_level(level): } if level not in classes: return level - return "

%s

" % (classes[level], level) + return f"

{level}

" @register.filter @@ -238,7 +238,7 @@ def connected_users(parser, token): except ValueError: raise template.TemplateSyntaxError( "connected_users usage: {% connected_users as users %}" - ) + ) from None return ConnectedUsers(varname) @@ -279,14 +279,13 @@ def display_messages(msgs): timeout = "undefined" return mark_safe( - """ + f""" """ - % (level, text, timeout) ) @@ -295,5 +294,5 @@ def currencyfmt(amount): """Simple temp. filter to replace babel.""" lang = get_language() if lang == "fr": - return "{} €".format(amount) - return "€{}".format(amount) + return f"{amount} €" + return f"€{amount}" diff --git a/modoboa/core/tests/test_authentication.py b/modoboa/core/tests/test_authentication.py index b40832e4e..ef118c232 100644 --- a/modoboa/core/tests/test_authentication.py +++ b/modoboa/core/tests/test_authentication.py @@ -19,7 +19,7 @@ from modoboa.core.password_hashers.utils import get_dovecot_schemes from modoboa.lib.tests import NO_SMTP, ModoTestCase from .. import factories, models -DOVEADM_TEST_PATH = "{}/doveadm".format(os.path.dirname(__file__)) +DOVEADM_TEST_PATH = f"{os.path.dirname(__file__)}/doveadm" class MockedAttestedCredentialData: @@ -38,7 +38,7 @@ class AuthenticationTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(AuthenticationTestCase, cls).setUpTestData() + super().setUpTestData() from modoboa.admin.factories import MailboxFactory mailbox = MailboxFactory( @@ -322,7 +322,7 @@ class PasswordResetTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(PasswordResetTestCase, cls).setUpTestData() + super().setUpTestData() cls.account_ok = factories.UserFactory( username="user@test.com", secondary_email="test@ext.com", diff --git a/modoboa/core/tests/test_checks.py b/modoboa/core/tests/test_checks.py index 0c538b865..51d56d9b5 100644 --- a/modoboa/core/tests/test_checks.py +++ b/modoboa/core/tests/test_checks.py @@ -6,7 +6,6 @@ from django.test.utils import override_settings from modoboa.core import checks from modoboa.lib.tests import SimpleModoTestCase -from modoboa.core.password_hashers.base import CRYPTHasher class CheckSessionCookieSecureTest(SimpleModoTestCase): diff --git a/modoboa/core/tests/test_core.py b/modoboa/core/tests/test_core.py index a7be45f93..9b5f35c71 100644 --- a/modoboa/core/tests/test_core.py +++ b/modoboa/core/tests/test_core.py @@ -21,7 +21,7 @@ class AuthenticationTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(AuthenticationTestCase, cls).setUpTestData() + super().setUpTestData() cls.account = factories.UserFactory( username="user@test.com", groups=("SimpleUsers",) ) @@ -115,7 +115,7 @@ class ProfileTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(ProfileTestCase, cls).setUpTestData() + super().setUpTestData() cls.account = factories.UserFactory( username="user@test.com", groups=("SimpleUsers",) ) @@ -231,7 +231,7 @@ class APIAccessFormTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(APIAccessFormTestCase, cls).setUpTestData() + super().setUpTestData() cls.account = factories.UserFactory( username="user@test.com", groups=("SimpleUsers",) ) diff --git a/modoboa/core/tests/test_ldap.py b/modoboa/core/tests/test_ldap.py index d91d11359..21c6ae290 100644 --- a/modoboa/core/tests/test_ldap.py +++ b/modoboa/core/tests/test_ldap.py @@ -13,7 +13,7 @@ from .. import factories, models @skipIf(NO_LDAP, "No ldap module installed") -class LDAPTestCaseMixin(object): +class LDAPTestCaseMixin: """Set of methods used to test LDAP features.""" @cached_property @@ -84,7 +84,7 @@ class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase): def setUp(self): """Create test data.""" - super(LDAPAuthenticationTestCase, self).setUp() + super().setUp() self.activate_ldap_authentication() def check_created_user(self, username, group="SimpleUsers", with_mb=True): @@ -142,7 +142,7 @@ class ProfileTestCase(LDAPTestCaseMixin, ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(ProfileTestCase, cls).setUpTestData() + super().setUpTestData() cls.account = factories.UserFactory( username="user@test.com", groups=("SimpleUsers",) ) diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index da8c4f866..eb3cb61b6 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -148,7 +148,7 @@ class DashboardTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create some data.""" - super(DashboardTestCase, cls).setUpTestData() + super().setUpTestData() cls.dadmin = factories.UserFactory( username="admin@test.com", groups=("DomainAdmins",) ) @@ -236,7 +236,7 @@ class SettingsTestCase(ModoTestCase): url = reverse("core:parameters") response = self.ajax_get(url) for app in ["core", "admin", "limits"]: - self.assertIn('data-app="{}"'.format(app), response["content"]) + self.assertIn(f'data-app="{app}"', response["content"]) def test_save_settings(self): """Test settings save.""" @@ -317,13 +317,13 @@ class PreferencesTestCase(ModoTestCase): @classmethod def setUpClass(cls): """Register user form.""" - super(PreferencesTestCase, cls).setUpClass() + super().setUpClass() param_tools.registry.add("user", UserSettings, "Test") @classmethod def tearDownClass(cls): """Remove user class.""" - super(PreferencesTestCase, cls).tearDownClass() + super().tearDownClass() del param_tools.registry._registry["user"]["core"] def test_get_user_index(self): diff --git a/modoboa/core/tests/utils.py b/modoboa/core/tests/utils.py index 74106e195..c3a9968fc 100644 --- a/modoboa/core/tests/utils.py +++ b/modoboa/core/tests/utils.py @@ -2,4 +2,4 @@ import os def get_doveadm_test_path(): - return "{}/doveadm".format(os.path.dirname(__file__)) + return f"{os.path.dirname(__file__)}/doveadm" diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index a2fe6b3df..27d441458 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -88,7 +88,7 @@ def get_logs_page(request, page_id=None): if page_id is None: return None return get_listing_page( - Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)), page_id + Log.objects.all().order_by(f"{sort_dir}{sort_order}"), page_id ) diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index 058603293..1faa11c2b 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -141,11 +141,10 @@ class LoginView(LoginViewMixin, auth_views.LoginView): def form_invalid(self, form): # FIXME: should we return a 401 as before ? self.logger.warning( - "Failed connection attempt from '%(addr)s' as user '%(user)s'" - % { - "addr": self.request.META["REMOTE_ADDR"], - "user": escape(form.cleaned_data["username"]), - } + "Failed connection attempt from '{addr}' as user '{user}'".format( + addr=self.request.META["REMOTE_ADDR"], + user=escape(form.cleaned_data["username"]), + ) ) return self.render_to_response(self.get_context_data(form=form), status=401) @@ -220,7 +219,7 @@ class VerifySMSCodeView(generic.FormView): try: kwargs.update({"totp_secret": self.request.session["totp_secret"]}) except KeyError: - raise Http404 + raise Http404 from None return kwargs def form_valid(self, form): @@ -241,11 +240,11 @@ class ResendSMSCodeView(generic.View): "sms_password_recovery" ) if not sms_password_recovery: - raise Http404 + raise Http404 from None try: user = models.User._default_manager.get(pk=self.request.session["user_pk"]) except KeyError: - raise Http404 + raise Http404 from None backend = sms_backends.get_active_backend(self.request.localconfig.parameters) secret = cryptutils.random_hex_key(20) code = oath.totp(secret) @@ -255,7 +254,7 @@ class ResendSMSCodeView(generic.View): ) ) if not backend.send(text, [user.phone_number]): - raise Http404 + raise Http404 from None self.request.session["totp_secret"] = secret return JsonResponse({"status": "ok"}) diff --git a/modoboa/core/views/dashboard.py b/modoboa/core/views/dashboard.py index be9e01e89..2c3550b6c 100644 --- a/modoboa/core/views/dashboard.py +++ b/modoboa/core/views/dashboard.py @@ -37,7 +37,7 @@ class DashboardView(auth_mixins.AccessMixin, generic.TemplateView): lang = "en" context.update({"selection": "dashboard"}) - feed_url = "{}{}/weblog/feeds/".format(MODOBOA_WEBSITE_URL, lang) + feed_url = f"{MODOBOA_WEBSITE_URL}{lang}/weblog/feeds/" if self.request.user.role != "SuperAdmins": custom_feed_url = self.request.localconfig.parameters.get_value( "rss_feed_url" @@ -57,7 +57,7 @@ class DashboardView(auth_mixins.AccessMixin, generic.TemplateView): "hide_features_widget" ) if self.request.user.is_superuser or not hide_features_widget: - url = "{}{}/api/projects/?featured=true".format(MODOBOA_WEBSITE_URL, lang) + url = f"{MODOBOA_WEBSITE_URL}{lang}/api/projects/?featured=true" features = [] if not settings.DISABLE_DASHBOARD_EXTERNAL_QUERIES: try: diff --git a/modoboa/dmarc/admin.py b/modoboa/dmarc/admin.py index 8c38f3f3d..846f6b406 100644 --- a/modoboa/dmarc/admin.py +++ b/modoboa/dmarc/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/modoboa/dmarc/apps.py b/modoboa/dmarc/apps.py index d2c14dd1d..e2c202075 100644 --- a/modoboa/dmarc/apps.py +++ b/modoboa/dmarc/apps.py @@ -13,4 +13,4 @@ class DmarcConfig(AppConfig): def ready(self): load_settings() - from . import handlers + from . import handlers # noqa diff --git a/modoboa/dmarc/lib.py b/modoboa/dmarc/lib.py index 565cfacd5..1c39b6e04 100644 --- a/modoboa/dmarc/lib.py +++ b/modoboa/dmarc/lib.py @@ -129,7 +129,7 @@ def import_report(content): else: print(f"Report skipped because of malformed data (empty {attr})") return - value = setattr(report, "policy_{}".format(attr), node.text) + setattr(report, f"policy_{attr}", node.text) report.save() for record in root.findall("record"): import_record(record, report) @@ -174,7 +174,7 @@ def import_report_from_email(content): fpo.seek(0) if file_type in FILE_TYPES: import_archive(fpo, content_type=part.get_content_type()) - except (OSError, IOError): + except OSError: print("Error: the attachment does not match the mimetype") err = True else: @@ -219,8 +219,8 @@ def week_range(year, weeknumber): """Return start and end dates of a given week.""" tz = timezone.get_current_timezone() fmt = "%Y-%W-%w" - start_week = datetime.datetime.strptime("{}-{}-{}".format(year, weeknumber, 1), fmt) - end_week = datetime.datetime.strptime("{}-{}-{}".format(year, weeknumber, 0), fmt) + start_week = datetime.datetime.strptime(f"{year}-{weeknumber}-{1}", fmt) + end_week = datetime.datetime.strptime(f"{year}-{weeknumber}-{0}", fmt) return start_week.replace(tzinfo=tz), end_week.replace(tzinfo=tz) @@ -237,7 +237,7 @@ def insert_record(target: dict, record, name: str) -> None: } target[name][record.source_ip]["total"] += record.count for typ in ["spf", "dkim"]: - result = getattr(record, "{}_result".format(typ)) + result = getattr(record, f"{typ}_result") key = "success" if result == "pass" else "failure" target[name][record.source_ip][typ][key] += record.count diff --git a/modoboa/dmarc/management/commands/import_aggregated_report.py b/modoboa/dmarc/management/commands/import_aggregated_report.py index fd16c7ac0..badb8a647 100644 --- a/modoboa/dmarc/management/commands/import_aggregated_report.py +++ b/modoboa/dmarc/management/commands/import_aggregated_report.py @@ -1,7 +1,5 @@ """Import a DMARC aggregated report.""" -from __future__ import print_function - from django.core.management.base import BaseCommand from ... import lib diff --git a/modoboa/dmarc/models.py b/modoboa/dmarc/models.py index 08fef1f78..a72204570 100644 --- a/modoboa/dmarc/models.py +++ b/modoboa/dmarc/models.py @@ -39,7 +39,7 @@ class Reporter(models.Model): def __str__(self): """Return name and email.""" - return "{} <{}>".format(self.org_name, self.email) + return f"{self.org_name} <{self.email}>" class Report(models.Model): @@ -63,7 +63,7 @@ class Report(models.Model): def __str__(self): """Display provider and dates.""" - return "{}: {} -> {}".format(self.reporter, self.start_date, self.end_date) + return f"{self.reporter}: {self.start_date} -> {self.end_date}" class Record(models.Model): diff --git a/modoboa/dmarc/templatetags/dmarc_tags.py b/modoboa/dmarc/templatetags/dmarc_tags.py index 74c5b2eb4..25307534e 100644 --- a/modoboa/dmarc/templatetags/dmarc_tags.py +++ b/modoboa/dmarc/templatetags/dmarc_tags.py @@ -16,20 +16,16 @@ def next_period(period): """Return next period.""" current = period.split("-") if int(current[1]) == 52: - week0 = datetime.datetime.strptime( - "{}-0-1".format(int(current[0]) + 1), "%Y-%W-%w" - ) - week1 = datetime.datetime.strptime( - "{}-1-1".format(int(current[0]) + 1), "%Y-%W-%w" - ) - week52 = datetime.datetime.strptime("{}-52-1".format(current[0]), "%Y-%W-%w") + week0 = datetime.datetime.strptime(f"{int(current[0]) + 1}-0-1", "%Y-%W-%w") + week1 = datetime.datetime.strptime(f"{int(current[0]) + 1}-1-1", "%Y-%W-%w") + week52 = datetime.datetime.strptime(f"{current[0]}-52-1", "%Y-%W-%w") if week0 == week52 or week0 == week1: parts = (int(current[0]) + 1, 1) else: parts = (int(current[0]) + 1, 0) else: parts = (current[0], int(current[1]) + 1) - return mark_safe("{}-{}".format(parts[0], parts[1])) + return mark_safe(f"{parts[0]}-{parts[1]}") @register.simple_tag @@ -37,24 +33,22 @@ def previous_period(period): """Return previous period.""" current = period.split("-") if int(current[1]) == 1: - week0 = datetime.datetime.strptime("{}-0-1".format(current[0]), "%Y-%W-%w") - week1 = datetime.datetime.strptime("{}-1-1".format(current[0]), "%Y-%W-%w") + week0 = datetime.datetime.strptime(f"{current[0]}-0-1", "%Y-%W-%w") + week1 = datetime.datetime.strptime(f"{current[0]}-1-1", "%Y-%W-%w") if week0 == week1: parts = (int(current[0]) - 1, 52) else: parts = (current[0], 0) elif int(current[1]) == 0: - week0 = datetime.datetime.strptime("{}-0-1".format(current[0]), "%Y-%W-%w") - week52 = datetime.datetime.strptime( - "{}-52-1".format(int(current[0]) - 1), "%Y-%W-%w" - ) + week0 = datetime.datetime.strptime(f"{current[0]}-0-1", "%Y-%W-%w") + week52 = datetime.datetime.strptime(f"{int(current[0]) - 1}-52-1", "%Y-%W-%w") if week0 == week52: parts = (int(current[0]) - 1, 51) else: parts = (int(current[0]) - 1, 52) else: parts = (current[0], int(current[1]) - 1) - return mark_safe("{}-{}".format(parts[0], parts[1])) + return mark_safe(f"{parts[0]}-{parts[1]}") @register.filter diff --git a/modoboa/dmarc/tests/mixins.py b/modoboa/dmarc/tests/mixins.py index 3f27def1a..393ad4ba9 100644 --- a/modoboa/dmarc/tests/mixins.py +++ b/modoboa/dmarc/tests/mixins.py @@ -7,12 +7,12 @@ import sys from django.core.management import call_command -class CallCommandMixin(object): +class CallCommandMixin: """A mixin to provide command execution shortcuts.""" - def setUp(self): + def setUp(self, *args, **kwargs): """Replace stdin""" - super(CallCommandMixin, self).setUp() + super().setUp(*args, **kwargs) self.stdin = sys.stdin def tearDown(self): diff --git a/modoboa/dmarc/tests/test_management_command.py b/modoboa/dmarc/tests/test_management_command.py index 1a23be9ed..6631c37d7 100644 --- a/modoboa/dmarc/tests/test_management_command.py +++ b/modoboa/dmarc/tests/test_management_command.py @@ -12,7 +12,7 @@ class ManagementCommandTestCase(mixins.CallCommandMixin, ModoTestCase): @classmethod def setUpTestData(cls): - super(ManagementCommandTestCase, cls).setUpTestData() + super().setUpTestData() cls.domain = admin_factories.DomainFactory(name="ngyn.org") def test_import_from_archive(self): diff --git a/modoboa/dmarc/tests/test_views.py b/modoboa/dmarc/tests/test_views.py index 97d3bcf7e..cf605b0fc 100644 --- a/modoboa/dmarc/tests/test_views.py +++ b/modoboa/dmarc/tests/test_views.py @@ -15,7 +15,7 @@ class DMARCViewsTestCase(mixins.CallCommandMixin, ModoTestCase): @classmethod def setUpTestData(cls): """Create test data.""" - super(DMARCViewsTestCase, cls).setUpTestData() + super().setUpTestData() cls.domain = admin_factories.DomainFactory(name="ngyn.org") def test_domainlist_view(self): @@ -34,7 +34,7 @@ class DMARCViewsTestCase(mixins.CallCommandMixin, ModoTestCase): user = core_models.User.objects.get(username="admin") self.client.force_login(user) url = reverse("dmarc:domain_report", args=[self.domain.pk]) - response = self.client.get("{}?period=2015-26".format(url)) + response = self.client.get(f"{url}?period=2015-26") self.assertContains(response, "'Failed', 100.0") def test_domainreport_view_week0(self): @@ -43,7 +43,7 @@ class DMARCViewsTestCase(mixins.CallCommandMixin, ModoTestCase): user = core_models.User.objects.get(username="admin") self.client.force_login(user) url = reverse("dmarc:domain_report", args=[self.domain.pk]) - response = self.client.get("{}?period=2019-0".format(url)) + response = self.client.get(f"{url}?period=2019-0") self.assertContains(response, "Dec. 31, 2018") def test_domainreport_view_week1(self): @@ -52,7 +52,7 @@ class DMARCViewsTestCase(mixins.CallCommandMixin, ModoTestCase): user = core_models.User.objects.get(username="admin") self.client.force_login(user) url = reverse("dmarc:domain_report", args=[self.domain.pk]) - response = self.client.get("{}?period=2019-1".format(url)) + response = self.client.get(f"{url}?period=2019-1") self.assertContains(response, "Jan. 7, 2019") def test_domainreport_view_week52(self): @@ -61,7 +61,7 @@ class DMARCViewsTestCase(mixins.CallCommandMixin, ModoTestCase): user = core_models.User.objects.get(username="admin") self.client.force_login(user) url = reverse("dmarc:domain_report", args=[self.domain.pk]) - response = self.client.get("{}?period=2018-52".format(url)) + response = self.client.get(f"{url}?period=2018-52") self.assertContains(response, "Dec. 30, 2018") def test_domainreport_view_arc(self): @@ -70,7 +70,7 @@ class DMARCViewsTestCase(mixins.CallCommandMixin, ModoTestCase): user = core_models.User.objects.get(username="admin") self.client.force_login(user) url = reverse("dmarc:domain_report", args=[self.domain.pk]) - response = self.client.get("{}?period=2021-22".format(url)) + response = self.client.get(f"{url}?period=2021-22") self.assertContains(response, "'Fully aligned', 86.0") self.assertContains(response, "'Partially aligned', 8.0") self.assertContains(response, "'Forwarded', 4.0") diff --git a/modoboa/dmarc/views.py b/modoboa/dmarc/views.py index 547308bea..666b79a51 100644 --- a/modoboa/dmarc/views.py +++ b/modoboa/dmarc/views.py @@ -33,7 +33,7 @@ def insert_record(target, record, name): } target[name][record.source_ip]["total"] += record.count for typ in ["spf", "dkim"]: - result = getattr(record, "{}_result".format(typ)) + result = getattr(record, f"{typ}_result") target[name][record.source_ip][typ][result] += record.count @@ -41,8 +41,8 @@ def week_range(year, weeknumber): """Return start and end dates of a given week.""" tz = timezone.get_current_timezone() fmt = "%Y-%W-%w" - start_week = datetime.datetime.strptime("{}-{}-{}".format(year, weeknumber, 1), fmt) - end_week = datetime.datetime.strptime("{}-{}-{}".format(year, weeknumber, 0), fmt) + start_week = datetime.datetime.strptime(f"{year}-{weeknumber}-{1}", fmt) + end_week = datetime.datetime.strptime(f"{year}-{weeknumber}-{0}", fmt) return start_week.replace(tzinfo=tz), end_week.replace(tzinfo=tz) @@ -58,7 +58,7 @@ class DomainReportView(auth_mixins.PermissionRequiredMixin, generic.TemplateView if not self.period: year, week, day = timezone.now().isocalendar() week -= 1 - self.period = "{}-{}".format(year, week) + self.period = f"{year}-{week}" else: year, week = self.period.split("-") @@ -78,7 +78,7 @@ class DomainReportView(auth_mixins.PermissionRequiredMixin, generic.TemplateView def get_context_data(self, *args, **kwargs): """Extra context data.""" - context = super(DomainReportView, self).get_context_data(*args, **kwargs) + context = super().get_context_data(*args, **kwargs) qset = self.get_queryset() stats = {"total": 0, "aligned": 0, "trusted": 0, "forwarded": 0, "failed": 0} aligned = {} diff --git a/modoboa/dnstools/lib.py b/modoboa/dnstools/lib.py index eb48b5068..67ef8e257 100644 --- a/modoboa/dnstools/lib.py +++ b/modoboa/dnstools/lib.py @@ -17,7 +17,7 @@ def _get_record_type_value(records, rr_type): # Multiple strings are separated by a space as described in: # https://tools.ietf.org/html/rfc4408#section-3.1.3 value = str(record).replace('" "', "").strip('"') - if value.startswith("v={}".format(rr_type)): + if value.startswith(f"v={rr_type}"): return value return None @@ -30,14 +30,14 @@ def get_spf_record(domain): def get_dkim_record(domain, selector): """Return DKIM records form domain (if any).""" - name = "{}._domainkey.{}".format(selector, domain) + name = f"{selector}._domainkey.{domain}" records = admin_lib.get_dns_records(name, "TXT") return _get_record_type_value(records, "DKIM1") def get_dmarc_record(domain): """Return DMARC record for domain (if any).""" - name = "_dmarc.{}".format(domain) + name = f"_dmarc.{domain}" records = admin_lib.get_dns_records(name, "TXT") return _get_record_type_value(records, "DMARC1") @@ -61,12 +61,12 @@ def _get_simple_record(name): def get_autoconfig_record(domain): """Return autoconfig record for domain (if any).""" - return _get_simple_record("autoconfig.{}".format(domain)) + return _get_simple_record(f"autoconfig.{domain}") def get_autodiscover_record(domain): """Return autodiscover record for domain (if any).""" - return _get_simple_record("autodiscover.{}".format(domain)) + return _get_simple_record(f"autodiscover.{domain}") class DNSSyntaxError(Exception): @@ -83,7 +83,7 @@ def check_spf_ip4(value): try: ipaddress.ip_network(parts[1], False) except ValueError: - raise DNSSyntaxError(_("Wrong IPv4 address format")) + raise DNSSyntaxError(_("Wrong IPv4 address format")) from None def check_spf_ip6(value): @@ -94,7 +94,7 @@ def check_spf_ip6(value): try: ipaddress.ip_network(value, False) except ValueError: - raise DNSSyntaxError(_("Wrong IPv6 address format")) + raise DNSSyntaxError(_("Wrong IPv6 address format")) from None def _check_domain_and_mask(value, mechanism): @@ -161,15 +161,13 @@ def check_spf_syntax(record): for part in parts: if part == "": continue - qualifier = None if part[0] in ["+", "-", "~", "?"]: - qualifier = part[0] part = part[1:] if part == "all": continue for mechanism in constants.SPF_MECHANISMS: if part.startswith(mechanism): - globals()["check_spf_{}".format(mechanism)](part) + globals()[f"check_spf_{mechanism}"](part) mechanisms.append(mechanism) break else: @@ -231,7 +229,7 @@ def check_dmarc_tag(tag, value): try: value = int(value) except ValueError: - raise DNSSyntaxError(error + _(" not an integer")) + raise DNSSyntaxError(error + _(" not an integer")) from None if "min_value" in tdef and value < tdef["min_value"]: raise DNSSyntaxError(error + _(" less than {}").format(tdef["min_value"])) if "max_value" in tdef and value > tdef["max_value"]: diff --git a/modoboa/dnstools/models.py b/modoboa/dnstools/models.py index e74e495c7..e606dbd7e 100644 --- a/modoboa/dnstools/models.py +++ b/modoboa/dnstools/models.py @@ -50,7 +50,7 @@ class DNSRecord(models.Model): objects = DNSRecordManager() def __str__(self): - return "{} ({}): {}".format(self.domain, self.type, self.value) + return f"{self.domain} ({self.type}): {self.value}" def get_dns_record(self): """Retrieve corresponding DNS record.""" @@ -59,13 +59,13 @@ class DNSRecord(models.Model): self.domain.name, self.domain.dkim_key_selector ) else: - func = getattr(lib, "get_{}_record".format(self.type)) + func = getattr(lib, f"get_{self.type}_record") self.value = func(self.domain.name) def check_syntax(self, ttl=7200): """Check record syntax.""" try: - func = getattr(lib, "check_{}_syntax".format(self.type)) + func = getattr(lib, f"check_{self.type}_syntax") result = func(self.value) except lib.DNSSyntaxError as err: self.error = str(err)[:50] diff --git a/modoboa/dnstools/tests.py b/modoboa/dnstools/tests.py index a3e57430d..1b2bf7411 100644 --- a/modoboa/dnstools/tests.py +++ b/modoboa/dnstools/tests.py @@ -133,7 +133,7 @@ class ViewsTestCase(ModoTestCase): @classmethod def setUpTestData(cls): """Create some records.""" - super(ViewsTestCase, cls).setUpTestData() + super().setUpTestData() cls.spf_rec = factories.DNSRecordFactory( type="spf", value="v=SPF1 mx -all", is_valid=True, domain__name="test.com" ) diff --git a/modoboa/dnstools/views.py b/modoboa/dnstools/views.py index c8871c783..74472d9a3 100644 --- a/modoboa/dnstools/views.py +++ b/modoboa/dnstools/views.py @@ -16,7 +16,7 @@ class DomainAccessRequiredMixin(auth_mixins.AccessMixin): def dispatch(self, request, *args, **kwargs): if not request.user.can_access(self.get_object()): return self.handle_no_permission() - return super(DomainAccessRequiredMixin, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) class DNSRecordDetailView( @@ -29,7 +29,7 @@ class DNSRecordDetailView( def get_context_data(self, **kwargs): """Add extra variables.""" - context = super(DNSRecordDetailView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context.update( { "title": _("{} record of {}").format( @@ -59,7 +59,7 @@ class AutoConfigRecordsStatusView( def get_context_data(self, **kwargs): """Add extra data to context.""" - context = super(AutoConfigRecordsStatusView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context.update( {"title": _("Auto configuration records for {}").format(self.object.name)} ) @@ -76,6 +76,6 @@ class DomainDNSConfigurationView( def get_context_data(self, **kwargs): """Add extra variables.""" - context = super(DomainDNSConfigurationView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context.update({"title": _("DNS configuration for {}").format(self.object)}) return context diff --git a/modoboa/imap_migration/__init__.py b/modoboa/imap_migration/__init__.py index 62d56a35c..6b3d80114 100644 --- a/modoboa/imap_migration/__init__.py +++ b/modoboa/imap_migration/__init__.py @@ -1,9 +1,5 @@ -# -*- coding: utf-8 -*- - """A plugin to migrate mailboxes using IMAP.""" -from __future__ import unicode_literals - from pkg_resources import get_distribution, DistributionNotFound diff --git a/modoboa/imap_migration/api/v2/viewsets.py b/modoboa/imap_migration/api/v2/viewsets.py index ef069ac20..91d92337b 100644 --- a/modoboa/imap_migration/api/v2/viewsets.py +++ b/modoboa/imap_migration/api/v2/viewsets.py @@ -1,7 +1,6 @@ """Viewsets for API V2 for imap_migration.""" import imaplib -import socket import ssl from rest_framework import response, filters, mixins, permissions, viewsets @@ -38,7 +37,7 @@ class EmailProviderViewSet(GetThrottleViewsetMixin, viewsets.ModelViewSet): imaplib.IMAP4_SSL(address, port) else: imaplib.IMAP4(address, port) - except (socket.error, imaplib.IMAP4.error, ssl.SSLError): + except (OSError, imaplib.IMAP4.error, ssl.SSLError): return response.Response({"status": "failed"}, 400) return response.Response(status=200) diff --git a/modoboa/imap_migration/auth_backends.py b/modoboa/imap_migration/auth_backends.py index 1765e8fda..fd84b4969 100644 --- a/modoboa/imap_migration/auth_backends.py +++ b/modoboa/imap_migration/auth_backends.py @@ -1,7 +1,6 @@ """IMAP authentication backend for Django.""" import imaplib -import socket import ssl from django.utils.encoding import smart_bytes, smart_str @@ -45,8 +44,10 @@ class IMAPBackend: conn = imaplib.IMAP4_SSL(address, port) else: conn = imaplib.IMAP4(address, port) - except (socket.error, imaplib.IMAP4.error, ssl.SSLError) as error: - raise ModoboaException(_("Connection to IMAP server failed: %s") % error) + except (OSError, imaplib.IMAP4.error, ssl.SSLError) as error: + raise ModoboaException( + _(f"Connection to IMAP server failed: {error}") + ) from None try: typ, data = conn.login(smart_bytes(username), smart_str(password)) @@ -68,9 +69,7 @@ class IMAPBackend: orig_username = username # Check if old addresses must be converted if self.provider_domain.new_domain: - username = "{}@{}".format( - self.address, self.provider_domain.new_domain.name - ) + username = f"{self.address}@{self.provider_domain.new_domain.name}" user, created = core_models.User.objects.get_or_create( username__iexact=username, defaults={"username": username.lower(), "email": username.lower()}, diff --git a/modoboa/imap_migration/factories.py b/modoboa/imap_migration/factories.py index f4c8f12f8..5a567fe0b 100644 --- a/modoboa/imap_migration/factories.py +++ b/modoboa/imap_migration/factories.py @@ -13,7 +13,7 @@ class EmailProviderFactory(factory.django.DjangoModelFactory): class Meta: model = models.EmailProvider - name = factory.Sequence(lambda n: "Provider {}".format(n)) + name = factory.Sequence(lambda n: f"Provider {n}") address = "127.0.0.1" port = 143 diff --git a/modoboa/imap_migration/management/commands/generate_offlineimap_config.py b/modoboa/imap_migration/management/commands/generate_offlineimap_config.py index 7d18d77ef..aa76b79d2 100644 --- a/modoboa/imap_migration/management/commands/generate_offlineimap_config.py +++ b/modoboa/imap_migration/management/commands/generate_offlineimap_config.py @@ -33,7 +33,7 @@ class Command(BaseCommand): "imap_create_folders": conf["create_folders"], "imap_folder_filter_exclude": conf["folder_filter_exclude"], "imap_folder_filter_include": ", ".join( - "'{0}'".format(w) for w in conf["folder_filter_include"].split(",") + f"'{w}'" for w in conf["folder_filter_include"].split(",") ), "migrations": Migration.objects.select_related( "provider", "mailbox__domain" diff --git a/modoboa/imap_migration/tests.py b/modoboa/imap_migration/tests.py index 8093f9d5f..9e3129ad1 100644 --- a/modoboa/imap_migration/tests.py +++ b/modoboa/imap_migration/tests.py @@ -21,12 +21,12 @@ from . import factories from . import models -class DataMixin(object): +class DataMixin: """A mixin to provide test data.""" @classmethod def setUpTestData(cls): - super(DataMixin, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() cls.mb = admin_models.Mailbox.objects.get(user__username="user@test.com") cls.migration = factories.MigrationFactory( @@ -110,7 +110,7 @@ class ManagementCommandTestCase(DataMixin, ModoTestCase): """Management command test cases.""" def setUp(self): - super(ManagementCommandTestCase, self).setUp() + super().setUp() self.workdir = tempfile.mkdtemp() def tearDown(self): diff --git a/modoboa/imap_migration/views.py b/modoboa/imap_migration/views.py index 334518a21..d6ac2468e 100644 --- a/modoboa/imap_migration/views.py +++ b/modoboa/imap_migration/views.py @@ -18,4 +18,4 @@ class IndexView( def get_context_data(self, **kwargs): """Set menu selection.""" kwargs.update({"selection": "imap_migrations"}) - return super(IndexView, self).get_context_data(**kwargs) + return super().get_context_data(**kwargs) diff --git a/modoboa/ldapsync/lib.py b/modoboa/ldapsync/lib.py index 441c3dbc8..eb34b0bb6 100644 --- a/modoboa/ldapsync/lib.py +++ b/modoboa/ldapsync/lib.py @@ -14,7 +14,7 @@ from modoboa.lib.exceptions import InternalError def create_connection(srv_address, srv_port, config, username, password): """Create a new connection with given server.""" - uri = "{}:{}".format(srv_address, srv_port) + uri = f"{srv_address}:{srv_port}" uri = "{}://{}".format("ldaps" if config["ldap_secured"] == "ssl" else "ldap", uri) conn = ldap.initialize(uri) conn.protocol_version = 3 @@ -79,7 +79,7 @@ def create_ldap_account(user, dn, conn): try: conn.add_s(dn, ldif) except ldap.LDAPError as e: - raise InternalError(_("Failed to create LDAP account: {}").format(e)) + raise InternalError(_("Failed to create LDAP account: {}").format(e)) from None def check_if_dn_exists(conn, dn): @@ -118,7 +118,7 @@ def update_ldap_account(user, config): try: conn.modify_s(dn, ldif) except ldap.LDAPError as e: - raise InternalError(_("Failed to update LDAP account: {}").format(e)) + raise InternalError(_("Failed to update LDAP account: {}").format(e)) from None def delete_ldap_account(user, config): @@ -131,14 +131,18 @@ def delete_ldap_account(user, config): try: conn.delete_s(dn) except ldap.LDAPError as e: - raise InternalError(_("Failed to delete LDAP account: {}").format(e)) + raise InternalError( + _("Failed to delete LDAP account: {}").format(e) + ) from None else: password = get_user_password(user, True) ldif = [(ldap.MOD_REPLACE, "userPassword", password)] try: conn.modify_s(dn, ldif) except ldap.LDAPError as e: - raise InternalError(_("Failed to disable LDAP account: {}").format(e)) + raise InternalError( + _("Failed to disable LDAP account: {}").format(e) + ) from None def find_user_groups(conn, config, dn, entry): @@ -148,13 +152,13 @@ def find_user_groups(conn, config, dn, entry): or config["ldap_group_type"] == "groupofnames" ) if condition: - flt = "(member={})".format(dn) + flt = f"(member={dn})" elif config["ldap_group_type"] == "posixgroup": flt = "(memberUid={})".format(force_str(entry["uid"][0])) result = conn.search_s(config["ldap_groups_search_base"], ldap.SCOPE_SUBTREE, flt) groups = [] - for dn, entry in result: + for dn, _entry in result: if not dn: continue groups.append(dn.split(",")[0].split("=")[1]) @@ -200,7 +204,7 @@ def import_accounts_from_ldap(config): break if email is None: if grp == "SimpleUsers": - print("Skipping {} because no email found".format(dn)) + print(f"Skipping {dn} because no email found") continue else: username = email @@ -231,8 +235,8 @@ def build_ldap_uri(config, node=""): node += "_" return "{}://{}:{}".format( "ldaps" if config["ldap_secured"] == "ssl" else "ldap", - config["ldap_{}server_address".format(node)], - config["ldap_{}server_port".format(node)], + config[f"ldap_{node}server_address"], + config[f"ldap_{node}server_port"], ) @@ -255,18 +259,11 @@ def update_dovecot_config_file(config): with open(conf_file, "w") as fp: fp.write( - """uris = {uris} + f"""uris = {uris} dn = "{bind_dn}" dnpass = '{bind_pwd}' base = {base} user_filter = {user_filter} -pass_filter = {pass_filter} -""".format( - uris=uris, - bind_dn=bind_dn, - bind_pwd=bind_pwd, - base=base, - user_filter=user_filter, - pass_filter=user_filter, - ) +pass_filter = {user_filter} +""" ) diff --git a/modoboa/lib/api_client.py b/modoboa/lib/api_client.py index 1f485fd55..0d1a11033 100644 --- a/modoboa/lib/api_client.py +++ b/modoboa/lib/api_client.py @@ -12,7 +12,7 @@ from django.utils.translation import gettext as _ logger = logging.getLogger("modoboa.admin") -class ModoAPIClient(object): +class ModoAPIClient: """A simple client for the public API.""" def __init__(self, api_url=None): @@ -57,10 +57,10 @@ class ModoAPIClient(object): def register_instance(self, hostname): """Register this instance.""" - url = "{}instances/search/?hostname={}".format(self._api_url, hostname) + url = f"{self._api_url}instances/search/?hostname={hostname}" instance = self.__send_request(url) if instance is None: - url = "{}instances/".format(self._api_url) + url = f"{self._api_url}instances/" data = {"hostname": hostname, "known_version": self.local_core_version} try: response = requests.post(url, data=data) @@ -76,7 +76,7 @@ class ModoAPIClient(object): def update_instance(self, pk, data): """Update instance and send stats.""" - url = "{}instances/{}/".format(self._api_url, pk) + url = f"{self._api_url}instances/{pk}/" try: response = requests.put(url, data=data) except RequestException as err: @@ -89,5 +89,5 @@ class ModoAPIClient(object): def versions(self): """Fetch core and extension versions.""" - url = "{}versions/".format(self._api_url) + url = f"{self._api_url}versions/" return self.__send_request(url) diff --git a/modoboa/lib/authbackends.py b/modoboa/lib/authbackends.py index 543efd0f8..1711dbb82 100644 --- a/modoboa/lib/authbackends.py +++ b/modoboa/lib/authbackends.py @@ -9,7 +9,7 @@ from modoboa.lib.email_utils import split_mailbox from modoboa.parameters import tools as param_tools -class SMTPBackend(object): +class SMTPBackend: """A backend to authenticate against an SMTP server.""" def authenticate(self, request, username=None, password=None): @@ -126,7 +126,7 @@ try: @classmethod def setting_fullname(cls, setting): """Return fullname for given setting.""" - return "{}{}".format(cls.settings_prefix, setting) + return f"{cls.settings_prefix}{setting}" class LDAPBackend(LDAPBackendBase): """Primary LDAP backend.""" diff --git a/modoboa/lib/connections.py b/modoboa/lib/connections.py index d830d509c..aabf460fa 100644 --- a/modoboa/lib/connections.py +++ b/modoboa/lib/connections.py @@ -9,7 +9,7 @@ class ConnectionsManager(type): """ def __init__(self, name, bases, ctx): - super(ConnectionsManager, self).__init__(name, bases, ctx) + super().__init__(name, bases, ctx) self.instances = {} def __call__(self, **kwargs): @@ -24,7 +24,7 @@ class ConnectionsManager(type): kwargs["password"] = decrypt(kwargs["password"]) if self.instances[key] is None: - self.instances[key] = super(ConnectionsManager, self).__call__(**kwargs) + self.instances[key] = super().__call__(**kwargs) else: self.instances[key].refresh(key, kwargs["password"]) return self.instances[key] diff --git a/modoboa/lib/cryptutils.py b/modoboa/lib/cryptutils.py index 19d3f004b..303aed64a 100644 --- a/modoboa/lib/cryptutils.py +++ b/modoboa/lib/cryptutils.py @@ -1,13 +1,8 @@ """Crypto related utilities.""" import base64 -import time from cryptography.fernet import Fernet -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.twofactor import InvalidToken -from cryptography.hazmat.primitives.twofactor.totp import TOTP -from cryptography.hazmat.primitives.hashes import SHA1 from django.conf import settings from django.utils.crypto import get_random_string diff --git a/modoboa/lib/db_utils.py b/modoboa/lib/db_utils.py index 6c3f0e575..c817394c8 100644 --- a/modoboa/lib/db_utils.py +++ b/modoboa/lib/db_utils.py @@ -19,7 +19,7 @@ def db_type(cname="default"): :return: a string or None """ if cname not in settings.DATABASES: - raise InternalError(_("Connection to database %s not configured" % cname)) + raise InternalError(_("Connection to database {} not configured".format(cname))) for t in ["postgres", "mysql", "sqlite"]: if settings.DATABASES[cname]["ENGINE"].find(t) != -1: return t diff --git a/modoboa/lib/email_utils.py b/modoboa/lib/email_utils.py index b216336bc..6c2501dfb 100644 --- a/modoboa/lib/email_utils.py +++ b/modoboa/lib/email_utils.py @@ -31,7 +31,7 @@ class EmailAddress: def __init__(self, address): self.name, self.address = u2u_decode.decode_address(address) if self.name: - self.fulladdress = "{} <{}>".format(self.name, self.address) + self.fulladdress = f"{self.name} <{self.address}>" else: self.fulladdress = self.address @@ -113,7 +113,7 @@ class Email: def body(self): if self._body is None: self._parse(self.msg) - self._body = getattr(self, "viewmail_%s" % self.dformat)() + self._body = getattr(self, f"viewmail_{self.dformat}")() return self._body @@ -215,7 +215,7 @@ class Email: # Duplicate Content-ID return - self._images[cid] = "data:%s;base64,%s" % ( + self._images[cid] = "data:{};base64,{}".format( content_type, "".join(msg.get_payload().splitlines(False)), ) @@ -263,7 +263,7 @@ class Email: contents = self.contents["plain"] else: contents = conditional_escape(contents) - return "
%s
" % contents + return f"
{contents}
" def viewmail_html(self, contents=None, **kwargs): # contents and **kwargs parameters to maintain compatibility with @@ -385,7 +385,7 @@ def set_email_headers(msg, subject, sender, rcpt): msg["From"] = sender msg["To"] = prepare_addresses(rcpt) msg["Message-ID"] = make_msgid() - msg["User-Agent"] = "Modoboa %s" % ( + msg["User-Agent"] = "Modoboa {}".format( pkg_resources.get_distribution("modoboa").version ) msg["Date"] = formatdate(time.time(), True) @@ -409,7 +409,7 @@ def _sendmail(sender, rcpt, msgstring, server="localhost", port=25): s.sendmail(sender, [rcpt], msgstring) s.quit() except smtplib.SMTPException as e: - return False, "SMTP error: %s" % str(e) + return False, f"SMTP error: {str(e)}" return True, None @@ -446,15 +446,12 @@ def sendmail_fromfile(sender, rcpt, fname): """ try: fp = open(fname) - except IOError as e: + except OSError as e: return False, str(e) - content = """From: %s -To: %s -""" % ( - sender, - rcpt, - ) + content = f"""From: {sender} +To: {rcpt} +""" content += fp.read() fp.close() diff --git a/modoboa/lib/exceptions.py b/modoboa/lib/exceptions.py index b1c603a44..1ddfb8ecd 100644 --- a/modoboa/lib/exceptions.py +++ b/modoboa/lib/exceptions.py @@ -4,6 +4,8 @@ """ +from typing import Optional + from django.utils.translation import gettext as _ @@ -12,7 +14,7 @@ class ModoboaException(Exception): Base class for Modoboa custom exceptions. """ - http_code = None + http_code: Optional[int] = None def __init__(self, *args, **kwargs): if "http_code" in kwargs: @@ -80,5 +82,5 @@ class PermDeniedException(ModoboaException): def __str__(self): if self.msg: - return _("Permission denied: %s" % self.msg) + return _("Permission denied: {}".format(self.msg)) return _("Permission denied") diff --git a/modoboa/lib/fields.py b/modoboa/lib/fields.py index 65fa08ac0..82e9a82f9 100644 --- a/modoboa/lib/fields.py +++ b/modoboa/lib/fields.py @@ -17,7 +17,7 @@ class DomainNameField(forms.fields.CharField): def clean(self, value): value = self.to_python(value).strip() - return super(DomainNameField, self).clean(value) + return super().clean(value) class UTF8EmailField(forms.fields.EmailField): @@ -41,7 +41,7 @@ class DRFEmailFieldUTF8(serializers.CharField): default_error_messages = {"invalid": gettext_lazy("Enter a valid email address.")} def __init__(self, **kwargs): - super(DRFEmailFieldUTF8, self).__init__(**kwargs) + super().__init__(**kwargs) validator = validators.UTF8EmailValidator( message=self.error_messages["invalid"] ) @@ -54,7 +54,7 @@ class DRFEmailFieldUTF8AndEmptyUser(serializers.CharField): default_error_messages = {"invalid": gettext_lazy("Enter a valid email address.")} def __init__(self, **kwargs): - super(DRFEmailFieldUTF8AndEmptyUser, self).__init__(**kwargs) + super().__init__(**kwargs) validator = validators.UTF8AndEmptyUserEmailValidator( message=self.error_messages["invalid"] ) diff --git a/modoboa/lib/form_utils.py b/modoboa/lib/form_utils.py index 0b6d3e750..9c645bcc6 100644 --- a/modoboa/lib/form_utils.py +++ b/modoboa/lib/form_utils.py @@ -17,7 +17,7 @@ from modoboa.lib.web_utils import render_to_json_response ABC = abc.ABCMeta(force_str("ABC"), (object,), {}) -class WizardStep(object): +class WizardStep: """A wizard step.""" def __init__(self, uid, formclass, title, formtpl=None, new_args=None): @@ -188,7 +188,7 @@ class WizardForm(ABC): """ -class DynamicForm(object): +class DynamicForm: """ A form which accepts dynamic fields. @@ -218,7 +218,7 @@ class DynamicForm(object): :param string pattern: pattern used to find field instances :param typ: a form field class """ - expr = re.compile(r"%s_\d+" % pattern) + expr = re.compile(rf"{pattern}_\d+") values = [] for k, v in list(qdict.items()): if k == pattern or expr.match(k): @@ -234,7 +234,7 @@ class DynamicForm(object): self.data = ndata -class TabForms(object): +class TabForms: """ Simple forms container. @@ -258,7 +258,7 @@ class TabForms(object): args.append(request.POST) if instances is not None: self.instances = instances - mname = "check_%s" % fd["id"] + mname = "check_{}".format(fd["id"]) if hasattr(self, mname): if not getattr(self, mname)(instances[fd["id"]]): to_remove += [fd] @@ -326,8 +326,7 @@ class TabForms(object): return self.forward() def forward(self): - for form in self.forms: - yield form + yield from self.forms def extra_context(self, context): """ "Provide additional information to template's context.""" @@ -387,7 +386,7 @@ class SeparatorField(Field): def __init__(self, *args, **kwargs): kwargs["required"] = False - super(SeparatorField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class YesNoField(TypedChoiceField): @@ -402,4 +401,4 @@ class YesNoField(TypedChoiceField): "coerce": lambda x: x == "True", } ) - super(YesNoField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) diff --git a/modoboa/lib/ldap_utils.py b/modoboa/lib/ldap_utils.py index 8c0219c9f..2c14f432d 100644 --- a/modoboa/lib/ldap_utils.py +++ b/modoboa/lib/ldap_utils.py @@ -42,7 +42,7 @@ from modoboa.lib.exceptions import InternalError from modoboa.parameters import tools as param_tools -class LDAPAuthBackend(object): +class LDAPAuthBackend: """LDAP authentication backend.""" def __init__(self): @@ -112,7 +112,7 @@ class LDAPAuthBackend(object): self.connect_to_server(user, password) user_dn = self._find_user_dn(user) if self.ldap_ad: - newpassword = ('"%s"' % newpassword).encode("utf-16").lstrip("\377\376") + newpassword = (f'"{newpassword}"').encode("utf-16").lstrip("\377\376") ldif = [ ( ldap.MOD_REPLACE, @@ -123,4 +123,4 @@ class LDAPAuthBackend(object): try: self.conn.modify_s(force_str(user_dn), ldif) except ldap.LDAPError as e: - raise InternalError(_("Failed to update password: {}").format(e)) + raise InternalError(_("Failed to update password: {}").format(e)) from None diff --git a/modoboa/lib/migrations/0002_rename_parameters.py b/modoboa/lib/migrations/0002_rename_parameters.py index 1127cd699..68adbbb9f 100644 --- a/modoboa/lib/migrations/0002_rename_parameters.py +++ b/modoboa/lib/migrations/0002_rename_parameters.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations APPLICATIONS = [ ("admin", "modoboa_admin"), diff --git a/modoboa/lib/migrations/0003_rename_parameters.py b/modoboa/lib/migrations/0003_rename_parameters.py index be11772ce..7baa563b2 100644 --- a/modoboa/lib/migrations/0003_rename_parameters.py +++ b/modoboa/lib/migrations/0003_rename_parameters.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations APPS = ( ("modoboa_admin", "admin"), diff --git a/modoboa/lib/migrations/0004_auto_20151114_1409.py b/modoboa/lib/migrations/0004_auto_20151114_1409.py index 2804d63ed..5fa5fb82b 100644 --- a/modoboa/lib/migrations/0004_auto_20151114_1409.py +++ b/modoboa/lib/migrations/0004_auto_20151114_1409.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations def move_language_preference(apps, schema_editor): diff --git a/modoboa/lib/paginator.py b/modoboa/lib/paginator.py index 36f534879..0ebc5ac2e 100644 --- a/modoboa/lib/paginator.py +++ b/modoboa/lib/paginator.py @@ -3,7 +3,7 @@ Custom pagination tools. """ -class Page(object): +class Page: """Class to represent a page.""" def __init__(self, paginator, pageid, id_start, id_stop, baseurl=None): @@ -66,7 +66,7 @@ class Page(object): return lid -class Paginator(object): +class Paginator: """Pagination class.""" def __init__(self, total, elems_per_page): diff --git a/modoboa/lib/singleton.py b/modoboa/lib/singleton.py index e49acc309..c1447ced6 100644 --- a/modoboa/lib/singleton.py +++ b/modoboa/lib/singleton.py @@ -4,7 +4,7 @@ class _Singleton(type): def __call__(self, *args, **kwargs): if self not in self._instances: - self._instances[self] = super(_Singleton, self).__call__(*args, **kwargs) + self._instances[self] = super().__call__(*args, **kwargs) return self._instances[self] diff --git a/modoboa/lib/sysutils.py b/modoboa/lib/sysutils.py index ded7c0a55..f97c0a0f8 100644 --- a/modoboa/lib/sysutils.py +++ b/modoboa/lib/sysutils.py @@ -27,7 +27,7 @@ def exec_cmd(cmd, sudo_user=None, pinput=None, capture_output=True, **kwargs): :return: return code, command output """ if sudo_user is not None: - cmd = "sudo -u %s %s" % (sudo_user, cmd) + cmd = f"sudo -u {sudo_user} {cmd}" kwargs["shell"] = True if pinput is not None: kwargs["stdin"] = subprocess.PIPE @@ -75,7 +75,7 @@ def doveadm_cmd(params, pinput=None, capture_output=True, **kwargs): sudo_user = dovecot_user if curuser != dovecot_user else None if dpath: return exec_cmd( - "{} {}".format(dpath, params), + f"{dpath} {params}", sudo_user=sudo_user, pinput=pinput, capture_output=capture_output, diff --git a/modoboa/lib/templatetags/form_tags.py b/modoboa/lib/templatetags/form_tags.py index 4bd43ceb7..69f37155f 100644 --- a/modoboa/lib/templatetags/form_tags.py +++ b/modoboa/lib/templatetags/form_tags.py @@ -20,7 +20,7 @@ def render_form(form, tpl=None): ret = "" for field in form: - ret += "%s\n" % render_field(field) + ret += f"{render_field(field)}\n" return mark_safe(ret) @@ -42,7 +42,7 @@ def render_field(field, help_display_mode="tooltip", label_width="col-sm-4", **o from modoboa.core.templatetags.core_tags import visirule if isinstance(field.field, SeparatorField): - return "%s" % (visirule(field), smart_str(field.label)) + return f"{smart_str(field.label)}" configure_field_classes(field) context = { "field": field, @@ -64,7 +64,7 @@ def render_fields_group(form, pattern): cpt = 1 haserror = len(first.errors) != 0 while True: - fname = "%s_%d" % (pattern, cpt) + fname = f"{pattern}_{cpt}" if fname not in form.fields: break bfield = forms.BoundField(form, form.fields[fname], fname) @@ -94,4 +94,4 @@ def render_field_width(field): width = form.field_widths[field.name] else: width = 5 - return "col-sm-{0}".format(width) + return f"col-sm-{width}" diff --git a/modoboa/lib/templatetags/lib_tags.py b/modoboa/lib/templatetags/lib_tags.py index 64e931f15..3214e457e 100644 --- a/modoboa/lib/templatetags/lib_tags.py +++ b/modoboa/lib/templatetags/lib_tags.py @@ -18,13 +18,13 @@ def join(items, sep=","): for k, v in list(items.items()): if res != "": res += sep - res += "%s : '%s'" % (k, v) + res += f"{k} : '{v}'" return res @register.simple_tag def tolist(values): - return mark_safe("[%s]" % ",".join(['"%s"' % v for v in values])) + return mark_safe("[{}]".format(",".join([f'"{v}"' for v in values]))) @register.simple_tag diff --git a/modoboa/lib/test_utils.py b/modoboa/lib/test_utils.py index 60fc012fd..804d2c7e9 100644 --- a/modoboa/lib/test_utils.py +++ b/modoboa/lib/test_utils.py @@ -7,7 +7,7 @@ import tempfile from django.core.management import call_command -class MapFilesTestCaseMixin(object): +class MapFilesTestCaseMixin: """A generic test case to check map files generation.""" MAP_FILES = None @@ -21,13 +21,13 @@ class MapFilesTestCaseMixin(object): shutil.rmtree(self.workdir) def _test_maps_generation(self, engine): - dburl = "{0}://user:password@localhost/testdb".format(engine) + dburl = f"{engine}://user:password@localhost/testdb" call_command( "generate_postfix_maps", "--dburl", dburl, "--destdir", self.workdir ) for mapfile in self.MAP_FILES: - path = "{0}/{1}".format(self.workdir, mapfile) + path = f"{self.workdir}/{mapfile}" self.assertTrue(os.path.exists(path)) with open(path) as fpo: content = fpo.read() diff --git a/modoboa/lib/tests/__init__.py b/modoboa/lib/tests/__init__.py index 23425ef51..ca3cb975b 100644 --- a/modoboa/lib/tests/__init__.py +++ b/modoboa/lib/tests/__init__.py @@ -19,7 +19,7 @@ try: s = socket.create_connection(("127.0.0.1", 25)) s.close() NO_SMTP = False -except socket.error: +except OSError: NO_SMTP = True try: @@ -36,7 +36,7 @@ class ParametersMixin: @classmethod def setUpTestData(cls): # noqa """Set LocalConfig instance.""" - super(ParametersMixin, cls).setUpTestData() + super().setUpTestData() cls.localconfig = core_models.LocalConfig.objects.first() def set_global_parameter(self, name, value, app=None): @@ -64,7 +64,7 @@ class ModoTestCase(ParametersMixin, TestCase): @classmethod def setUpTestData(cls): # noqa """Create a default user.""" - super(ModoTestCase, cls).setUpTestData() + super().setUpTestData() management.call_command("load_initial_data", "--no-frontend") def setUp(self, username="admin", password="password"): @@ -101,7 +101,7 @@ class ModoAPITestCase(ParametersMixin, APITestCase): @classmethod def setUpTestData(cls): # noqa """Create a default user.""" - super(ModoAPITestCase, cls).setUpTestData() + super().setUpTestData() management.call_command("load_initial_data") cls.token = Token.objects.create( user=core_models.User.objects.get(username="admin") @@ -109,7 +109,7 @@ class ModoAPITestCase(ParametersMixin, APITestCase): def setUp(self): """Setup.""" - super(ModoAPITestCase, self).setUp() + super().setUp() self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) self.workdir = tempfile.mkdtemp() self.set_global_parameter("storage_dir", self.workdir, app="pdfcredentials") diff --git a/modoboa/lib/tests/test_email_utils.py b/modoboa/lib/tests/test_email_utils.py index 1c6844ccc..a3202462d 100644 --- a/modoboa/lib/tests/test_email_utils.py +++ b/modoboa/lib/tests/test_email_utils.py @@ -15,8 +15,8 @@ SAMPLES_DIR = os.path.realpath( class EmailTestImplementation(Email): def _fetch_message(self): - message_path = os.path.join(SAMPLES_DIR, "%s-input.txt" % self.mailid) - assert os.path.isfile(message_path), "%s does not exist." % message_path + message_path = os.path.join(SAMPLES_DIR, f"{self.mailid}-input.txt") + assert os.path.isfile(message_path), f"{message_path} does not exist." with open(message_path, "rb") as fp: mail_text = smart_bytes(fp.read()) @@ -38,7 +38,7 @@ class EmailTests(SimpleTestCase): ext = kwargs["dformat"] if "dformat" in kwargs else "plain" ext += "_links" if "links" in kwargs and kwargs["links"] else "_nolinks" message_path = os.path.join(SAMPLES_DIR, f"{message_id}-output-{ext}.txt") - assert os.path.isfile(message_path), "%s does not exist." % message_path + assert os.path.isfile(message_path), f"{message_path} does not exist." with open(message_path, "rb") as fp: # output should always be unicode (py2) or str (py3) diff --git a/modoboa/lib/validators.py b/modoboa/lib/validators.py index e54c48ca5..67b868d9d 100644 --- a/modoboa/lib/validators.py +++ b/modoboa/lib/validators.py @@ -7,7 +7,7 @@ from django.core.validators import EmailValidator, URLValidator from django.utils.translation import gettext_lazy -class HostnameValidator(object): +class HostnameValidator: """Validator for fqdn.""" message = gettext_lazy("Enter a valid domain name") diff --git a/modoboa/lib/web_utils.py b/modoboa/lib/web_utils.py index ae6333e79..1f9d4cf1d 100644 --- a/modoboa/lib/web_utils.py +++ b/modoboa/lib/web_utils.py @@ -16,7 +16,7 @@ from django.template.loader import render_to_string def _render_error(request, errortpl="error", user_context=None): if user_context is None: user_context = {} - return render(request, "common/%s.html" % errortpl, user_context) + return render(request, f"common/{errortpl}.html", user_context) def render_actions(actions): @@ -102,7 +102,7 @@ def static_url(path): """ if path.startswith("/"): path = path[1:] - return "%s%s" % (settings.STATIC_URL, path) + return f"{settings.STATIC_URL}{path}" def size2integer(value, output_unit="B"): @@ -138,11 +138,11 @@ def size2integer(value, output_unit="B"): if m.group(2)[0] in ["G", "g"]: return int(m.group(1)) * 2**10 else: - raise ValueError("Unsupported output unit {}".format(output_unit)) + raise ValueError(f"Unsupported output unit {output_unit}") return 0 -class NavigationParameters(object): +class NavigationParameters: """ Just a simple object to manipulate navigation parameters. """ diff --git a/modoboa/limits/api/v1/tests.py b/modoboa/limits/api/v1/tests.py index 8b6c0bcfe..9c0705fdb 100644 --- a/modoboa/limits/api/v1/tests.py +++ b/modoboa/limits/api/v1/tests.py @@ -20,9 +20,9 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(APIAdminLimitsTestCase, cls).setUpTestData() + super().setUpTestData() for name, _definition in utils.get_user_limit_templates(): - cls.localconfig.parameters.set_value("deflt_user_{0}_limit".format(name), 2) + cls.localconfig.parameters.set_value(f"deflt_user_{name}_limit", 2) cls.localconfig.save() populate_database() cls.user = User.objects.get(username="admin@test.com") @@ -189,12 +189,10 @@ class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(APIDomainLimitsTestCase, cls).setUpTestData() + super().setUpTestData() cls.localconfig.parameters.set_value("enable_domain_limits", True) for name, _definition in utils.get_domain_limit_templates(): - cls.localconfig.parameters.set_value( - "deflt_domain_{0}_limit".format(name), 2 - ) + cls.localconfig.parameters.set_value(f"deflt_domain_{name}_limit", 2) cls.localconfig.save() populate_database() @@ -253,9 +251,9 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(ResourcesAPITestCase, cls).setUpTestData() + super().setUpTestData() for name, _definition in utils.get_user_limit_templates(): - cls.localconfig.parameters.set_value("deflt_user_{0}_limit".format(name), 2) + cls.localconfig.parameters.set_value(f"deflt_user_{name}_limit", 2) cls.localconfig.save() populate_database() cls.user = User.objects.get(username="admin@test.com") @@ -282,12 +280,12 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): compare(expected, response.data) # As reseller => fails - self.client.credentials(HTTP_AUTHORIZATION="Token {}".format(self.r_token.key)) + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.r_token.key}") response = self.client.get(url) self.assertEqual(response.status_code, 404) # As domain admin => fails - self.client.credentials(HTTP_AUTHORIZATION="Token {}".format(self.da_token.key)) + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.da_token.key}") response = self.client.get(url) self.assertEqual(response.status_code, 404) @@ -304,7 +302,7 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): ) # As domain admin => fails - self.client.credentials(HTTP_AUTHORIZATION="Token {}".format(self.da_token.key)) + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.da_token.key}") resources.update({"domains": 2, "mailboxes": 2}) url = reverse("v1:resources-detail", args=[self.user.pk]) response = self.client.put(url, resources) @@ -312,7 +310,7 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): # As reseller => ok permissions.grant_access_to_object(self.reseller, self.user, True) - self.client.credentials(HTTP_AUTHORIZATION="Token {}".format(self.r_token.key)) + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.r_token.key}") resources.update({"domains": 500, "mailboxes": 500}) url = reverse("v1:resources-detail", args=[self.user.pk]) response = self.client.put(url, resources) diff --git a/modoboa/limits/forms.py b/modoboa/limits/forms.py index 6c226b842..433aa8513 100644 --- a/modoboa/limits/forms.py +++ b/modoboa/limits/forms.py @@ -13,7 +13,7 @@ class ResourcePoolForm(forms.Form): def __init__(self, *args, **kwargs): self.account = kwargs.pop("instance", None) - super(ResourcePoolForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) for name, tpl in utils.get_user_limit_templates(): if "required_role" in tpl: condition = ( @@ -22,7 +22,7 @@ class ResourcePoolForm(forms.Form): ) if condition: continue - self.fields["{}_limit".format(name)] = forms.IntegerField( + self.fields[f"{name}_limit"] = forms.IntegerField( label=tpl["label"], help_text=tpl["help"] ) if self.account: @@ -30,7 +30,7 @@ class ResourcePoolForm(forms.Form): def clean(self): """Ensure limit values are correct.""" - cleaned_data = super(ResourcePoolForm, self).clean() + cleaned_data = super().clean() if self.errors: return cleaned_data for lname in list(self.fields.keys()): @@ -49,7 +49,7 @@ class ResourcePoolForm(forms.Form): def save(self): owner = get_object_owner(self.account) for name, _definition in utils.get_user_limit_templates(): - fieldname = "{}_limit".format(name) + fieldname = f"{name}_limit" if fieldname not in self.cleaned_data: continue limit = self.account.userobjectlimit_set.get(name=name) @@ -65,9 +65,9 @@ class DomainLimitsForm(forms.Form): def __init__(self, *args, **kwargs): """Define limits as fields.""" self.domain = kwargs.pop("instance", None) - super(DomainLimitsForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) for name, tpl in utils.get_domain_limit_templates(): - self.fields["{}_limit".format(name)] = forms.IntegerField( + self.fields[f"{name}_limit"] = forms.IntegerField( label=tpl["label"], help_text=tpl["help"] ) if not self.domain: @@ -80,7 +80,7 @@ class DomainLimitsForm(forms.Form): def clean(self): """Ensure limit values are correct.""" - cleaned_data = super(DomainLimitsForm, self).clean() + cleaned_data = super().clean() if self.errors: return cleaned_data for lname in list(self.fields.keys()): @@ -91,7 +91,7 @@ class DomainLimitsForm(forms.Form): def save(self, user, **kwargs): """Set limits.""" for name, _definition in utils.get_domain_limit_templates(): - fieldname = "{}_limit".format(name) + fieldname = f"{name}_limit" if fieldname not in self.cleaned_data: continue limit = self.domain.domainobjectlimit_set.get(name=name) diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index 6b3bc325c..8d44367dd 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -51,7 +51,7 @@ def create_user_limits(sender, instance, **kwargs): max_value = 0 # creator can be None if user was created by a factory if not creator or creator.is_superuser: - max_value = global_params["deflt_user_{0}_limit".format(name)] + max_value = global_params[f"deflt_user_{name}_limit"] models.UserObjectLimit.objects.create( user=instance, name=name, content_type=ct, max_value=max_value ) @@ -64,7 +64,7 @@ def create_domain_limits(sender, instance, **kwargs): return global_params = dict(param_tools.get_global_parameters("limits")) for name, _definition in utils.get_domain_limit_templates(): - max_value = global_params["deflt_domain_{0}_limit".format(name)] + max_value = global_params[f"deflt_domain_{name}_limit"] models.DomainObjectLimit.objects.create( domain=instance, name=name, max_value=max_value ) diff --git a/modoboa/limits/migrations/0002_auto_20151114_1518.py b/modoboa/limits/migrations/0002_auto_20151114_1518.py index f54808265..5bff06849 100644 --- a/modoboa/limits/migrations/0002_auto_20151114_1518.py +++ b/modoboa/limits/migrations/0002_auto_20151114_1518.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/modoboa/limits/migrations/0004_auto_20160413_1312.py b/modoboa/limits/migrations/0004_auto_20160413_1312.py index cc5468074..00253451d 100644 --- a/modoboa/limits/migrations/0004_auto_20160413_1312.py +++ b/modoboa/limits/migrations/0004_auto_20160413_1312.py @@ -1,4 +1,4 @@ -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): diff --git a/modoboa/limits/models.py b/modoboa/limits/models.py index e0f59b227..d535e5bcd 100644 --- a/modoboa/limits/models.py +++ b/modoboa/limits/models.py @@ -9,7 +9,7 @@ from modoboa.core import models as core_models from . import lib, utils -class ObjectLimitMixin(object): +class ObjectLimitMixin: """Common methods.""" @property @@ -54,7 +54,7 @@ class ObjectLimitMixin(object): """Display current usage.""" if self.max_value == -1: return _("unlimited") - return "{}%".format(self.usage) + return f"{self.usage}%" class UserObjectLimit(ObjectLimitMixin, models.Model): @@ -67,7 +67,7 @@ class UserObjectLimit(ObjectLimitMixin, models.Model): ) max_value = models.IntegerField(default=0) - class Meta(object): + class Meta: unique_together = (("user", "name"),) @property @@ -117,7 +117,7 @@ class UserObjectLimit(ObjectLimitMixin, models.Model): """Display current usage.""" if self.max_value == -1: return _("unlimited") - return "{}%".format(self.usage) + return f"{self.usage}%" class DomainObjectLimit(ObjectLimitMixin, models.Model): @@ -127,7 +127,7 @@ class DomainObjectLimit(ObjectLimitMixin, models.Model): name = models.CharField(max_length=254) max_value = models.IntegerField(default=0) - class Meta(object): + class Meta: unique_together = (("domain", "name"),) @property @@ -143,7 +143,7 @@ class DomainObjectLimit(ObjectLimitMixin, models.Model): """Return the current number of objects.""" definition = self.definition if not definition: - raise RuntimeError("Bad limit {}".format(self.name)) + raise RuntimeError(f"Bad limit {self.name}") relation = getattr(self.domain, definition["relation"]) if "extra_filters" in definition: relation = relation.filter(**definition["extra_filters"]) diff --git a/modoboa/limits/tests/test_domain_limits.py b/modoboa/limits/tests/test_domain_limits.py index 2ec872fd2..2502bf47d 100644 --- a/modoboa/limits/tests/test_domain_limits.py +++ b/modoboa/limits/tests/test_domain_limits.py @@ -16,14 +16,12 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(DomainLimitsTestCase, cls).setUpTestData() + super().setUpTestData() cls.localconfig.parameters.set_values( {"enable_admin_limits": False, "enable_domain_limits": True} ) for name, _definition in utils.get_domain_limit_templates(): - cls.localconfig.parameters.set_value( - "deflt_domain_{0}_limit".format(name), 2 - ) + cls.localconfig.parameters.set_value(f"deflt_domain_{name}_limit", 2) cls.localconfig.save() admin_factories.populate_database() diff --git a/modoboa/limits/tests/test_import.py b/modoboa/limits/tests/test_import.py index 08b4a4edd..049949877 100644 --- a/modoboa/limits/tests/test_import.py +++ b/modoboa/limits/tests/test_import.py @@ -14,7 +14,7 @@ class LimitImportTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(LimitImportTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() cls.reseller = core_factories.UserFactory( username="reseller", groups=("Resellers",) @@ -27,18 +27,16 @@ class LimitImportTestCase(ModoTestCase): self.client.force_login(self.reseller) self.assertFalse(limit.is_exceeded()) f = ContentFile( - """domainalias; domalias1.com; {domain}; True -domainalias; domalias2.com; {domain}; True -""".format( - domain=self.domain - ), + f"""domainalias; domalias1.com; {self.domain}; True +domainalias; domalias2.com; {self.domain}; True +""", name="domains.csv", ) self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) self.assertTrue(limit.is_exceeded()) f = ContentFile( - "domainalias; domalias3.com; {}; True".format(self.domain), + f"domainalias; domalias3.com; {self.domain}; True", name="domains.csv", ) response = self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) @@ -51,20 +49,16 @@ domainalias; domalias2.com; {domain}; True content = "" for cpt in range(initial_count, 2): content += ( - "account; admin{cpt}@{domain}; toto; User; One; True; " - "DomainAdmins; user{cpt}@{domain}; 5; {domain}\n".format( - domain=self.domain, cpt=cpt - ) + f"account; admin{cpt}@{self.domain}; toto; User; One; True; " + f"DomainAdmins; user{cpt}@{self.domain}; 5; {self.domain}\n" ) f = ContentFile(content, name="domain_admins.csv") response = self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) self.assertTrue(limit.is_exceeded()) f = ContentFile( - """account; admin3@{domain}; toto; User; One; True; DomainAdmins; admin3@{domain}; 5; {domain} -""".format( - domain=self.domain - ), + f"""account; admin3@{self.domain}; toto; User; One; True; DomainAdmins; admin3@{self.domain}; 5; {self.domain} +""", name="domain_admins.csv", ) # NOQA:E501 response = self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) @@ -75,21 +69,19 @@ domainalias; domalias2.com; {domain}; True self.client.force_login(self.dadmin) self.assertFalse(limit.is_exceeded()) f = ContentFile( - "account; user1@{domain}; toto; User; One; True; SimpleUsers; " - "user1@{domain}; 5\r\n" - "account; truc@{domain}; toto; René; Truc; True; SimpleUsers; " - "truc@{domain}; 5\r\n".format(domain=self.domain), + f"account; user1@{self.domain}; toto; User; One; True; SimpleUsers; " + f"user1@{self.domain}; 5\r\n" + f"account; truc@{self.domain}; toto; René; Truc; True; SimpleUsers; " + f"truc@{self.domain}; 5\r\n", name="domains.csv", ) response = self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) self.assertTrue(limit.is_exceeded()) f = ContentFile( - """ -account; user3@{domain}; toto; User; One; True; SimpleUsers; user3@{domain}; 5 -""".format( - domain=self.domain - ), + f""" +account; user3@{self.domain}; toto; User; One; True; SimpleUsers; user3@{self.domain}; 5 +""", name="domains.csv", ) response = self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) @@ -100,22 +92,18 @@ account; user3@{domain}; toto; User; One; True; SimpleUsers; user3@{domain}; 5 self.client.force_login(self.dadmin) self.assertFalse(limit.is_exceeded()) f = ContentFile( - """alias; alias1@{domain}; True; user@{domain} -alias; alias2@{domain}; True; user@{domain} -""".format( - domain=self.domain - ), + f"""alias; alias1@{self.domain}; True; user@{self.domain} +alias; alias2@{self.domain}; True; user@{self.domain} +""", name="aliases.csv", ) self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) self.assertTrue(limit.is_exceeded()) f = ContentFile( - """ -alias; alias3@{domain}; True; user@{domain} -""".format( - domain=self.domain - ), + f""" +alias; alias3@{self.domain}; True; user@{self.domain} +""", name="aliases.csv", ) with self.assertRaises(exceptions.ValidationError): @@ -129,9 +117,9 @@ class UserLimitImportTestCase(LimitImportTestCase): """Create test data.""" localconfig = core_models.LocalConfig.objects.first() for name, _definition in utils.get_user_limit_templates(): - localconfig.parameters.set_value("deflt_user_{0}_limit".format(name), 2) + localconfig.parameters.set_value(f"deflt_user_{name}_limit", 2) localconfig.save() - super(UserLimitImportTestCase, cls).setUpTestData() + super().setUpTestData() def test_domains_import(self): """Check domains limit.""" @@ -191,9 +179,9 @@ class DomainLimitImportTestCase(LimitImportTestCase): {"enable_domain_limits": True, "enable_admin_limits": False} ) for name, _definition in utils.get_domain_limit_templates(): - localconfig.parameters.set_value("deflt_domain_{0}_limit".format(name), 2) + localconfig.parameters.set_value(f"deflt_domain_{name}_limit", 2) localconfig.save() - super(DomainLimitImportTestCase, cls).setUpTestData() + super().setUpTestData() mb = admin_factories.MailboxFactory( user__username="user@test4.com", domain__name="test4.com", address="user" ) diff --git a/modoboa/limits/tests/test_user_limits.py b/modoboa/limits/tests/test_user_limits.py index b5fa2ae00..d0d39fe17 100644 --- a/modoboa/limits/tests/test_user_limits.py +++ b/modoboa/limits/tests/test_user_limits.py @@ -15,7 +15,7 @@ class PermissionsTestCase(lib_tests.ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(PermissionsTestCase, cls).setUpTestData() + super().setUpTestData() populate_database() def test_domainadmin_deletes_reseller(self): @@ -49,12 +49,12 @@ class ResourceTestCase(lib_tests.ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Custom setUpTestData method.""" - super(ResourceTestCase, cls).setUpTestData() + super().setUpTestData() cls.localconfig.parameters.set_values( {"enable_admin_limits": True, "enable_domain_limits": False} ) for name, _definition in utils.get_user_limit_templates(): - cls.localconfig.parameters.set_value("deflt_user_{0}_limit".format(name), 2) + cls.localconfig.parameters.set_value(f"deflt_user_{name}_limit", 2) cls.localconfig.save() populate_database() @@ -116,7 +116,7 @@ class ResourceTestCase(lib_tests.ModoTestCase): else: aliases.remove(name) for cpt, alias in enumerate(aliases): - fname = "aliases" if not cpt else "aliases_%d" % cpt + fname = "aliases" if not cpt else f"aliases_{cpt}" values[fname] = alias self.ajax_post(reverse("admin:domain_change", args=[dom.id]), values, status) @@ -131,7 +131,7 @@ class DomainAdminTestCase(ResourceTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(DomainAdminTestCase, cls).setUpTestData() + super().setUpTestData() cls.user = User.objects.get(username="admin@test.com") cls.user.userobjectlimit_set.filter( name__in=["mailboxes", "mailbox_aliases"] @@ -139,7 +139,7 @@ class DomainAdminTestCase(ResourceTestCase): def setUp(self): """Test initialization.""" - super(DomainAdminTestCase, self).setUp() + super().setUp() self.client.force_login(self.user) def test_mailboxes_limit(self): @@ -173,7 +173,7 @@ class DomainAdminTestCase(ResourceTestCase): self._check_limit("mailbox_aliases", 3, -1) self.ajax_delete( reverse("admin:alias_delete") - + "?selection=%d" % Alias.objects.get(address="alias2@test.com").id + + f"?selection={Alias.objects.get(address='alias2@test.com').id}" ) self._check_limit("mailbox_aliases", 2, -1) @@ -201,14 +201,14 @@ class ResellerTestCase(ResourceTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(ResellerTestCase, cls).setUpTestData() + super().setUpTestData() cls.localconfig.parameters.set_value("deflt_user_quota_limit", 1000) cls.localconfig.save() cls.user = UserFactory(username="reseller", groups=("Resellers",)) def setUp(self): """Test initialization.""" - super(ResellerTestCase, self).setUp() + super().setUp() self.client.force_login(self.user) def test_domains_limit(self): @@ -288,8 +288,8 @@ class ResellerTestCase(ResourceTestCase): self._create_domain("domain.tld") self._create_account("admin1@domain.tld", role="DomainAdmins") domadmin = User.objects.get(username="admin1@domain.tld") - for l in ["mailboxes", "mailbox_aliases"]: - self.assertEqual(domadmin.userobjectlimit_set.get(name=l).max_value, 0) + for name in ["mailboxes", "mailbox_aliases"]: + self.assertEqual(domadmin.userobjectlimit_set.get(name=name).max_value, 0) def test_domain_admins_limit_from_domain_tpl(self): self.user.userobjectlimit_set.filter(name="domains").update(max_value=3) @@ -380,7 +380,7 @@ class ResellerTestCase(ResourceTestCase): self.client.logout() self.client.login(username="admin", password="password") self.ajax_delete( - "{0}?domid={1}&daid={2}".format( + "{}?domid={}&daid={}".format( reverse("admin:permission_remove"), dom.id, self.user.id ), {}, diff --git a/modoboa/maillog/api/v2/viewsets.py b/modoboa/maillog/api/v2/viewsets.py index 58a60546d..34b51999d 100644 --- a/modoboa/maillog/api/v2/viewsets.py +++ b/modoboa/maillog/api/v2/viewsets.py @@ -41,7 +41,7 @@ class StatisticsViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): end = int(time.mktime(serializer.validated_data["end"].timetuple())) else: end = int(time.mktime(time.localtime())) - start = "-1{}".format(period) + start = f"-1{period}" graphs = graph_sets[gset].export( fname, start, end, serializer.validated_data.get("graphic") ) diff --git a/modoboa/maillog/apps.py b/modoboa/maillog/apps.py index c264e4e2a..92d8d4787 100644 --- a/modoboa/maillog/apps.py +++ b/modoboa/maillog/apps.py @@ -73,4 +73,4 @@ class MaillogConfig(AppConfig): def ready(self): load_maillog_settings() - from . import handlers + from . import handlers # noqa diff --git a/modoboa/maillog/factories.py b/modoboa/maillog/factories.py index 011740199..4b567b64f 100644 --- a/modoboa/maillog/factories.py +++ b/modoboa/maillog/factories.py @@ -15,10 +15,10 @@ class MaillogFactory(factory.django.DjangoModelFactory): class Meta: model = models.Maillog - queue_id = factory.Sequence(lambda n: "ID{}".format(n)) + queue_id = factory.Sequence(lambda n: f"ID{n}") date = timezone.now() - sender = factory.Sequence(lambda n: "sender{}@example.test".format(n)) - rcpt = factory.Sequence(lambda n: "rcpt{}@example2.test".format(n)) + sender = factory.Sequence(lambda n: f"sender{n}@example.test") + rcpt = factory.Sequence(lambda n: f"rcpt{n}@example2.test") size = 100 status = "sent" from_domain = factory.SubFactory(admin_factories.DomainFactory) diff --git a/modoboa/maillog/graphics.py b/modoboa/maillog/graphics.py index 8b438153d..1a632e90a 100644 --- a/modoboa/maillog/graphics.py +++ b/modoboa/maillog/graphics.py @@ -36,12 +36,12 @@ class Curve: :return: a list """ rrdfile = os.path.join( - param_tools.get_global_parameter("rrd_rootdir"), "%s.rrd" % rrdfile + param_tools.get_global_parameter("rrd_rootdir"), f"{rrdfile}.rrd" ) return [ - "DEF:%s=%s:%s:%s" % (self.dsname, rrdfile, self.dsname, self.cfunc), - "CDEF:%(ds)spm=%(ds)s,UN,0,%(ds)s,IF,60,*" % {"ds": self.dsname}, - 'XPORT:%spm:"%s"' % (self.dsname, self.legend), + f"DEF:{self.dsname}={rrdfile}:{self.dsname}:{self.cfunc}", + f"CDEF:{self.dsname}pm={self.dsname},UN,0,{self.dsname},IF,60,*", + f'XPORT:{self.dsname}pm:"{self.legend}"', ] @@ -52,7 +52,7 @@ class Graphic: """Constructor.""" self._curves = [] try: - order = getattr(self, "order") + order = self.order except AttributeError: for member in inspect.getmembers(self): if isinstance(member[1], Curve): @@ -102,9 +102,7 @@ class Graphic: cmdargs += curve.to_rrd_command_args(rrdfile) code = 0 - cmd = "{} xport --json -t --start {} --end {} ".format( - self.rrdtool_binary, str(start), str(end) - ) + cmd = f"{self.rrdtool_binary} xport --json -t --start {str(start)} --end {str(end)} " cmd += " ".join(cmdargs) code, output = exec_cmd(smart_bytes(cmd)) if code: @@ -122,7 +120,7 @@ class Graphic: return result -class GraphicSet(object): +class GraphicSet: """A set of graphics.""" domain_selector = False diff --git a/modoboa/maillog/handlers.py b/modoboa/maillog/handlers.py index 57286d2af..0deb2e4ff 100644 --- a/modoboa/maillog/handlers.py +++ b/modoboa/maillog/handlers.py @@ -4,7 +4,6 @@ from django.urls import reverse from django.dispatch import receiver from django.utils.translation import gettext as _ -from modoboa.admin import signals as admin_signals from modoboa.core import signals as core_signals from modoboa.parameters import tools as param_tools diff --git a/modoboa/maillog/lib.py b/modoboa/maillog/lib.py index fa5e04184..e91feee40 100644 --- a/modoboa/maillog/lib.py +++ b/modoboa/maillog/lib.py @@ -1,4 +1,3 @@ -# coding: utf-8 import sys import time @@ -12,12 +11,12 @@ def date_to_timestamp(timetuple): :return: an integer """ date = " ".join( - [("%d" % elem) if isinstance(elem, int) else elem for elem in timetuple] + [str(elem) if isinstance(elem, int) else elem for elem in timetuple] ) fmt = "%Y %m %d %H %M %S" if timetuple[1].isdigit() else "%Y %b %d %H %M %S" try: local = time.strptime(date, fmt) except ValueError: - print >> sys.stderr, "Error: failed to convert date and time" + print("Error: failed to convert date and time", file=sys.stderr) return 0 return int(time.mktime(local)) diff --git a/modoboa/maillog/management/commands/logparser.py b/modoboa/maillog/management/commands/logparser.py index 93cc0e98a..ba5cf492b 100644 --- a/modoboa/maillog/management/commands/logparser.py +++ b/modoboa/maillog/management/commands/logparser.py @@ -1,5 +1,3 @@ -# coding: utf-8 - """ Postfix log parser. @@ -19,7 +17,6 @@ Predefined events are: """ from datetime import datetime -import io import os import re import sys @@ -207,12 +204,12 @@ class LogParser: # Set up data sources for our RRD params = [] for v in variables: - params += ["DS:%s:%s:%s:0:U" % (v, ds_type, rrdstep * 2)] + params += [f"DS:{v}:{ds_type}:{rrdstep * 2}:0:U"] # Set up RRD to archive data for cf in ["AVERAGE", "MAX"]: for step in [day_steps, week_steps, month_steps, year_steps]: - params += ["RRA:%s:0.5:%s:%s" % (cf, step, realrows)] + params += [f"RRA:{cf}:0.5:{step}:{realrows}"] # With those setup, we can now created the RRD rrdtool.create(str(fname), "--start", str(m), "--step", str(rrdstep), *params) @@ -224,16 +221,16 @@ class LogParser: Add missing Data Sources (DS) to existing Round Robin Archive (RRA): See init_rrd for details. """ - ds_def = "DS:%s:ABSOLUTE:%s:0:U" % (dsname, rrdstep * 2) + ds_def = f"DS:{dsname}:ABSOLUTE:{rrdstep * 2}:0:U" rrdtool.tune(fname, ds_def) - self._dprint("[rrd] added DS %s to %s" % (dsname, fname)) + self._dprint(f"[rrd] added DS {dsname} to {fname}") def add_point_to_rrd(self, fname, tpl, values, ts=None): """Try to add a new point to RRD file.""" if ts: - values = "{}:{}".format(ts, values) + values = f"{ts}:{values}" if self.verbose: - print("[rrd] VERBOSE update -t %s %s" % (tpl, values)) + print(f"[rrd] VERBOSE update -t {tpl} {values}") try: rrdtool.update(str(fname), "-t", tpl, values) except rrdtool.OperationalError as e: @@ -252,20 +249,20 @@ class LogParser: False : syslog may have probably been already recorded or something wrong """ - fname = "%s/%s.rrd" % (self.workdir, dom) + fname = f"{self.workdir}/{dom}.rrd" m = t - (t % rrdstep) - self._dprint("[rrd] updating %s" % fname) + self._dprint(f"[rrd] updating {fname}") if not os.path.exists(fname): self.lupdates[fname] = self.init_rrd(fname, m) - self._dprint("[rrd] create new RRD file %s" % fname) + self._dprint(f"[rrd] create new RRD file {fname}") else: if fname not in self.lupdates: self.lupdates[fname] = rrdtool.last(str(fname)) if m <= self.lupdates[fname]: if self.verbose: - print("[rrd] VERBOSE events at %s already recorded in RRD" % m) + print(f"[rrd] VERBOSE events at {m} already recorded in RRD") return False tpl = "" @@ -277,14 +274,14 @@ class LogParser: # Est ce vraiment nécessaire... ? if m > self.lupdates[fname] + rrdstep: values = "" - for v in variables: + for _v in variables: if values != "": values += ":" values += "0" for p in range(self.lupdates[fname] + rrdstep, m, rrdstep): self.add_point_to_rrd(fname, tpl, values, ts=p) - values = "%s" % m + values = f"{m}" tpl = "" for v in variables: values += ":" @@ -359,7 +356,7 @@ class LogParser: m = self._srs_regex["reverse_srs1"].match(mail_address) if m is None else m if m is not None: - return "%s@%s" % m.group(2, 1) + return "{}@{}".format(*m.group(2, 1)) else: return mail_address @@ -477,12 +474,10 @@ class LogParser: return False (msg_to, msg_status) = m.groups() if queue_id not in self.workdict: - self._dprint( - "[parser] inconsistent mail (%s: %s), skipping" % (queue_id, msg_to) - ) + self._dprint(f"[parser] inconsistent mail ({queue_id}: {msg_to}), skipping") return True if msg_status not in variables: - self._dprint("[parser] unsupported status %s, skipping" % msg_status) + self._dprint(f"[parser] unsupported status {msg_status}, skipping") return True # orig_to is optional. @@ -565,11 +560,11 @@ class LogParser: host, prog, subprog, pid, log = m.groups() try: - parser = getattr(self, "_parse_{}".format(prog)) + parser = getattr(self, f"_parse_{prog}") if not parser(log, host, pid, subprog): - self._dprint("[parser] ignoring %r log: %r" % (prog, log)) + self._dprint(f"[parser] ignoring {prog!r} log: {log!r}") except AttributeError: - self._dprint('[parser] no log handler for "{}": {}'.format(prog, log)) + self._dprint(f'[parser] no log handler for "{prog}": {log}') def process(self): """Process the log file. @@ -581,12 +576,12 @@ class LogParser: with open(self.logfile, encoding="utf-8", errors="ignore") as fp: for line in fp: self._parse_line(line) - except IOError as errno: - self._dprint("%s" % errno) + except OSError as errno: + self._dprint(f"{errno}") sys.exit(1) for dom, data in self.data.items(): - self._dprint("[rrd] dealing with domain %s" % dom) + self._dprint(f"[rrd] dealing with domain {dom}") for t in sorted(data.keys()): self.update_rrd(dom, t) diff --git a/modoboa/maillog/management/commands/update_statistics.py b/modoboa/maillog/management/commands/update_statistics.py index 031ddbc51..537ae6977 100644 --- a/modoboa/maillog/management/commands/update_statistics.py +++ b/modoboa/maillog/management/commands/update_statistics.py @@ -29,7 +29,7 @@ class Command(BaseCommand): """Create RRD file.""" step = 3600 ds_name = "new_accounts" - params = ["DS:{}:ABSOLUTE:{}:0:U".format(ds_name, step * 2)] + params = [f"DS:{ds_name}:ABSOLUTE:{step * 2}:0:U"] params += [ "RRA:AVERAGE:0.5:1:48", # 48 hours with a 1h granularity "RRA:AVERAGE:0.5:24:31", # 31 days with a 1d granularity diff --git a/modoboa/maillog/parser.py b/modoboa/maillog/parser.py index 093b91e56..3b47a1b75 100644 --- a/modoboa/maillog/parser.py +++ b/modoboa/maillog/parser.py @@ -1,6 +1,5 @@ """Parsing tools for maillog related files.""" -import io import re import time @@ -163,7 +162,7 @@ class MaillogParser: m = self._srs_regex["reverse_srs1"].match(mail_address) if m is None else m if m is not None: - return "%s@%s" % m.group(2, 1) + return "{}@{}".format(*m.group(2, 1)) return mail_address def new_domain_event(self, domain, name, size=None): @@ -222,9 +221,7 @@ class MaillogParser: return False (msg_to, msg_status) = m.groups() if queue_id not in self.workdict: - self._dprint( - "[parser] inconsistent mail (%s: %s), skipping" % (queue_id, msg_to) - ) + self._dprint(f"[parser] inconsistent mail ({queue_id}: {msg_to}), skipping") return True # orig_to is optional. @@ -269,18 +266,18 @@ class MaillogParser: host, prog, subprog, pid, log = m.groups() try: - parser = getattr(self, "_parse_{}".format(prog)) + parser = getattr(self, f"_parse_{prog}") if not parser(log, host, pid, subprog): - self._dprint("[parser] ignoring %r log: %r" % (prog, log)) + self._dprint(f"[parser] ignoring {prog!r} log: {log!r}") except AttributeError: - self._dprint('[parser] no log handler for "%r": %r'.format(prog, log)) + self._dprint(f'[parser] no log handler for "{prog!r}": {log!r}') def parse(self, logfile): """Process the log file.""" try: - fp = io.open(logfile, encoding="utf-8") - except IOError as errno: - self._dprint("%s" % errno) + fp = open(logfile, encoding="utf-8") + except OSError as errno: + self._dprint(f"{errno}") return for line in fp.readlines(): diff --git a/modoboa/maillog/tests/test_views.py b/modoboa/maillog/tests/test_views.py index 4da688cc9..9544895d5 100644 --- a/modoboa/maillog/tests/test_views.py +++ b/modoboa/maillog/tests/test_views.py @@ -15,11 +15,11 @@ from modoboa.core import models as core_models from modoboa.lib.tests import ModoTestCase -class RunCommandsMixin(object): +class RunCommandsMixin: """Mixin to run management commands.""" def setUp(self): - super(RunCommandsMixin, self).setUp() + super().setUp() self.workdir = tempfile.mkdtemp() self.set_global_parameter("rrd_rootdir", self.workdir) @@ -54,12 +54,12 @@ class ViewsTestCase(RunCommandsMixin, ModoTestCase): @classmethod def setUpTestData(cls): # noqa - super(ViewsTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() cls.da = core_models.User.objects.get(username="admin@test.com") def tearDown(self): - super(ViewsTestCase, self).tearDown() + super().tearDown() self.set_global_parameter("greylist", False) def test_index(self): @@ -78,39 +78,37 @@ class ViewsTestCase(RunCommandsMixin, ModoTestCase): self.run_logparser() url = reverse("maillog:graph_list") self.ajax_get(url, status=404) - response = self.ajax_get("{}?gset=mailtraffic".format(url)) + response = self.ajax_get(f"{url}?gset=mailtraffic") self.assertIn("averagetraffic", response["graphs"]) for period in ["week", "month", "year"]: - response = self.ajax_get( - "{}?gset=mailtraffic&period={}".format(url, period) - ) + response = self.ajax_get(f"{url}?gset=mailtraffic&period={period}") self.assertIn("averagetraffic", response["graphs"]) self.assertEqual(response["period_name"], period) # custom period today = datetime.date.today() - start = "{} 11:00:00".format(today) - end = "{} 11:40:00".format(today) + start = f"{today} 11:00:00" + end = f"{today} 11:40:00" response = self.ajax_get( - "{}?gset=mailtraffic&period=custom&start={}&end={}".format(url, start, end) + f"{url}?gset=mailtraffic&period=custom&start={start}&end={end}" ) self.assertIn("averagetraffic", response["graphs"]) # unknown domain response = self.ajax_get( - "{}?gset=mailtraffic&searchquery=unknown.com".format(url), status=400 + f"{url}?gset=mailtraffic&searchquery=unknown.com", status=400 ) # check with greylist enabled self.set_global_parameter("greylist", True) - response = self.ajax_get("{}?gset=mailtraffic".format(url)) + response = self.ajax_get(f"{url}?gset=mailtraffic") self.assertIn("averagetraffic", response["graphs"]) def test_account_created_graph(self): """Check data.""" self.run_update_statistics(rebuild=True) url = reverse("maillog:graph_list") - response = self.ajax_get("{}?gset=accountgraphicset".format(url)) + response = self.ajax_get(f"{url}?gset=accountgraphicset") data = response["graphs"]["accountcreationgraphic"]["series"][0]["data"] self.assertEqual(data[-1]["y"], 5.0) @@ -122,10 +120,10 @@ class ViewsTestCase(RunCommandsMixin, ModoTestCase): response = self.ajax_get(url) self.assertIn("averagetraffic", response["graphs"]) - response = self.ajax_get("{}&searchquery=test.com".format(url)) + response = self.ajax_get(f"{url}&searchquery=test.com") self.assertIn("averagetraffic", response["graphs"]) - response = self.ajax_get("{}&searchquery=test2.com".format(url), status=403) + response = self.ajax_get(f"{url}&searchquery=test2.com", status=403) def test_get_domain_list(self): """Test get_domain_list view.""" @@ -141,14 +139,14 @@ class ManagementCommandsTestCase(RunCommandsMixin, ModoTestCase): @classmethod def setUpTestData(cls): # noqa - super(ManagementCommandsTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() def test_logparser(self): """Test logparser command.""" self.run_logparser() for d in ["global", "test.com"]: - path = os.path.join(self.workdir, "{}.rrd".format(d)) + path = os.path.join(self.workdir, f"{d}.rrd") self.assertTrue(os.path.exists(path)) def test_logparser_with_greylist(self): @@ -156,7 +154,7 @@ class ManagementCommandsTestCase(RunCommandsMixin, ModoTestCase): self.set_global_parameter("greylist", True) self.run_logparser() for d in ["global", "test.com"]: - path = os.path.join(self.workdir, "{}.rrd".format(d)) + path = os.path.join(self.workdir, f"{d}.rrd") self.assertTrue(os.path.exists(path)) def test_update_statistics(self): diff --git a/modoboa/maillog/views.py b/modoboa/maillog/views.py index 15264d9b6..e366590c0 100644 --- a/modoboa/maillog/views.py +++ b/modoboa/maillog/views.py @@ -1,5 +1,3 @@ -# coding: utf-8 - """Custom views.""" import re @@ -77,12 +75,12 @@ def graphs(request): start = request.GET["start"] end = request.GET["end"] expr = re.compile(r"[:\- ]") - period_name = "%s_%s" % (expr.sub("", start), expr.sub("", end)) + period_name = "{}_{}".format(expr.sub("", start), expr.sub("", end)) start = date_to_timestamp(expr.split(start)) end = date_to_timestamp(expr.split(end)) else: end = int(time.mktime(time.localtime())) - start = "-1%s" % period + start = f"-1{period}" period_name = period tplvars["domain_selector"] = graph_sets[gset].domain_selector diff --git a/modoboa/parameters/forms.py b/modoboa/parameters/forms.py index 49a40eba4..5b587d89d 100644 --- a/modoboa/parameters/forms.py +++ b/modoboa/parameters/forms.py @@ -21,7 +21,7 @@ class GenericParametersForm(forms.Form): kwargs["prefix"] = self.app load_values_from_db = kwargs.pop("load_values_from_db", True) - super(GenericParametersForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._add_dynamic_fields() @@ -36,8 +36,8 @@ class GenericParametersForm(forms.Form): """Add visibility rules to this form.""" for key, rule in list(rules.items()): field, value = rule.split("=") - visibility = {"field": "id_%s-%s" % (self.app, field), "value": value} - self.visirules["%s-%s" % (self.app, key)] = visibility + visibility = {"field": f"id_{self.app}-{field}", "value": value} + self.visirules[f"{self.app}-{key}"] = visibility def _add_dynamic_fields(self): """Add dynamic fields to this form.""" @@ -60,7 +60,7 @@ class AdminParametersForm(GenericParametersForm): def __init__(self, *args, **kwargs): """Store LocalConfig instance.""" self.localconfig = kwargs.pop("localconfig", None) - super(AdminParametersForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _load_initial_values(self): """Load form initial values from database.""" @@ -92,7 +92,7 @@ class UserParametersForm(GenericParametersForm): def __init__(self, *args, **kwargs): self.user = kwargs.pop("user", None) - super(UserParametersForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _load_initial_values(self): """Load initial values from User instance.""" diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py index a1f96eded..bbf407d1a 100644 --- a/modoboa/parameters/tools.py +++ b/modoboa/parameters/tools.py @@ -20,8 +20,8 @@ class NotDefined(exceptions.ModoboaException): def __str__(self): if self.name is None: - return "Application {} not registered".format(self.app) - return "Parameter {} not defined for app {}".format(self.name, self.app) + return f"Application {self.app} not registered" + return f"Parameter {self.name} not defined for app {self.app}" class Registry: @@ -124,7 +124,7 @@ class Registry: if for_app and for_app != app: continue serializer = fields["serializer_class"]() - for section, sconfig in list(fields["structure"].items()): + for _section, sconfig in list(fields["structure"].items()): item = { "label": sconfig["label"], "display": sconfig.get("display", ""), diff --git a/modoboa/pdfcredentials/api/v2/views.py b/modoboa/pdfcredentials/api/v2/views.py index 15f7fc581..f1756f4b0 100644 --- a/modoboa/pdfcredentials/api/v2/views.py +++ b/modoboa/pdfcredentials/api/v2/views.py @@ -1,8 +1,5 @@ """PDF Credentials API v2 views.""" -import os - -from django.utils.translation import gettext as _ from django.http import HttpResponse from rest_framework import permissions diff --git a/modoboa/pdfcredentials/app_settings.py b/modoboa/pdfcredentials/app_settings.py index e6f81a745..6e44736b6 100644 --- a/modoboa/pdfcredentials/app_settings.py +++ b/modoboa/pdfcredentials/app_settings.py @@ -308,9 +308,9 @@ class ParametersForm(param_forms.AdminParametersForm): def __init__(self, *args, **kwargs): """Set initial values.""" - super(ParametersForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if not self.fields["webpanel_url"].initial: - url = "https://{}{}".format(self.hostname, settings.LOGIN_URL) + url = f"https://{self.hostname}{settings.LOGIN_URL}" self.fields["webpanel_url"].initial = url if not self.fields["smtp_server_address"].initial: self.fields["smtp_server_address"].initial = self.hostname diff --git a/modoboa/pdfcredentials/apps.py b/modoboa/pdfcredentials/apps.py index 45439ef86..1bce5fa4b 100644 --- a/modoboa/pdfcredentials/apps.py +++ b/modoboa/pdfcredentials/apps.py @@ -35,6 +35,6 @@ class PDFCredentialsConfig(AppConfig): verbose_name = "PDF credentials for Modoboa" def ready(self): - from . import handlers + from . import handlers # noqa load_pdfcredential_settings() diff --git a/modoboa/pdfcredentials/handlers.py b/modoboa/pdfcredentials/handlers.py index c67ca2ec4..1e9ae5ec0 100644 --- a/modoboa/pdfcredentials/handlers.py +++ b/modoboa/pdfcredentials/handlers.py @@ -28,7 +28,7 @@ def password_updated(sender, account, password, created, **kwargs): return try: init_storage_dir() - except InternalError as e: + except InternalError: logger = logging.getLogger("modoboa.admin") logger.error( _( diff --git a/modoboa/pdfcredentials/lib.py b/modoboa/pdfcredentials/lib.py index 72897fac9..5dbd934b7 100644 --- a/modoboa/pdfcredentials/lib.py +++ b/modoboa/pdfcredentials/lib.py @@ -29,11 +29,11 @@ def init_storage_dir(): ) try: os.mkdir(storage_dir) - except (OSError, IOError) as inst: + except OSError as inst: raise InternalError( _("Failed to create the directory that will contain " "PDF documents (%s)") % inst - ) + ) from None def get_creds_filename(account): @@ -74,7 +74,7 @@ def crypt_and_save_to_file(content, filename, length, chunksize=64 * 512): chunk += b" " * (16 - len(chunk) % 16) fp.write(encryptor.update(force_bytes(chunk))) fp.write(encryptor.finalize()) - except FileNotFoundError as e: + except FileNotFoundError: logger = logging.getLogger("modoboa.admin") logger.error( _( @@ -123,5 +123,5 @@ def rfc_6266_content_disposition(filename): filename.replace("\\", "\\\\").replace('"', r"\"") ) except UnicodeEncodeError: - file_expr = "filename*=utf-8''{}".format(quote(filename)) + file_expr = f"filename*=utf-8''{quote(filename)}" return f"attachment; {file_expr}" diff --git a/modoboa/pdfcredentials/models.py b/modoboa/pdfcredentials/models.py index 71a836239..6b2021999 100644 --- a/modoboa/pdfcredentials/models.py +++ b/modoboa/pdfcredentials/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/modoboa/pdfcredentials/tests/test_lib.py b/modoboa/pdfcredentials/tests/test_lib.py index c66aea3e2..dee8c8221 100644 --- a/modoboa/pdfcredentials/tests/test_lib.py +++ b/modoboa/pdfcredentials/tests/test_lib.py @@ -2,13 +2,10 @@ import os import shutil -import tempfile -from django.test import override_settings from django.urls import reverse from modoboa.admin import factories -from modoboa.core import models as core_models from modoboa.lib.tests import ModoAPITestCase from modoboa.lib.exceptions import InternalError @@ -57,7 +54,7 @@ class PDFCredentialsLibTestCase(ModoAPITestCase): """Test that PDFs are decrypted.""" username = "toto1818@test.com" self._create_account(username) - fname = os.path.join(self.workdir, "{}.pdf".format(username)) + fname = os.path.join(self.workdir, f"{username}.pdf") self.assertTrue(os.path.exists(fname)) filebuff = lib.decrypt_file(fname) self.assertIn( @@ -67,7 +64,7 @@ class PDFCredentialsLibTestCase(ModoAPITestCase): .strip() .decode(), ) - with open(fname, "r") as file: + with open(fname) as file: with self.assertRaises(UnicodeDecodeError): Popen( "/usr/bin/file -b --mime -", shell=True, stdout=PIPE, stdin=PIPE diff --git a/modoboa/pdfcredentials/tests/test_views.py b/modoboa/pdfcredentials/tests/test_views.py index 96ef17db4..b79972db8 100644 --- a/modoboa/pdfcredentials/tests/test_views.py +++ b/modoboa/pdfcredentials/tests/test_views.py @@ -19,12 +19,12 @@ class EventsTestCase(ModoTestCase): @classmethod def setUpTestData(cls): """Create some data.""" - super(EventsTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.DomainFactory(name="test.com") def setUp(self): """Create temp. directory to store files.""" - super(EventsTestCase, self).setUp() + super().setUp() self.workdir = tempfile.mkdtemp() self.set_global_parameter("storage_dir", self.workdir) @@ -141,7 +141,7 @@ class EventsTestCase(ModoTestCase): """Test storage directory creation.""" self.set_global_parameter("storage_dir", "/nonexistentdir") with self.assertLogs(logger="modoboa.admin", level="ERROR") as log: - resp = self._create_account("leon@test.com") + self._create_account("leon@test.com") self.assertIn( "ERROR:modoboa.admin:Failed to create PDF_credentials directory. " "Please check the permissions or the path.", diff --git a/modoboa/policyd/core.py b/modoboa/policyd/core.py index 96a085bf0..e914ae24f 100644 --- a/modoboa/policyd/core.py +++ b/modoboa/policyd/core.py @@ -135,7 +135,7 @@ async def decrement_limit(rclient, ltype, name): """Decrement the given limit by one.""" new_counter = await rclient.hincrby(constants.REDIS_HASHNAME, name, -1) if new_counter <= 0: - logger.info("Limit reached for {} {}".format(ltype, name)) + logger.info(f"Limit reached for {ltype} {name}") asyncio.ensure_future(notify_limit_reached(ltype, name)) @@ -152,13 +152,13 @@ async def apply_policies(attributes): localpart, domain = split_mailbox(sasl_username) if await rclient.hexists(constants.REDIS_HASHNAME, domain): counter = await rclient.hget(constants.REDIS_HASHNAME, domain) - logger.info("Domain {} current counter: {}".format(domain, counter)) + logger.info(f"Domain {domain} current counter: {counter}") if int(counter) <= 0: return FAILURE_ACTION decr_domain = True if await rclient.hexists(constants.REDIS_HASHNAME, sasl_username): counter = await rclient.hget(constants.REDIS_HASHNAME, sasl_username) - logger.info("Account {} current counter: {}".format(sasl_username, counter)) + logger.info(f"Account {sasl_username} current counter: {counter}") if int(counter) <= 0: return FAILURE_ACTION decr_user = True diff --git a/modoboa/policyd/management/commands/policy_daemon.py b/modoboa/policyd/management/commands/policy_daemon.py index e1abc2948..c8096c96e 100644 --- a/modoboa/policyd/management/commands/policy_daemon.py +++ b/modoboa/policyd/management/commands/policy_daemon.py @@ -45,7 +45,7 @@ class Command(BaseCommand): getattr(signal, signame), functools.partial(ask_exit, signame, loop) ) - logger.info("Serving on {}".format(server.sockets[0].getsockname())) + logger.info(f"Serving on {server.sockets[0].getsockname()}") if options["debug"]: loop.set_debug(True) diff --git a/modoboa/postfix_autoreply/__init__.py b/modoboa/postfix_autoreply/__init__.py index c1b6d5fa5..d62a6f91a 100644 --- a/modoboa/postfix_autoreply/__init__.py +++ b/modoboa/postfix_autoreply/__init__.py @@ -1,9 +1,5 @@ -# -*- coding: utf-8 -*- - """Away message editor for Modoboa (postfix compatible).""" -from __future__ import unicode_literals - from pkg_resources import DistributionNotFound, get_distribution try: diff --git a/modoboa/postfix_autoreply/api/v2/tests.py b/modoboa/postfix_autoreply/api/v2/tests.py index 782934838..0fd554432 100644 --- a/modoboa/postfix_autoreply/api/v2/tests.py +++ b/modoboa/postfix_autoreply/api/v2/tests.py @@ -14,7 +14,7 @@ class ARMessageViewSetTestCase(ModoAPITestCase): @classmethod def setUpTestData(cls): - super(ARMessageViewSetTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() cls.account = User.objects.get(username="user@test.com") cls.account2 = User.objects.get(username="user@test2.com") @@ -27,17 +27,13 @@ class ARMessageViewSetTestCase(ModoAPITestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 2) # Test filters - response = self.client.get( - url + "?mbox={}".format(self.account.mailbox.full_address) - ) + response = self.client.get(url + f"?mbox={self.account.mailbox.full_address}") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) - response = self.client.get( - url + "?mbox={}".format(self.account.mailbox.address) - ) + response = self.client.get(url + f"?mbox={self.account.mailbox.address}") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 2) - response = self.client.get(url + "?mbox__user={}".format(self.account.pk)) + response = self.client.get(url + f"?mbox__user={self.account.pk}") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) # Test retrieve diff --git a/modoboa/postfix_autoreply/factories.py b/modoboa/postfix_autoreply/factories.py index 70c2f88f6..d58471d56 100644 --- a/modoboa/postfix_autoreply/factories.py +++ b/modoboa/postfix_autoreply/factories.py @@ -1,9 +1,5 @@ -# -*- coding: utf-8 -*- - """Postfix auto reply factories.""" -from __future__ import unicode_literals - import factory from modoboa.admin import factories as admin_factories diff --git a/modoboa/postfix_autoreply/forms.py b/modoboa/postfix_autoreply/forms.py index f36641a45..f43f60ac8 100644 --- a/modoboa/postfix_autoreply/forms.py +++ b/modoboa/postfix_autoreply/forms.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Custom forms.""" from collections import OrderedDict @@ -152,7 +150,7 @@ Best regards, try: tpl % {"name": "Antoine Nguyen"} except (KeyError, ValueError): - raise forms.ValidationError(gettext_lazy("Invalid syntax")) + raise forms.ValidationError(gettext_lazy("Invalid syntax")) from None return tpl diff --git a/modoboa/postfix_autoreply/handlers.py b/modoboa/postfix_autoreply/handlers.py index 694a78448..6bd741ac1 100644 --- a/modoboa/postfix_autoreply/handlers.py +++ b/modoboa/postfix_autoreply/handlers.py @@ -17,14 +17,14 @@ def manage_transport_entry(sender, instance, **kwargs): """Create or update a transport entry for this domain.""" if kwargs.get("created"): tr_models.Transport.objects.get_or_create( - pattern="autoreply.{}".format(instance), service="autoreply" + pattern=f"autoreply.{instance}", service="autoreply" ) return oldname = getattr(instance, "oldname", "None") if oldname is None or oldname == instance.name: return - tr_models.Transport.objects.filter(pattern="autoreply.{}".format(oldname)).update( - pattern="autoreply.{}".format(instance.name) + tr_models.Transport.objects.filter(pattern=f"autoreply.{oldname}").update( + pattern=f"autoreply.{instance.name}" ) qset = admin_models.AliasRecipient.objects.select_related( "alias", "r_mailbox" @@ -39,7 +39,7 @@ def manage_transport_entry(sender, instance, **kwargs): @receiver(signals.post_delete, sender=admin_models.Domain) def delete_transport_entry(sender, instance, **kwargs): """Delete a transport entry.""" - tr_models.Transport.objects.filter(pattern="autoreply.{}".format(instance)).delete() + tr_models.Transport.objects.filter(pattern=f"autoreply.{instance}").delete() @receiver(signals.post_save, sender=admin_models.Mailbox) @@ -49,24 +49,22 @@ def rename_autoreply_alias(sender, instance, **kwargs): if old_address is None or old_address == instance.full_address: return admin_models.AliasRecipient.objects.filter( - address__contains="{}@autoreply".format(old_address) - ).update(address="{}@autoreply.{}".format(instance.full_address, instance.domain)) + address__contains=f"{old_address}@autoreply" + ).update(address=f"{instance.full_address}@autoreply.{instance.domain}") @receiver(signals.post_delete, sender=admin_models.Mailbox) def delete_autoreply_alias(sender, instance, **kwargs): """Delete alias.""" admin_models.AliasRecipient.objects.filter( - address="{}@autoreply.{}".format(instance.full_address, instance.domain) + address=f"{instance.full_address}@autoreply.{instance.domain}" ).delete() @receiver(signals.post_save, sender=models.ARmessage) def manage_autoreply_alias(sender, instance, **kwargs): """Create or delete the alias.""" - ar_alias_address = "{}@autoreply.{}".format( - instance.mbox.full_address, instance.mbox.domain - ) + ar_alias_address = f"{instance.mbox.full_address}@autoreply.{instance.mbox.domain}" admin_models.Alias.objects.get( address=instance.mbox.full_address, domain=instance.mbox.domain, internal=True ) @@ -94,15 +92,13 @@ def extra_js(sender, caller, st_type, user, **kwargs): """Add static content.""" if caller != "user_index" or st_type != "js": return "" - return """function autoreply_cb() { - $('.datefield').datetimepicker({ + return f"""function autoreply_cb() {{ + $('.datefield').datetimepicker({{ format: 'YYYY-MM-DD HH:mm:ss', - locale: '%(lang)s' - }); -} -""" % { - "lang": user.language - } + locale: '{user.language}' + }}); +}} +""" @receiver(core_signals.extra_user_menu_entries) diff --git a/modoboa/postfix_autoreply/known_problems.py b/modoboa/postfix_autoreply/known_problems.py index bcb354c55..843f99621 100644 --- a/modoboa/postfix_autoreply/known_problems.py +++ b/modoboa/postfix_autoreply/known_problems.py @@ -1,9 +1,5 @@ -# -*- coding: utf-8 -*- - """Know problems and associated fixes.""" -from __future__ import unicode_literals - from modoboa.admin import models as admin_models from modoboa.admin.management.commands.subcommands import _repair from . import models @@ -22,12 +18,12 @@ def ensure_autoreplies_recipents_are_valids(**options): mbox__address=address, mbox__domain__name=domain ) if not arqs.exists(): - _repair.log("Delete {0} (AR does not exist)".format(alr), **options) + _repair.log(f"Delete {alr} (AR does not exist)", **options) alr.delete() deleted += 1 elif arqs.filter(enabled=False).exists(): - _repair.log("Delete {0} (AR is disabled)".format(alr), **options) + _repair.log(f"Delete {alr} (AR is disabled)", **options) alr.delete() deleted += 1 if deleted: - _repair.log("{0} alias recipient(s) deleted".format(deleted), **options) + _repair.log(f"{deleted} alias recipient(s) deleted", **options) diff --git a/modoboa/postfix_autoreply/management/commands/autoreply.py b/modoboa/postfix_autoreply/management/commands/autoreply.py index 7dbcb02c5..43715f083 100644 --- a/modoboa/postfix_autoreply/management/commands/autoreply.py +++ b/modoboa/postfix_autoreply/management/commands/autoreply.py @@ -5,7 +5,6 @@ import fileinput from io import StringIO import logging import smtplib -import socket import sys from logging.handlers import SysLogHandler @@ -91,7 +90,7 @@ def send_autoreply(sender, mailbox, armessage, original_msg): context.update({"untildate": localize(armessage.untildate)}) content = armessage.content % context msg = EmailMessage( - "Auto: {} Re: {}".format(armessage.subject, subject), + f"Auto: {armessage.subject} Re: {subject}", smart_str(content), mailbox.user.encoded_address, [sender], @@ -126,7 +125,7 @@ class Command(BaseCommand): def handle(self, *args, **options): try: handler = SysLogHandler(address=options["syslog_socket_path"]) - except socket.error as ex: + except OSError as ex: if ex.errno == 2: # try the default, localhost:514 handler = SysLogHandler() @@ -193,7 +192,7 @@ class Command(BaseCommand): try: mbox = Mailbox.objects.get(address=address, domain__name=domain) except Mailbox.DoesNotExist: - msg = "Unknown recipient %s" % (fulladdress) + msg = f"Unknown recipient {fulladdress}" logger.debug("autoreply %s", msg) continue try: diff --git a/modoboa/postfix_autoreply/migrations/0003_move_aliases.py b/modoboa/postfix_autoreply/migrations/0003_move_aliases.py index a45a0d27a..aba8e5c53 100644 --- a/modoboa/postfix_autoreply/migrations/0003_move_aliases.py +++ b/modoboa/postfix_autoreply/migrations/0003_move_aliases.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations def move_aliases(apps, schema_editor): diff --git a/modoboa/postfix_autoreply/migrations/0004_delete_alias.py b/modoboa/postfix_autoreply/migrations/0004_delete_alias.py index 4e23ae3fb..86df2b121 100644 --- a/modoboa/postfix_autoreply/migrations/0004_delete_alias.py +++ b/modoboa/postfix_autoreply/migrations/0004_delete_alias.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/modoboa/postfix_autoreply/migrations/0005_auto_20151202_1623.py b/modoboa/postfix_autoreply/migrations/0005_auto_20151202_1623.py index eb2c5b0cb..18873c578 100644 --- a/modoboa/postfix_autoreply/migrations/0005_auto_20151202_1623.py +++ b/modoboa/postfix_autoreply/migrations/0005_auto_20151202_1623.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models +from django.db import migrations def remove_useless_aliases(apps, schema_editor): diff --git a/modoboa/postfix_autoreply/models.py b/modoboa/postfix_autoreply/models.py index 02602124e..8feb0c875 100644 --- a/modoboa/postfix_autoreply/models.py +++ b/modoboa/postfix_autoreply/models.py @@ -29,7 +29,7 @@ class ARmessage(models.Model): db_table = "postfix_autoreply_armessage" def __str__(self): - return "AR<{}>: {}".format(self.mbox, self.enabled) + return f"AR<{self.mbox}>: {self.enabled}" class ARhistoric(models.Model): diff --git a/modoboa/postfix_autoreply/tests.py b/modoboa/postfix_autoreply/tests.py index 4b252a325..2e1cf00d9 100644 --- a/modoboa/postfix_autoreply/tests.py +++ b/modoboa/postfix_autoreply/tests.py @@ -13,7 +13,7 @@ from django.utils.formats import date_format from modoboa.admin import factories as admin_factories, models as admin_models from modoboa.core.models import User -from modoboa.lib.tests import ModoTestCase, ModoAPITestCase +from modoboa.lib.tests import ModoTestCase from modoboa.transport import models as tr_models from . import factories, models @@ -111,7 +111,7 @@ class EventsTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(EventsTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() def test_domain_created_event(self): @@ -241,12 +241,12 @@ class FormTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(FormTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() def setUp(self): """Initialize tests.""" - super(FormTestCase, self).setUp() + super().setUp() self.client.logout() self.client.login(username="user@test.com", password="toto") @@ -314,7 +314,7 @@ class RepairTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create some data.""" - super(RepairTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() def test_management_command(self): @@ -322,7 +322,7 @@ class RepairTestCase(ModoTestCase): mbox = admin_models.Mailbox.objects.get(user__username="user@test.com") alias = admin_models.Alias.objects.get(internal=True, address=mbox.full_address) arm = factories.ARmessageFactory(mbox=mbox, enabled=False) - ar_address = "{}@autoreply.{}".format(mbox.full_address, mbox.domain.name) + ar_address = f"{mbox.full_address}@autoreply.{mbox.domain.name}" admin_factories.AliasRecipientFactory(alias=alias, address=ar_address) management.call_command("modo", "repair", "--quiet") self.assertFalse( @@ -342,13 +342,13 @@ class ManagementCommandTestCase(ModoTestCase): @classmethod def setUpTestData(cls): # NOQA:N802 """Create some data.""" - super(ManagementCommandTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() cls.account = User.objects.get(username="user@test.com") def setUp(self): """Replace stdin.""" - super(ManagementCommandTestCase, self).setUp() + super().setUp() self.stdin = sys.stdin sys.stdin = StringIO(SIMPLE_EMAIL_CONTENT.strip()) self.arm = factories.ARmessageFactory(mbox=self.account.mailbox) diff --git a/modoboa/postfix_autoreply/views.py b/modoboa/postfix_autoreply/views.py index 58ce8ee07..8495fea3c 100644 --- a/modoboa/postfix_autoreply/views.py +++ b/modoboa/postfix_autoreply/views.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from django.contrib.auth.decorators import login_required from django.template.loader import render_to_string from django.utils.translation import gettext as _ diff --git a/modoboa/relaydomains/api/v1/serializers.py b/modoboa/relaydomains/api/v1/serializers.py index 4461b45d2..aa9028228 100644 --- a/modoboa/relaydomains/api/v1/serializers.py +++ b/modoboa/relaydomains/api/v1/serializers.py @@ -26,11 +26,7 @@ class TransportSerializer(serializers.ModelSerializer): errors = self.backend.clean_fields(data["_settings"]) if errors: raise serializers.ValidationError( - { - "_settings": ",".join( - ["{}: {}".format(error[0], error[1]) for error in errors] - ) - } + {"_settings": ",".join([f"{error[0]}: {error[1]}" for error in errors])} ) return data diff --git a/modoboa/relaydomains/api/v1/tests.py b/modoboa/relaydomains/api/v1/tests.py index 7f3a1944b..1c05bdf08 100644 --- a/modoboa/relaydomains/api/v1/tests.py +++ b/modoboa/relaydomains/api/v1/tests.py @@ -9,12 +9,12 @@ from modoboa.lib.tests import ModoAPITestCase from modoboa.transport import factories as tr_factories, models as tr_models -class DataMixin(object): +class DataMixin: """A mixin to provide test data.""" @classmethod def setUpTestData(cls): # NOQA:N802 - super(DataMixin, cls).setUpTestData() + super().setUpTestData() transport = tr_factories.TransportFactory( pattern="test.com", _settings={ diff --git a/modoboa/relaydomains/forms.py b/modoboa/relaydomains/forms.py index 34f00c1e4..b55fe4329 100644 --- a/modoboa/relaydomains/forms.py +++ b/modoboa/relaydomains/forms.py @@ -6,7 +6,7 @@ from modoboa.lib.form_utils import WizardStep from modoboa.transport import forms as tr_forms, models as tr_models -class DisableSignals(object): +class DisableSignals: """Context manager to disable signals.""" def __init__(self): @@ -52,7 +52,7 @@ class RelayDomainFormGeneral(tr_forms.TransportForm): # We don't want to recreate the transport we just deleted it # (post_save signal). return None - instance = super(RelayDomainFormGeneral, self).save() + instance = super().save() instance.pattern = domain.name instance.save() if not domain.transport: diff --git a/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py b/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py index 8b28b733d..bdeae0015 100644 --- a/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py +++ b/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/modoboa/relaydomains/postfix_maps.py b/modoboa/relaydomains/postfix_maps.py index 00b4e49c3..3d937c36c 100644 --- a/modoboa/relaydomains/postfix_maps.py +++ b/modoboa/relaydomains/postfix_maps.py @@ -1,7 +1,7 @@ """Map file definitions for postfix.""" -class RelayDomainsMap(object): +class RelayDomainsMap: """Map file to list all relay domains.""" filename = "sql-relaydomains.cf" @@ -19,7 +19,7 @@ class RelayDomainsMap(object): ) -class SplitedDomainsTransportMap(object): +class SplitedDomainsTransportMap: """A transport map for splited domains. (ie. ones with both local and remote mailboxes) @@ -55,7 +55,7 @@ class SplitedDomainsTransportMap(object): ) -class RelayRecipientVerification(object): +class RelayRecipientVerification: """A map file to enable recipient verification.""" filename = "sql-relay-recipient-verification.cf" diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index 3e2688c54..9b3b722b1 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -13,7 +13,7 @@ from modoboa.transport import factories as tr_factories, models as tr_models from . import models -class Operations(object): +class Operations: def _create_relay_domain(self, name, status=200, **kwargs): values = { @@ -48,7 +48,7 @@ class Operations(object): else: aliases.remove(name) for cpt, alias in enumerate(aliases): - fname = "aliases" if not cpt else "aliases_%d" % cpt + fname = "aliases" if not cpt else f"aliases_{cpt}" values[fname] = alias return self.ajax_post( reverse("admin:domain_change", args=[domain.id]), values, status @@ -65,7 +65,7 @@ class RelayDomainsTestCase(ModoTestCase, Operations): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(RelayDomainsTestCase, cls).setUpTestData() + super().setUpTestData() admin_factories.populate_database() cls.transport = tr_factories.TransportFactory( pattern="relaydomain.tld", @@ -277,17 +277,17 @@ class LimitsTestCase(ModoTestCase, Operations): @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" - super(LimitsTestCase, cls).setUpTestData() + super().setUpTestData() for name, _definition in limits_utils.get_user_limit_templates(): cls.localconfig.parameters.set_value( - "deflt_user_{0}_limit".format(name), 2, app="limits" + f"deflt_user_{name}_limit", 2, app="limits" ) cls.localconfig.save() cls.user = UserFactory.create(username="reseller", groups=("Resellers",)) def setUp(self): """Initialize test.""" - super(LimitsTestCase, self).setUp() + super().setUp() self.client.force_login(self.user) def test_relay_domains_limit(self): diff --git a/modoboa/sievefilters/__init__.py b/modoboa/sievefilters/__init__.py index 15612f649..a48e9b877 100644 --- a/modoboa/sievefilters/__init__.py +++ b/modoboa/sievefilters/__init__.py @@ -1,9 +1,5 @@ -# -*- coding: utf-8 -*- - """The sieve filters editor of Modoboa.""" -from __future__ import unicode_literals - from pkg_resources import get_distribution, DistributionNotFound diff --git a/modoboa/sievefilters/api/v2/viewsets.py b/modoboa/sievefilters/api/v2/viewsets.py index 52d7fca9f..7f56b93cc 100644 --- a/modoboa/sievefilters/api/v2/viewsets.py +++ b/modoboa/sievefilters/api/v2/viewsets.py @@ -15,7 +15,11 @@ from rest_framework.decorators import action from modoboa.lib.viewsets import HasMailbox from modoboa.sievefilters import constants -from modoboa.sievefilters.lib import SieveClient, SieveClientError, SieveActionTemplateNotFound +from modoboa.sievefilters.lib import ( + SieveClient, + SieveClientError, + SieveActionTemplateNotFound, +) from modoboa.sievefilters.rfc6266 import build_header from modoboa.sievefilters.api.v2 import serializers @@ -184,7 +188,7 @@ class FilterSetViewSet(viewsets.ViewSet): except FilterAlreadyExists: raise serializers.ValidationError( {"name": _("Filter %s already exists") % fltname} - ) + ) from None sclient.pushscript(fset.name, str(fset)) return response.Response(serializer.validated_data, status=201) diff --git a/modoboa/sievefilters/imaputils.py b/modoboa/sievefilters/imaputils.py index 505a0be5b..215daf79d 100644 --- a/modoboa/sievefilters/imaputils.py +++ b/modoboa/sievefilters/imaputils.py @@ -4,7 +4,6 @@ import base64 import imaplib import re from operator import itemgetter -import socket import ssl from django.utils.translation import gettext as _ @@ -60,9 +59,9 @@ class IMAPconnector: try: typ, data = self.m._simple_command(name, *args) except imaplib.IMAP4.error as e: - raise ImapError(e) + raise ImapError(e) from None if typ == "NO": - raise ImapError(data) + raise ImapError(data) from None if "responses" not in kwargs: if name not in self.m.untagged_responses: return None @@ -103,8 +102,8 @@ class IMAPconnector: self.m = imaplib.IMAP4_SSL(self.address, self.port) else: self.m = imaplib.IMAP4(self.address, self.port) - except (socket.error, imaplib.IMAP4.error, ssl.SSLError) as error: - raise ImapError(_("Connection to IMAP server failed: %s" % error)) + except (OSError, imaplib.IMAP4.error, ssl.SSLError) as error: + raise ImapError(_(f"Connection to IMAP server failed: {error}")) from None token = ( b"n,a=" @@ -139,9 +138,7 @@ class IMAPconnector: def _listmboxes(self, topmailbox, mailboxes): """Retrieve mailboxes list.""" pattern = ( - '"{0}{1}%"'.format( - topmailbox.encode("imap4-utf-7").decode(), self.hdelimiter - ) + f'"{topmailbox.encode("imap4-utf-7").decode()}{self.hdelimiter}%"' if topmailbox else "%" ) diff --git a/modoboa/sievefilters/lib.py b/modoboa/sievefilters/lib.py index 066035f68..eaec2f733 100644 --- a/modoboa/sievefilters/lib.py +++ b/modoboa/sievefilters/lib.py @@ -33,7 +33,7 @@ class SieveClient: try: ret, msg = self.login(user, password) except (managesieve.Error, ConnectionError) as e: - raise SieveClientError(str(e)) + raise SieveClientError(str(e)) from None if not ret: raise SieveClientError(msg) @@ -76,7 +76,7 @@ class SieveClient: def pushscript(self, name: str, content: str, active: bool = False) -> None: if not self.msc.havespace(name, len(content)): - error = "%s (%s)" % (_("Not enough space on server"), self.msc.errmsg) + error = "{} ({})".format(_("Not enough space on server"), self.msc.errmsg) raise SieveClientError(error) if not self.msc.putscript(name, content): raise SieveClientError(self.msc.errmsg.decode()) diff --git a/modoboa/sievefilters/mocks.py b/modoboa/sievefilters/mocks.py index baad7283f..c60533798 100644 --- a/modoboa/sievefilters/mocks.py +++ b/modoboa/sievefilters/mocks.py @@ -1,5 +1,3 @@ -# coding: utf-8 - """Mock objects.""" SAMPLE_SIEVE_SCRIPT = """require ["fileinto", "copy"]; diff --git a/modoboa/sievefilters/rfc6266.py b/modoboa/sievefilters/rfc6266.py index bbb973fd0..b79ea2b95 100644 --- a/modoboa/sievefilters/rfc6266.py +++ b/modoboa/sievefilters/rfc6266.py @@ -82,25 +82,25 @@ def build_header(filename, disposition="attachment", filename_compat=None): rv = disposition if is_token(filename): - rv += "; filename=%s" % (filename,) + rv += f"; filename={filename}" return rv elif is_ascii(filename) and is_lws_safe(filename): qd_filename = qd_quote(filename) - rv += '; filename="%s"' % (qd_filename,) + rv += f'; filename="{qd_filename}"' if qd_filename == filename: # RFC 6266 claims some implementations are iffy on qdtext's # backslash-escaping, we'll include filename* in that case. return rv elif filename_compat: if is_token(filename_compat): - rv += "; filename=%s" % (filename_compat,) + rv += f"; filename={filename_compat}" else: assert is_lws_safe(filename_compat) - rv += '; filename="%s"' % (qd_quote(filename_compat),) + rv += f'; filename="{qd_quote(filename_compat)}"' # alnum are already considered always-safe, but the rest isn't. # Python encodes ~ when it shouldn't, for example. - rv += "; filename*=utf-8''%s" % ( + rv += "; filename*=utf-8''{}".format( percent_encode(filename, safe=attr_chars_nonalnum, encoding="utf-8"), ) diff --git a/modoboa/transport/backends.py b/modoboa/transport/backends.py index 85d5fe8bb..baeb65b2c 100644 --- a/modoboa/transport/backends.py +++ b/modoboa/transport/backends.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from modoboa.lib import validators as lib_validators -class TransportBackend(object): +class TransportBackend: """Base backend class.""" name = None @@ -42,7 +42,7 @@ class TransportBackend(object): ftype = setting.get("type", "string") if ftype == "string": validator = setting.get("validator") - vfunc = "_validate_{}".format(validator) + vfunc = f"_validate_{validator}" if validator and hasattr(self, vfunc): if getattr(self, vfunc)(value): continue @@ -60,7 +60,7 @@ class TransportBackend(object): pass -class TransportBackendManager(object): +class TransportBackendManager: """Transport backends manager.""" def __init__(self): diff --git a/modoboa/transport/factories.py b/modoboa/transport/factories.py index 37ecc8ffe..eeda75f84 100644 --- a/modoboa/transport/factories.py +++ b/modoboa/transport/factories.py @@ -12,6 +12,6 @@ class TransportFactory(factory.django.DjangoModelFactory): model = models.Transport django_get_or_create = ("pattern",) - pattern = factory.Sequence(lambda n: "transport{}".format(n)) + pattern = factory.Sequence(lambda n: f"transport{n}") service = "relay" next_hop = "[external.host.tld]:25" diff --git a/modoboa/transport/forms.py b/modoboa/transport/forms.py index cfe61ba28..9b0266158 100644 --- a/modoboa/transport/forms.py +++ b/modoboa/transport/forms.py @@ -11,12 +11,12 @@ TYPE_TO_FIELD_MAP = { } -class BackendSettingsMixin(object): +class BackendSettingsMixin: """A mixin to deal with backend settings in a model form.""" def __init__(self, *args, **kwargs): """Constructor.""" - super(BackendSettingsMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setting_field_names = [] def inject_backend_settings(self, name, settings): @@ -44,7 +44,7 @@ class BackendSettingsMixin(object): def save(self, commit=True): """Set settings to JSON field.""" - transport = super(BackendSettingsMixin, self).save(commit=False) + transport = super().save(commit=False) transport._settings = { name: self.cleaned_data[name] for name in self.setting_field_names } @@ -64,7 +64,7 @@ class TransportForm(BackendSettingsMixin, forms.ModelForm): def __init__(self, *args, **kwargs): """Set backend list.""" - super(TransportForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields["service"].choices = backends.manager.get_backend_list() settings = backends.manager.get_all_backend_settings() for name, backend_settings in settings.items(): @@ -79,18 +79,18 @@ class TransportForm(BackendSettingsMixin, forms.ModelForm): backend_name = self.data.get("service") backend_settings = backends.manager.get_backend_settings(backend_name) for name, field in self.fields.items(): - if name.startswith("{}_".format(backend_name)): - name = name.replace("{}_".format(backend_name), "") + if name.startswith(f"{backend_name}_"): + name = name.replace(f"{backend_name}_", "") for setting in backend_settings: if setting["name"] == name: break if setting.get("required", True): field.required = True - return super(TransportForm, self)._clean_fields() + return super()._clean_fields() def clean(self): """Check values.""" - cleaned_data = super(TransportForm, self).clean() + cleaned_data = super().clean() if self.errors: return cleaned_data self.clean_backend_fields(cleaned_data["service"]) diff --git a/modoboa/transport/postfix_maps.py b/modoboa/transport/postfix_maps.py index 35cb3caa8..66e2e3c3e 100644 --- a/modoboa/transport/postfix_maps.py +++ b/modoboa/transport/postfix_maps.py @@ -1,7 +1,7 @@ """Map file definitions for postfix.""" -class TransportMap(object): +class TransportMap: """A transport map.""" filename = "sql-transport.cf" diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 84b99bf45..000000000 --- a/pylintrc +++ /dev/null @@ -1,639 +0,0 @@ -[MAIN] - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Clear in-memory caches upon conclusion of linting. Useful if running pylint -# in a server-like mode. -clear-cache-post-run=no - -# Load and enable all available extensions. Use --list-extensions to see a list -# all available extensions. -#enable-all-extensions= - -# In error mode, messages with a category besides ERROR or FATAL are -# suppressed, and no reports are done by default. Error mode is compatible with -# disabling specific errors. -#errors-only= - -# Always return a 0 (non-error) status code, even if lint errors are found. -# This is primarily useful in continuous integration scripts. -#exit-zero= - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-allow-list= - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. (This is an alternative name to extension-pkg-allow-list -# for backward compatibility.) -extension-pkg-whitelist= - -# Return non-zero exit code if any of these messages/categories are detected, -# even if score is above --fail-under value. Syntax same as enable. Messages -# specified are enabled, while categories only check already-enabled messages. -fail-on= - -# Specify a score threshold under which the program will exit with error. -fail-under=10 - -# Interpret the stdin as a python script, whose filename needs to be passed as -# the module_or_package argument. -#from-stdin= - -# Files or directories to be skipped. They should be base names, not paths. -ignore=CVS,migrations - -# Add files or directories matching the regular expressions patterns to the -# ignore-list. The regex matches against paths and can be in Posix or Windows -# format. Because '\\' represents the directory delimiter on Windows systems, -# it can't be used as an escape character. -ignore-paths= - -# Files or directories matching the regular expression patterns are skipped. -# The regex matches against base names, not paths. The default value ignores -# Emacs file locks -ignore-patterns=^\.# - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use, and will cap the count on Windows to -# avoid hangs. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Minimum Python version to use for version dependent checks. Will default to -# the version used to run pylint. -py-version=3.11 - -# Discover python modules and packages in the file system subtree. -recursive=no - -# Add paths to the list of the source roots. Supports globbing patterns. The -# source root is an absolute path or a path relative to the current working -# directory used to determine a package namespace for modules located under the -# source root. -source-roots= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# In verbose mode, extra non-checker-related info will be displayed. -#verbose= - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. If left empty, argument names will be checked with the set -# naming style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. If left empty, attribute names will be checked with the set naming -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. If left empty, class attribute names will be checked -# with the set naming style. -#class-attribute-rgx= - -# Naming style matching correct class constant names. -class-const-naming-style=UPPER_CASE - -# Regular expression matching correct class constant names. Overrides class- -# const-naming-style. If left empty, class constant names will be checked with -# the set naming style. -#class-const-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. If left empty, class names will be checked with the set naming style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. If left empty, constant names will be checked with the set naming -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. If left empty, function names will be checked with the set -# naming style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. If left empty, inline iteration names will be checked -# with the set naming style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. If left empty, method names will be checked with the set naming style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. If left empty, module names will be checked with the set naming style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Regular expression matching correct type alias names. If left empty, type -# alias names will be checked with the set naming style. -#typealias-rgx= - -# Regular expression matching correct type variable names. If left empty, type -# variable names will be checked with the set naming style. -#typevar-rgx= - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. If left empty, variable names will be checked with the set -# naming style. -#variable-rgx= - - -[CLASSES] - -# Warn about protected attribute access inside special methods -check-protected-access-in-special-methods=no - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - asyncSetUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[DESIGN] - -# List of regular expressions of class ancestor names to ignore when counting -# public methods (see R0903) -exclude-too-few-public-methods= - -# List of qualified class names to ignore when counting class parents (see -# R0901) -ignored-parents= - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when caught. -overgeneral-exceptions=builtins.BaseException,builtins.Exception - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow explicit reexports by alias from a package __init__. -allow-reexport-from-package=no - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules= - -# Output a graph (.gv or any supported image format) of external dependencies -# to the given file (report RP0402 must not be disabled). -ext-import-graph= - -# Output a graph (.gv or any supported image format) of all (i.e. internal and -# external) dependencies to the given file (report RP0402 must not be -# disabled). -import-graph= - -# Output a graph (.gv or any supported image format) of internal dependencies -# to the given file (report RP0402 must not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, -# UNDEFINED. -confidence=HIGH, - CONTROL_FLOW, - INFERENCE, - INFERENCE_FAILURE, - UNDEFINED - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then re-enable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - no-member, - locally-disabled, - too-few-public-methods, - too-many-public-methods, - too-many-ancestors, - line-too-long, - missing-class-docstring, - missing-function-docstring - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[METHOD_ARGS] - -# List of qualified names (i.e., library.method) which require a timeout -# parameter e.g. 'requests.api.get,requests.api.post' -timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -notes-rgx= - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit,argparse.parse_error - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'fatal', 'error', 'warning', 'refactor', -# 'convention', and 'info' which contain the number of messages in each -# category, as well as 'statement' which is the total number of statements -# analyzed. This score is used by the global evaluation report (RP0004). -evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -#output-format= - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[SIMILARITIES] - -# Comments are removed from the similarity computation -ignore-comments=yes - -# Docstrings are removed from the similarity computation -ignore-docstrings=yes - -# Imports are removed from the similarity computation -ignore-imports=yes - -# Signatures are removed from the similarity computation -ignore-signatures=yes - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. No available dictionaries : You need to install -# both the python package and the system dependency for enchant to work.. -spelling-dict= - -# List of comma separated words that should be considered directives if they -# appear at the beginning of a comment and should not be checked. -spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of symbolic message names to ignore for Mixin members. -ignored-checks-for-mixins=no-member, - not-async-context-manager, - not-context-manager, - attribute-defined-outside-init - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# Regex pattern to define which classes are considered mixins. -mixin-class-rgx=.*[Mm]ixin - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of names allowed to shadow builtins -allowed-redefined-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/pyproject.toml b/pyproject.toml index e46718e1e..203d00998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,10 +19,10 @@ classifiers = [ "License :: OSI Approved :: ISC License (ISCL)", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Communications :: Email", "Topic :: Internet :: WWW/HTTP", ] @@ -87,7 +87,7 @@ dev = [ "django-debug-toolbar", "pre-commit", "black", - "pylint", + "ruff", "django-extensions", "Werkzeug", "pyOpenSSL" @@ -115,3 +115,9 @@ Issues = "https://github.com/modoboa/modoboa/issues" [tool.setuptools.dynamic] version = {attr = "modoboa.version.get_version"} + +[tool.ruff] +exclude = ["migrations"] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "B", "Q", "UP"]