First pass of Type Annotations

This commit is contained in:
Martin Mahner 2020-06-03 19:01:14 +02:00
parent 08f7d9c27f
commit cf3ad14795
12 changed files with 204 additions and 123 deletions

View file

@ -1,4 +1,7 @@
from typing import Dict, List, Optional, Tuple, Type, Union
from django.apps import AppConfig, apps
from django.core.handlers.wsgi import WSGIRequest
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
@ -118,7 +121,7 @@ class dpasteAppConfig(AppConfig):
PLAIN_CODE_SYMBOL = "_code"
@property
def TEXT_FORMATTER(self):
def TEXT_FORMATTER(self,) -> List[Tuple[str, str, Type[object]]]:
"""
Choices list with all "Text" lexer. Prepend keys with an underscore
so they don't accidentally clash with a Pygments Lexer name.
@ -130,6 +133,8 @@ class dpasteAppConfig(AppConfig):
Lexer Highlight Class)
If the Highlight Class is not given, PygmentsHighlighter is used.
@FIXME: Make `Type[object]` use the Type[Highlighter] class.
"""
from dpaste.highlight import (
PlainTextHighlighter,
@ -144,7 +149,9 @@ class dpasteAppConfig(AppConfig):
]
@property
def CODE_FORMATTER(self):
def CODE_FORMATTER(
self,
) -> List[Union[Tuple[str, str, Type[object]], Tuple[str, str]]]:
"""
Choices list with all "Code" Lexer. Each list
contains a lexer tuple of:
@ -621,7 +628,7 @@ class dpasteAppConfig(AppConfig):
CACHE_TIMEOUT = 60 * 10
@staticmethod
def get_base_url(request=None):
def get_base_url(request: Optional[WSGIRequest] = None) -> str:
"""
String. The full qualified hostname and path to the dpaste instance.
This is used to generate a link in the API response. If the "Sites"
@ -637,7 +644,7 @@ class dpasteAppConfig(AppConfig):
return "https://dpaste-base-url.example.org"
@property
def extra_template_context(self):
def extra_template_context(self) -> Dict[str, str]:
"""
Returns a dictionary with context variables which are passed to
all Template Views.

View file

@ -1,7 +1,9 @@
import datetime
from datetime import datetime, timedelta
from typing import Dict, Optional, Tuple, Type, Union
from django import forms
from django.apps import apps
from django.core.handlers.wsgi import WSGIRequest
from django.utils.translation import gettext_lazy as _
from .highlight import LEXER_CHOICES, LEXER_DEFAULT, LEXER_KEYS
@ -10,7 +12,9 @@ from .models import Snippet
config = apps.get_app_config("dpaste")
def get_expire_values(expires):
def get_expire_values(
expires: str,
) -> Union[Tuple[None, int], Tuple[datetime, int]]:
if expires == "never":
expire_type = Snippet.EXPIRE_KEEP
expires = None
@ -20,9 +24,7 @@ def get_expire_values(expires):
else:
expire_type = Snippet.EXPIRE_TIME
expires = expires and expires or config.EXPIRE_DEFAULT
expires = datetime.datetime.now() + datetime.timedelta(
seconds=int(expires)
)
expires = datetime.now() + timedelta(seconds=int(expires))
return expires, expire_type
@ -59,7 +61,7 @@ class SnippetForm(forms.ModelForm):
model = Snippet
fields = ("content", "lexer", "rtl")
def __init__(self, request, *args, **kwargs):
def __init__(self, request: WSGIRequest, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.request = request
@ -72,13 +74,13 @@ class SnippetForm(forms.ModelForm):
if "l" in request.GET and request.GET["l"] in LEXER_KEYS:
self.fields["lexer"].initial = request.GET["l"]
def clean_content(self):
def clean_content(self) -> str:
content = self.cleaned_data.get("content", "")
if not content.strip():
raise forms.ValidationError(_("This field is required."))
return content
def clean_expires(self):
def clean_expires(self) -> Optional[datetime]:
"""
Extract the 'expire_type' from the choice of expire choices.
"""
@ -87,7 +89,7 @@ class SnippetForm(forms.ModelForm):
self.cleaned_data["expire_type"] = expire_type
return expires
def clean(self):
def clean(self) -> Dict[str, Optional[Union[Type[object], None]]]:
"""
The `title` field is a hidden honeypot field. If its filled,
this is likely spam.
@ -96,7 +98,9 @@ class SnippetForm(forms.ModelForm):
raise forms.ValidationError("This snippet was identified as Spam.")
return self.cleaned_data
def save(self, parent=None, *args, **kwargs):
def save(
self, parent: Optional[Snippet] = None, *args, **kwargs
) -> Snippet:
# Set parent snippet
self.instance.parent = parent

