Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"open-close file" logic to "with open" and type hints #230

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vscode
dist
build
*.pyc
Expand Down
37 changes: 23 additions & 14 deletions captcha/fields.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from decimal import Decimal
from typing import Any, Mapping
from django.core.exceptions import ImproperlyConfigured
from django.forms import ValidationError
from django.forms.fields import CharField, MultiValueField
Expand Down Expand Up @@ -41,16 +43,16 @@ class BaseCaptchaTextInput(MultiWidget):
Base class for Captcha widgets
"""

def __init__(self, attrs=None):
def __init__(self, attrs=None) -> None:
widgets = (CaptchaHiddenInput(attrs), CaptchaAnswerInput(attrs))
super().__init__(widgets, attrs)

def decompress(self, value):
def decompress(self, value) -> list[str | None]:
if value:
return value.split(",")
return [None, None]

def fetch_captcha_store(self, name, value, attrs=None, generator=None):
def fetch_captcha_store(self, name, value, attrs=None, generator=None) -> None:
"""
Fetches a new CaptchaStore
This has to be called inside render
Expand All @@ -72,22 +74,22 @@ def fetch_captcha_store(self, name, value, attrs=None, generator=None):
self._key = key
self.id_ = self.build_attrs(attrs).get("id", None)

def id_for_label(self, id_):
def id_for_label(self, id_: str) -> str:
if id_:
return id_ + "_1"
return id_

def image_url(self):
def image_url(self) -> str:
return reverse("captcha-image", kwargs={"key": self._key})

def audio_url(self):
def audio_url(self) -> str | None:
return (
reverse("captcha-audio", kwargs={"key": self._key})
if settings.CAPTCHA_FLITE_PATH
else None
)

def refresh_url(self):
def refresh_url(self) -> str:
return reverse("captcha-refresh")


Expand All @@ -105,21 +107,23 @@ def __init__(
self.generator = generator
super().__init__(attrs)

def build_attrs(self, *args, **kwargs):
ret = super().build_attrs(*args, **kwargs)
def build_attrs(self, *args, **kwargs) -> dict[str, str | float | Decimal]:
ret: dict[str, str | float | Decimal] = super().build_attrs(*args, **kwargs)

if self.id_prefix and "id" in ret:
ret["id"] = "%s_%s" % (self.id_prefix, ret["id"])

return ret

def id_for_label(self, id_):
ret = super().id_for_label(id_)
def id_for_label(self, id_: str) -> str:
ret: str = super().id_for_label(id_)
if self.id_prefix and "id" in ret:
ret = "%s_%s" % (self.id_prefix, ret)
return ret

def get_context(self, name, value, attrs):
def get_context(self, name, value, attrs) -> dict[str, Any]:
"""Add captcha specific variables to context."""
context = super().get_context(name, value, attrs)
context: dict[str, Any] = super().get_context(name, value, attrs)
context["image"] = self.image_url()
context["audio"] = self.audio_url()
return context
Expand All @@ -134,7 +138,7 @@ def format_output(self, rendered_widgets):
}
return ret

def render(self, name, value, attrs=None, renderer=None):
def render(self, name, value, attrs=None, renderer=None) -> str:
self.fetch_captcha_store(name, value, attrs, self.generator)

extra_kwargs = {}
Expand Down Expand Up @@ -173,8 +177,10 @@ def compress(self, data_list):
def clean(self, value):
super().clean(value)
response, value[1] = (value[1] or "").strip().lower(), ""

if not settings.CAPTCHA_GET_FROM_POOL:
CaptchaStore.remove_expired()

if settings.CAPTCHA_TEST_MODE and response.lower() == "passed":
# automatically pass the test
try:
Expand All @@ -183,8 +189,10 @@ def clean(self, value):
except CaptchaStore.DoesNotExist:
# ignore errors
pass

elif not self.required and not response:
pass

else:
try:
CaptchaStore.objects.get(
Expand All @@ -196,4 +204,5 @@ def clean(self, value):
"invalid", gettext_lazy("Invalid CAPTCHA")
)
)

return value
58 changes: 34 additions & 24 deletions captcha/helpers.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,64 @@
import random
from typing import Literal

from django.urls import reverse
from PIL.Image import Image as ImageType

from captcha.conf import settings


def math_challenge():
def math_challenge() -> tuple[str, str]:
operators = ("+", "*", "-")
operands = (random.randint(1, 10), random.randint(1, 10))
operator = random.choice(operators)
operands: tuple[int, int] = (random.randint(1, 10), random.randint(1, 10))
operator: Literal["+", "*", "-"] = random.choice(operators)

if operands[0] < operands[1] and "-" == operator:
operands = (operands[1], operands[0])

challenge = "%d%s%d" % (operands[0], operator, operands[1])

return (
"{}=".format(challenge.replace("*", settings.CAPTCHA_MATH_CHALLENGE_OPERATOR)),
str(eval(challenge)),
)


def random_char_challenge():
def random_char_challenge() -> tuple[str, str]:
chars, ret = "abcdefghijklmnopqrstuvwxyz", ""
for i in range(settings.CAPTCHA_LENGTH):
for _ in range(settings.CAPTCHA_LENGTH):
ret += random.choice(chars)
return ret.upper(), ret


def unicode_challenge():
def unicode_challenge() -> tuple[str, str]:
chars, ret = "äàáëéèïíîöóòüúù", ""
for i in range(settings.CAPTCHA_LENGTH):
ret += random.choice(chars)
return ret.upper(), ret


def word_challenge():
fd = open(settings.CAPTCHA_WORDS_DICTIONARY, "r")
lines = fd.readlines()
fd.close()
def word_challenge() -> tuple[str, str]:
with open(settings.CAPTCHA_WORDS_DICTIONARY, "r") as fd:
lines = fd.readlines()