View file

@ -1,9 +1,11 @@
from io import StringIO
from logging import getLogger
from typing import Any, Iterator, Optional, Type
from django.apps import apps
from django.template.defaultfilters import escape, linebreaksbr
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe
from django.utils.safestring import SafeString, mark_safe
from django.utils.translation import gettext_lazy as _
from pygments import highlight
from pygments.formatters.html import HtmlFormatter
@ -34,7 +36,13 @@ class Highlighter(object):
return l[1]
return fallback
def render(self, code_string, lexer_name, direction=None, **kwargs):
def render(
self,
code_string: str,
lexer_name: str,
direction: Optional[str] = None,
**kwargs
) -> SafeString:
highlighted_string = self.highlight(code_string, lexer_name=lexer_name)
context = {
"highlighted": highlighted_string,
@ -52,7 +60,7 @@ class PlainTextHighlighter(Highlighter):
template_name = "dpaste/highlight/text.html"
def highlight(self, code_string, **kwargs):
def highlight(self, code_string: str, **kwargs) -> SafeString:
return linebreaksbr(code_string)
@ -99,7 +107,7 @@ class RestructuredTextHighlighter(PlainTextHighlighter):
},
}
def highlight(self, code_string, **kwargs):
def highlight(self, code_string: str, **kwargs) -> SafeString:
from docutils.core import publish_parts
self.publish_args["source"] = code_string
@ -113,10 +121,10 @@ class RestructuredTextHighlighter(PlainTextHighlighter):
class NakedHtmlFormatter(HtmlFormatter):
"""Pygments HTML formatter with no further HTML tags."""
def wrap(self, source, outfile):
def wrap(self, source: Iterator[Any], outfile: StringIO) -> Iterator[Any]:
return self._wrap_code(source)
def _wrap_code(self, source):
def _wrap_code(self, source: Iterator[Any]) -> None:
yield from source
@ -125,7 +133,7 @@ class PlainCodeHighlighter(Highlighter):
Plain Code. No highlighting but Pygments like span tags around each line.
"""
def highlight(self, code_string, **kwargs):
def highlight(self, code_string: str, **kwargs) -> str:
return "\n".join(
[
'<span class="plain">{}</span>'.format(escape(l) or "&#8203;")
@ -144,7 +152,7 @@ class PygmentsHighlighter(Highlighter):
lexer = None
lexer_fallback = PythonLexer()
def highlight(self, code_string, lexer_name):
def highlight(self, code_string: str, lexer_name: str) -> str:
if not self.lexer:
try:
self.lexer = get_lexer_by_name(lexer_name)
@ -155,18 +163,7 @@ class PygmentsHighlighter(Highlighter):
return highlight(code_string, self.lexer, self.formatter)
class SolidityHighlighter(PygmentsHighlighter):
"""Solidity Specific Highlighter. This uses a 3rd party Pygments lexer."""
def __init__(self):
# SolidityLexer does not necessarily need to be installed
# since its imported here and not used later.
from pygments_lexer_solidity import SolidityLexer
self.lexer = SolidityLexer()
def get_highlighter_class(lexer_name):
def get_highlighter_class(lexer_name: str) -> Type[Highlighter]:
"""
Get Highlighter for lexer name.

View file

@ -5,31 +5,78 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='Snippet',
name="Snippet",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('secret_id', models.CharField(max_length=255, unique=True, null=True, verbose_name='Secret ID', blank=True)),
('content', models.TextField(verbose_name='Content')),
('lexer', models.CharField(default=b'python', max_length=30, verbose_name='Lexer')),
('published', models.DateTimeField(auto_now_add=True, verbose_name='Published')),
('expire_type', models.PositiveSmallIntegerField(default=1, verbose_name='Expire Type', choices=[(1, 'Expire by timestamp'), (2, 'Keep Forever'), (3, 'One-Time snippet')])),
('expires', models.DateTimeField(null=True, verbose_name='Expires', blank=True)),
('view_count', models.PositiveIntegerField(default=0, verbose_name='View count')),
('lft', models.PositiveIntegerField(editable=False, db_index=True)),
('rght', models.PositiveIntegerField(editable=False, db_index=True)),
('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
('level', models.PositiveIntegerField(editable=False, db_index=True)),
('parent', models.ForeignKey(related_name='children', blank=True, to='dpaste.Snippet', null=True, on_delete=models.CASCADE)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"secret_id",
models.CharField(
max_length=255,
unique=True,
null=True,
verbose_name="Secret ID",
blank=True,
),
),
("content", models.TextField(verbose_name="Content")),
(
"lexer",
models.CharField(
default=b"python", max_length=30, verbose_name="Lexer"
),
),
(
"published",
models.DateTimeField(auto_now_add=True, verbose_name="Published"),
),
(
"expire_type",
models.PositiveSmallIntegerField(
default=1,
verbose_name="Expire Type",
choices=[
(1, "Expire by timestamp"),
(2, "Keep Forever"),
(3, "One-Time snippet"),
],
),
),
(
"expires",
models.DateTimeField(null=True, verbose_name="Expires", blank=True),
),
(
"view_count",
models.PositiveIntegerField(default=0, verbose_name="View count"),
),
("lft", models.PositiveIntegerField(editable=False, db_index=True)),
("rght", models.PositiveIntegerField(editable=False, db_index=True)),
("tree_id", models.PositiveIntegerField(editable=False, db_index=True)),
("level", models.PositiveIntegerField(editable=False, db_index=True)),
(
"parent",
models.ForeignKey(
related_name="children",
blank=True,
to="dpaste.Snippet",
null=True,
on_delete=models.CASCADE,
),
),
],
options={
'ordering': ('-published',),
'db_table': 'dpaste_snippet',
},
options={"ordering": ("-published",), "db_table": "dpaste_snippet",},
bases=(models.Model,),
),
]

View file

@ -7,24 +7,12 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dpaste', '0001_initial'),
("dpaste", "0001_initial"),
]
operations = [
migrations.RemoveField(
model_name='snippet',
name='level',
),
migrations.RemoveField(
model_name='snippet',
name='lft',
),
migrations.RemoveField(
model_name='snippet',
name='rght',
),
migrations.RemoveField(
model_name='snippet',
name='tree_id',
),
migrations.RemoveField(model_name="snippet", name="level",),
migrations.RemoveField(model_name="snippet", name="lft",),
migrations.RemoveField(model_name="snippet", name="rght",),
migrations.RemoveField(model_name="snippet", name="tree_id",),
]

View file

@ -7,14 +7,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dpaste', '0002_auto_20170119_1038'),
("dpaste", "0002_auto_20170119_1038"),
]
operations = [
migrations.AddField(
model_name='snippet',
name='highlighted',
field=models.TextField(default='', verbose_name='Highlighted Content'),
model_name="snippet",
name="highlighted",
field=models.TextField(default="", verbose_name="Highlighted Content"),
preserve_default=False,
),
]