while True:
word = random.choice(lines).strip()
word: str = random.choice(lines).strip()
if (
len(word) >= settings.CAPTCHA_DICTIONARY_MIN_LENGTH
and len(word) <= settings.CAPTCHA_DICTIONARY_MAX_LENGTH
):
break

return word.upper(), word.lower()


def huge_words_and_punctuation_challenge():
def huge_words_and_punctuation_challenge() -> tuple[str, str]:
"Yay, undocumneted. Mostly used to test Issue 39 - http://code.google.com/p/django-simple-captcha/issues/detail?id=39"
fd = open(settings.CAPTCHA_WORDS_DICTIONARY, "rb")
lines = fd.readlines()
fd.close()
with open(settings.CAPTCHA_WORDS_DICTIONARY, "rb") as fd:
lines = fd.readlines()

word = ""

while True:
word1 = random.choice(lines).strip()
word2 = random.choice(lines).strip()
Expand All @@ -62,11 +69,12 @@ def huge_words_and_punctuation_challenge():
and len(word) <= settings.CAPTCHA_DICTIONARY_MAX_LENGTH
):
break

return word.upper(), word.lower()


def noise_arcs(draw, image):
size = image.size
def noise_arcs(draw, image: ImageType):
size: tuple[int, int] = image.size
draw.arc([-20, -20, size[0], 20], 0, 295, fill=settings.CAPTCHA_FOREGROUND_COLOR)
draw.line(
[-20, 20, size[0] + 20, size[1] - 20], fill=settings.CAPTCHA_FOREGROUND_COLOR
Expand All @@ -75,31 +83,33 @@ def noise_arcs(draw, image):
return draw


def noise_dots(draw, image):
size = image.size
for p in range(int(size[0] * size[1] * 0.1)):
def noise_dots(draw, image: ImageType):
size: tuple[int, int] = image.size

for _ in range(int(size[0] * size[1] * 0.1)):
draw.point(
(random.randint(0, size[0]), random.randint(0, size[1])),
fill=settings.CAPTCHA_FOREGROUND_COLOR,
)

return draw


def noise_null(draw, image):
def noise_null(draw, image: ImageType):
return draw


def post_smooth(image):
def post_smooth(image: ImageType) -> ImageType:
from PIL import ImageFilter

return image.filter(ImageFilter.SMOOTH)


def captcha_image_url(key):
def captcha_image_url(key) -> str:
"""Return url to image. Need for ajax refresh and, etc"""
return reverse("captcha-image", args=[key])


def captcha_audio_url(key):
def captcha_audio_url(key) -> str:
"""Return url to image. Need for ajax refresh and, etc"""
return reverse("captcha-audio", args=[key])
36 changes: 19 additions & 17 deletions captcha/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import random
import time
from typing import Callable, Final

from django.db import models
from django.utils import timezone
Expand All @@ -13,13 +14,11 @@

# Heavily based on session key generation in Django
# Use the system (hardware-based) random number generator if it exists.
if hasattr(random, "SystemRandom"):
randrange = random.SystemRandom().randrange
else:
randrange = random.randrange
MAX_RANDOM_KEY = 18446744073709551616 # 2 << 63
randrange: Callable = random.SystemRandom().randrange if hasattr(random, "SystemRandom") else random.randrange

logger = logging.getLogger(__name__)
MAX_RANDOM_KEY: Final[int] = 18446744073709551616 # 2 << 63

logger: logging.Logger = logging.getLogger(__name__)


class CaptchaStore(models.Model):
Expand All @@ -29,40 +28,43 @@ class CaptchaStore(models.Model):
hashkey = models.CharField(blank=False, max_length=40, unique=True)
expiration = models.DateTimeField(blank=False)

def save(self, *args, **kwargs):
self.response = self.response.lower()
def save(self, *args, **kwargs) -> None:
self.response: models.CharField = self.response.lower()

if not self.expiration:
self.expiration = timezone.now() + datetime.timedelta(
minutes=int(captcha_settings.CAPTCHA_TIMEOUT)
)

if not self.hashkey:
key_ = (
key_: bytes = (
smart_str(randrange(0, MAX_RANDOM_KEY))
+ smart_str(time.time())
+ smart_str(self.challenge, errors="ignore")
+ smart_str(self.response, errors="ignore")
).encode("utf8")
self.hashkey = hashlib.sha1(key_).hexdigest()

self.hashkey: models.CharField = hashlib.sha1(key_).hexdigest()
del key_

super().save(*args, **kwargs)

def __str__(self):
def __str__(self) -> str:
return self.challenge

def remove_expired(cls):
@classmethod
def remove_expired(cls) -> None:
cls.objects.filter(expiration__lte=timezone.now()).delete()

remove_expired = classmethod(remove_expired)

@classmethod
def generate_key(cls, generator=None):
def generate_key(cls, generator=None) -> str:
challenge, response = captcha_settings.get_challenge(generator)()
store = cls.objects.create(challenge=challenge, response=response)

return store.hashkey

@classmethod
def pick(cls):
def pick(cls) -> str:
if not captcha_settings.CAPTCHA_GET_FROM_POOL:
return cls.generate_key()

Expand All @@ -81,7 +83,7 @@ def fallback():
return (store and store.hashkey) or fallback()

@classmethod
def create_pool(cls, count=1000):
def create_pool(cls, count: int=1000):
assert count > 0
while count > 0:
cls.generate_key()
Expand Down
4 changes: 2 additions & 2 deletions captcha/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from django.urls import re_path
from django.urls import re_path, URLPattern

from captcha import views


urlpatterns = [
urlpatterns: list[URLPattern] = [
re_path(
r"image/(?P<key>\w+)/$",
views.captcha_image,
Expand Down
Loading
Loading