View file

@ -7,13 +7,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dpaste', '0003_snippet_highlighted'),
("dpaste", "0003_snippet_highlighted"),
]
operations = [
migrations.AlterField(
model_name='snippet',
name='lexer',
field=models.CharField(default='python', max_length=30, verbose_name='Lexer'),
model_name="snippet",
name="lexer",
field=models.CharField(
default="python", max_length=30, verbose_name="Lexer"
),
),
]

View file

@ -6,12 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dpaste', '0004_auto_20180107_1603'),
("dpaste", "0004_auto_20180107_1603"),
]
operations = [
migrations.RemoveField(
model_name='snippet',
name='highlighted',
),
migrations.RemoveField(model_name="snippet", name="highlighted",),
]

View file

@ -7,13 +7,20 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dpaste', '0005_remove_snippet_highlighted'),
("dpaste", "0005_remove_snippet_highlighted"),
]
operations = [
migrations.AlterField(
model_name='snippet',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dpaste.Snippet', verbose_name='Parent Snippet'),
model_name="snippet",
name="parent",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="children",
to="dpaste.Snippet",
verbose_name="Parent Snippet",
),
),
]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dpaste', '0006_auto_20180622_1051'),
("dpaste", "0006_auto_20180622_1051"),
]
operations = [
migrations.AddField(
model_name='snippet',
name='rtl',
field=models.BooleanField(default=False, verbose_name='Right-to-left'),
model_name="snippet",
name="rtl",
field=models.BooleanField(default=False, verbose_name="Right-to-left"),
),
]

View file

@ -4,6 +4,7 @@ from random import SystemRandom
from django.apps import apps
from django.db import models
from django.urls import reverse
from django.utils.safestring import SafeString
from django.utils.translation import gettext_lazy as _
from dpaste import highlight
@ -13,7 +14,7 @@ logger = getLogger(__file__)
R = SystemRandom()
def generate_secret_id(length):
def generate_secret_id(length: int) -> str:
if length > config.SLUG_LENGTH:
logger.warning(
"Slug creation triggered a duplicate, "
@ -75,18 +76,18 @@ class Snippet(models.Model):
ordering = ("-published",)
db_table = "dpaste_snippet"
def __str__(self):
def __str__(self) -> str:
return self.secret_id
def save(self, *args, **kwargs):
def save(self, *args, **kwargs) -> None:
if not self.secret_id:
self.secret_id = generate_secret_id(length=config.SLUG_LENGTH)
super().save(*args, **kwargs)
def get_absolute_url(self):
def get_absolute_url(self) -> str:
return reverse("snippet_details", kwargs={"snippet_id": self.secret_id})
def highlight(self):
def highlight(self) -> SafeString:
HighlighterClass = highlight.get_highlighter_class(self.lexer)
return HighlighterClass().render(
code_string=self.content,
@ -95,12 +96,12 @@ class Snippet(models.Model):
)
@property
def lexer_name(self):
def lexer_name(self) -> str:
"""Display name for this lexer."""
return highlight.Highlighter.get_lexer_display_name(self.lexer)
@property
def remaining_views(self):
def remaining_views(self) -> int:
if self.expire_type == self.EXPIRE_ONETIME:
remaining = config.ONETIME_LIMIT - self.view_count
return remaining > 0 and remaining or 0

View file

@ -1,8 +1,11 @@
import datetime
import difflib
import json
from typing import Any, Dict, Optional, Union
from django.apps import apps
from django.core.handlers.wsgi import WSGIRequest
from django.db.models.query import QuerySet
from django.http import (
Http404,
HttpResponse,
@ -11,9 +14,11 @@ from django.http import (
HttpResponseRedirect,
)
from django.shortcuts import get_object_or_404, render
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.cache import add_never_cache_headers, patch_cache_control
from django.utils.datastructures import MultiValueDict
from django.utils.http import http_date
from django.utils.translation import ugettext
from django.views.generic import FormView
@ -23,6 +28,7 @@ from pygments.lexers import get_lexer_for_filename
from pygments.util import ClassNotFound
from dpaste import highlight
from dpaste.apps import dpasteAppConfig
from dpaste.forms import SnippetForm, get_expire_values
from dpaste.highlight import PygmentsHighlighter
from dpaste.models import Snippet
@ -43,22 +49,26 @@ class SnippetView(FormView):
form_class = SnippetForm
template_name = "dpaste/new.html"
def get(self, request, *args, **kwargs):
def get(self, request: WSGIRequest, *args, **kwargs) -> TemplateResponse:
response = super().get(request, *args, **kwargs)
if config.CACHE_HEADER:
patch_cache_control(response, max_age=config.CACHE_TIMEOUT)
return response
def get_form_kwargs(self):
def get_form_kwargs(
self,
) -> Dict[str, Optional[Union[MultiValueDict, WSGIRequest]]]:
kwargs = super().get_form_kwargs()
kwargs.update({"request": self.request})
return kwargs
def form_valid(self, form):
def form_valid(self, form: SnippetForm) -> HttpResponseRedirect:
snippet = form.save()
return HttpResponseRedirect(snippet.get_absolute_url())
def get_context_data(self, **kwargs):
def get_context_data(
self, **kwargs
) -> Dict[str, Union[SnippetForm, "SnippetView", str]]:
ctx = super().get_context_data(**kwargs)
ctx.update(config.extra_template_context)
return ctx
@ -76,7 +86,9 @@ class SnippetDetailView(DetailView, FormView):
slug_url_kwarg = "snippet_id"
slug_field = "secret_id"
def post(self, request, *args, **kwargs):
def post(
self, request: WSGIRequest, *args, **kwargs
) -> Union[HttpResponseRedirect, TemplateResponse]:
"""
Delete a snippet. This is allowed by anybody as long as he knows the
snippet id. I got too many manual requests to do this, mostly for legal
@ -95,7 +107,7 @@ class SnippetDetailView(DetailView, FormView):
return super().post(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
def get(self, request: WSGIRequest, *args, **kwargs) -> HttpResponse:
snippet = self.get_object()
# One-Time snippet get deleted if the view count matches our limit
@ -126,7 +138,7 @@ class SnippetDetailView(DetailView, FormView):
return response
def get_initial(self):
def get_initial(self) -> Dict[str, Union[str, bool]]:
snippet = self.get_object()
return {
"content": snippet.content,
@ -134,16 +146,23 @@ class SnippetDetailView(DetailView, FormView):
"rtl": snippet.rtl,
}
def get_form_kwargs(self):
def get_form_kwargs(
self,
) -> Dict[
str,
Optional[
Union[Dict[str, Union[str, bool]], MultiValueDict, WSGIRequest]
],
]:
kwargs = super().get_form_kwargs()
kwargs.update({"request": self.request})
return kwargs
def form_valid(self, form):
def form_valid(self, form: SnippetForm) -> HttpResponseRedirect:
snippet = form.save(parent=self.get_object())
return HttpResponseRedirect(snippet.get_absolute_url())
def get_snippet_diff(self):
def get_snippet_diff(self) -> None:
snippet = self.get_object()
if not snippet.parent_id:
@ -165,7 +184,7 @@ class SnippetDetailView(DetailView, FormView):
# Remove blank lines
return highlighted
def get_context_data(self, **kwargs):
def get_context_data(self, **kwargs) -> Dict[str, Any]:
self.object = self.get_object()
ctx = super().get_context_data(**kwargs)
@ -187,26 +206,30 @@ class SnippetRawView(SnippetDetailView):
template_name = "dpaste/raw.html"
def dispatch(self, request, *args, **kwargs):
def dispatch(self, request: WSGIRequest, *args, **kwargs) -> HttpResponse:
if not config.RAW_MODE_ENABLED:
return HttpResponseForbidden(
ugettext("This dpaste installation has Raw view mode disabled.")
)
return super().dispatch(request, *args, **kwargs)
def render_plain_text(self, context, **response_kwargs):
def render_plain_text(
self, context: dpasteAppConfig, **response_kwargs
) -> HttpResponse:
snippet = self.get_object()
response = HttpResponse(snippet.content)
response["Content-Type"] = "text/plain;charset=UTF-8"
response["X-Content-Type-Options"] = "nosniff"
return response
def render_to_response(self, context, **response_kwargs):
def render_to_response(
self, context: Dict[str, Any], **response_kwargs
) -> HttpResponse:
if config.RAW_MODE_PLAIN_TEXT:
return self.render_plain_text(config, **response_kwargs)
return super().render_to_response(context, **response_kwargs)
def get_context_data(self, **kwargs):
def get_context_data(self, **kwargs) -> Dict[str, Any]:
ctx = super().get_context_data(**kwargs)
ctx.update(config.extra_template_context)
return ctx
@ -220,16 +243,18 @@ class SnippetHistory(TemplateView):
template_name = "dpaste/history.html"
def get_user_snippets(self):
def get_user_snippets(self) -> QuerySet:
snippet_id_list = self.request.session.get("snippet_list", [])
return Snippet.objects.filter(pk__in=snippet_id_list)
def get(self, request, *args, **kwargs):
def get(self, request: WSGIRequest, *args, **kwargs) -> TemplateResponse:
response = super().get(request, *args, **kwargs)
add_never_cache_headers(response)
return response
def post(self, request, *args, **kwargs):
def post(
self, request: WSGIRequest, *args, **kwargs
) -> HttpResponseRedirect:
"""
Delete all user snippets at once.
"""
@ -240,7 +265,9 @@ class SnippetHistory(TemplateView):
url = "{0}#".format(reverse("snippet_history"))
return HttpResponseRedirect(url)
def get_context_data(self, **kwargs):
def get_context_data(
self, **kwargs
) -> Dict[str, Union["SnippetHistory", QuerySet, str]]:
ctx = super().get_context_data(**kwargs)
ctx.update({"snippet_list": self.get_user_snippets()})
ctx.update(config.extra_template_context)
@ -257,14 +284,14 @@ class APIView(View):
API View
"""
def _format_default(self, s):
def _format_default(self, s: Snippet) -> str:
"""
The default response is the snippet URL wrapped in quotes.
"""
base_url = config.get_base_url(request=self.request)
return f'"{base_url}{s.get_absolute_url()}"'
def _format_url(self, s):
def _format_url(self, s: Snippet) -> str:
"""
The `url` format returns the snippet URL,
no quotes, but a linebreak at the end.
@ -272,7 +299,7 @@ class APIView(View):
base_url = config.get_base_url(request=self.request)
return f"{base_url}{s.get_absolute_url()}\n"
def _format_json(self, s):
def _format_json(self, s: Snippet) -> str:
"""
The `json` format export.
"""
@ -285,7 +312,7 @@ class APIView(View):
}
)
def post(self, request, *args, **kwargs):
def post(self, request: WSGIRequest, *args, **kwargs) -> HttpResponse:
content = request.POST.get("content", "")
lexer = request.POST.get("lexer", highlight.LEXER_DEFAULT).strip()
filename = request.POST.get("filename", "").strip()
@ -357,7 +384,11 @@ class APIView(View):
# -----------------------------------------------------------------------------
def page_not_found(request, exception=None, template_name="dpaste/404.html"):
def page_not_found(
request: WSGIRequest,
exception: Optional[Http404] = None,
template_name: str = "dpaste/404.html",
) -> HttpResponse:
context = {}
context.update(config.extra_template_context)
response = render(request, template_name, context, status=404)