From 2c6d68ef5e2cc684669563a56c416b832c801df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 27 Oct 2019 22:50:30 -0700 Subject: [PATCH 01/19] Up our mypy game. --- .travis/mypy | 63 ---------------- src/klein/__init__.py | 2 +- src/klein/_app.py | 20 +++-- src/klein/_decorators.py | 1 + src/klein/_dihttp.py | 14 ++-- src/klein/_form.py | 91 ++++++++++++++-------- src/klein/_headers.py | 7 +- src/klein/_headers_compat.py | 10 +-- src/klein/_iform.py | 2 - src/klein/_imessage.py | 2 - src/klein/_interfaces.py | 2 +- src/klein/_isession.py | 5 +- src/klein/_message.py | 3 - src/klein/_plating.py | 1 + src/klein/_request.py | 9 +-- src/klein/_request_compat.py | 9 +-- src/klein/_requirer.py | 24 +++--- src/klein/_response.py | 8 +- src/klein/_session.py | 46 ++++++----- src/klein/_tubes.py | 9 +-- src/klein/_typing.py | 12 ++- src/klein/interfaces.py | 2 +- src/klein/resource.py | 17 +++-- src/klein/storage/_memory.py | 24 +++--- src/klein/test/_strategies.py | 63 ++++++++++------ src/klein/test/_trial.py | 6 +- src/klein/test/test_app.py | 2 +- src/klein/test/test_form.py | 9 +-- src/klein/test/test_headers.py | 9 +-- src/klein/test/test_headers_compat.py | 12 +-- src/klein/test/test_memory.py | 6 +- src/klein/test/test_message.py | 9 ++- src/klein/test/test_request.py | 6 +- src/klein/test/test_request_compat.py | 8 +- src/klein/test/test_requirer.py | 25 ++++-- src/klein/test/test_resource.py | 8 +- src/klein/test/test_response.py | 6 +- src/klein/test/test_session.py | 7 +- src/klein/test/test_trial.py | 2 + tox.ini | 105 +++++++++++++++++++++++--- 40 files changed, 378 insertions(+), 288 deletions(-) delete mode 100755 .travis/mypy diff --git a/.travis/mypy b/.travis/mypy deleted file mode 100755 index 7b145957..00000000 --- a/.travis/mypy +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/sh - -# -# Work around bugs in mypy by filtering out false positive error messages. -# - -set -e -set -u - -tmp="$(mktemp -t mypy.XXXX)"; - -# -# Filter out messages from mypy that are not actual errors. -# -# Notes: -# -# * Filters applied only to specific files are things to fix in either Klein or -# in mypy. -# -# * "type alias is invalid in runtime context" is a bug in mypy: -# https://github.com/python/mypy/issues/5354 -# -# * "Method must have at least one argument" is because Zope Interface classes -# don't include 'self' in method arguments. -# -# * 'Argument "converter" to "attrib"...' is a bug in `mypy`: -# https://github.com/python/mypy/pull/6006 -# https://github.com/python-attrs/attrs/issues/474 -# -# * We run mypy with disallow_untyped_defs=True to require that type -# annotations are used, but we ignore the errors for files that are not yet -# participating. -# -# * "Type alias to Union" not callable is telling us that our zope.interface -# compatibility hack doesn't support adaptation. Real ZI support in mypy -# should obviate this. - -mypy "$@" \ - | grep -v \ - -e ': error: Argument "converter" to "attrib" has incompatible type' \ - -e ': error: Method must have at least one argument' \ - -e ': error: The type alias to [A-Za-z]* is invalid in runtime context' \ - -e ': note: Use "-> None" if function does not return a value' \ - -e '^src/klein/_app.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/_decorators.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/_iapp.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/_plating.py:.* error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/_plating.py:[0-9:]*: error: Need type annotation for ' \ - -e '^src/klein/_resource.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/test/_trial.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/test/py3_test_resource.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/test/test_app.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/test/test_plating.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/test/test_plating.py:[0-9:]*: error: Need type annotation for ' \ - -e '^src/klein/test/test_resource.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - -e '^src/klein/test/util.py:[0-9:]*: error: Function is missing a [a-z ]* annotation' \ - > "${tmp}" || true; - -sort < "${tmp}"; - -if grep -e ": error: " "${tmp}" > /dev/null; then - exit 1; -fi; diff --git a/src/klein/__init__.py b/src/klein/__init__.py index 1accb4e1..96eb3961 100644 --- a/src/klein/__init__.py +++ b/src/klein/__init__.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: # Inform mypy of import shenanigans. from .resource import _SpecialModuleObject - resource = _SpecialModuleObject() + resource = _SpecialModuleObject(None) else: from . import resource diff --git a/src/klein/_app.py b/src/klein/_app.py index e21691f4..6997e0e6 100644 --- a/src/klein/_app.py +++ b/src/klein/_app.py @@ -11,8 +11,9 @@ try: from inspect import iscoroutine except ImportError: - def iscoroutine(*args, **kwargs): # type: ignore + def iscoroutine(*args, **kwargs): # type: ignore[misc] return False +from typing import IO, Optional from weakref import ref from twisted.internet import endpoints, reactor @@ -142,6 +143,7 @@ def execute_error_handler(self, handler, request, failure): def resource(self): + # type: () -> KleinResource """ Return an L{IResource} which suitably wraps this app. @@ -389,8 +391,15 @@ def urlFor(self, request, endpoint, values=None, method=None, url_for = urlFor - def run(self, host=None, port=None, logFile=None, - endpoint_description=None, displayTracebacks=True): + def run( + self, + host=None, # type: Optional[str] + port=None, # type: Optional[int] + logFile=None, # type: Optional[IO] + endpoint_description=None, # type: Optional[str] + displayTracebacks=True, # type: bool + ): + # type: (...) -> None """ Run a minimal twisted.web server on the specified C{port}, bound to the interface specified by C{host} and logging to C{logFile}. @@ -403,22 +412,17 @@ def run(self, host=None, port=None, logFile=None, to. "0.0.0.0" will allow you to listen on all interfaces, and "127.0.0.1" will allow you to listen on just the loopback interface. - @type host: str @param port: The TCP port to accept HTTP requests on. - @type port: int @param logFile: The file object to log to, by default C{sys.stdout} - @type logFile: file object @param endpoint_description: specification of endpoint. Must contain protocol, port and interface. May contain other optional arguments, e.g. to use SSL: "ssl:443:privateKey=key.pem:certKey=crt.pem" - @type endpoint_description: str @param displayTracebacks: Weather a processing error will result in a page displaying the traceback with debugging information or not. - @type displayTracebacks: bool """ if logFile is None: logFile = sys.stdout diff --git a/src/klein/_decorators.py b/src/klein/_decorators.py index 528ecc5c..44e49afe 100644 --- a/src/klein/_decorators.py +++ b/src/klein/_decorators.py @@ -1,5 +1,6 @@ from functools import wraps + def bindable(bindable): """ Mark a method as a "bindable" method. diff --git a/src/klein/_dihttp.py b/src/klein/_dihttp.py index a646ec43..c6a441ee 100644 --- a/src/klein/_dihttp.py +++ b/src/klein/_dihttp.py @@ -1,4 +1,3 @@ - """ Dependency-Injected HTTP metadata. """ @@ -16,13 +15,12 @@ from .interfaces import IDependencyInjector, IRequiredParameter -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from hyperlink import DecodedURL from typing import Dict from klein.interfaces import IRequestLifecycle from twisted.web.iweb import IRequest from twisted.python.components import Componentized - Componentized, DecodedURL, IRequest, IRequestLifecycle, Dict def urlFromRequest(request): @@ -49,7 +47,7 @@ def urlFromRequest(request): ) -@provider(IRequiredParameter, IDependencyInjector) +@provider(IRequiredParameter, IDependencyInjector) # type: ignore[misc] class RequestURL(object): """ Require a hyperlink L{DecodedURL} object from a L{Requirer}. @@ -75,7 +73,7 @@ def finalize(cls): -@implementer(IRequiredParameter, IDependencyInjector) +@implementer(IRequiredParameter, IDependencyInjector) # type: ignore[misc] @attr.s(frozen=True) class RequestComponent(object): """ @@ -121,8 +119,10 @@ class Response(object): """ code = attr.ib(type=int, default=200) headers = attr.ib( - type=Mapping[Union[Text, bytes], Union[Text, bytes, - Sequence[Union[Text, bytes]]]], + type=Mapping[ + Union[Text, bytes], + Union[Text, bytes, Sequence[Union[Text, bytes]]] + ], default=attr.Factory(dict), ) body = attr.ib(type=Any, default=u'') diff --git a/src/klein/_form.py b/src/klein/_form.py index 8b46366a..88b227c4 100644 --- a/src/klein/_form.py +++ b/src/klein/_form.py @@ -1,17 +1,16 @@ # -*- test-case-name: klein.test.test_form -*- -from __future__ import print_function, unicode_literals +from __future__ import unicode_literals import json from typing import ( Any, AnyStr, Callable, Dict, Iterable, List, Optional, Sequence, - TYPE_CHECKING, Text, Union, cast + TYPE_CHECKING, Text, cast, ) import attr from twisted.internet.defer import inlineCallbacks -from twisted.python.compat import unicode from twisted.python.components import Componentized, registerAdapter from twisted.web.error import MissingRenderMethod from twisted.web.http import FORBIDDEN @@ -23,21 +22,21 @@ from ._app import _call from ._decorators import bindable -from .interfaces import (EarlyExit, IDependencyInjector, IRequestLifecycle, - IRequiredParameter, ISession, SessionMechanism, - ValidationError, ValueAbsent) +from .interfaces import ( + EarlyExit, IDependencyInjector, IRequestLifecycle, IRequiredParameter, + ISession, SessionMechanism, ValidationError, ValueAbsent, +) if TYPE_CHECKING: # pragma: no cover from typing import Type from mypy_extensions import DefaultNamedArg, NoReturn from twisted.internet.defer import Deferred - if not TYPE_CHECKING: - (Tag, Any, Callable, Dict, Optional, AnyStr, Iterable, IRequest, List, - Text, DefaultNamedArg, Union, NoReturn, Deferred, Type) else: def DefaultNamedArg(*ignore): pass + + class CrossSiteRequestForgery(Resource, object): """ Cross site request forgery detected. Request aborted. @@ -47,6 +46,7 @@ def __init__(self, message): super(CrossSiteRequestForgery, self).__init__() self.message = message + def render(self, request): # type: (IRequest) -> bytes """ @@ -57,14 +57,17 @@ def render(self, request): CSRF_PROTECTION = "__csrf_protection__" + + def textConverter(value): # type: (AnyStr) -> Text """ Converter for form values (which may be any type of string) into text. """ - return ( - value if isinstance(value, unicode) else unicode(value, "utf-8") - ) + if isinstance(value, bytes): + return value.decode("utf-8") + else: + return value @@ -77,7 +80,7 @@ class IParsedJSONBody(Interface): -@implementer(IRequiredParameter) +@implementer(IRequiredParameter) # type: ignore[misc] @attr.s(frozen=True) class Field(object): """ @@ -105,7 +108,10 @@ def registerInjector(self, injectionComponents, parameterName, Register this form field as a dependency injector. """ protoForm = IProtoForm(injectionComponents) - return protoForm.addField(self.maybeNamed(parameterName)) + return cast( + IDependencyInjector, + protoForm.addField(self.maybeNamed(parameterName)) + ) def maybeNamed(self, name): @@ -138,9 +144,13 @@ def asTags(self): @return: A new set of tags to include in a template. @rtype: iterable of L{twisted.web.template.Tag} """ + value = self.value + if self.value is None: + # FIXME: mypy says this is unreachable because self.value is not + # optional. Should it be? + value = "" # type: ignore[misc] input_tag = tags.input( - type=self.formInputType, name=self.formFieldName, - value=(self.value if self.value is not None else "") + type=self.formInputType, name=self.formFieldName, value=value ) error_tags = [] if self.error: @@ -152,6 +162,7 @@ def asTags(self): yield input_tag yield error_tags + def extractValue(self, request): # type: (IRequest) -> Any """ @@ -217,6 +228,7 @@ def text(cls, **kw): # type: (**Any) -> Field """ return cls(converter=textConverter, formInputType="text", **kw) + @classmethod def password(cls, **kw): # type: (**Any) -> Field """ @@ -227,6 +239,7 @@ def password(cls, **kw): # type: (**Any) -> Field return cls(converter=textConverter, formInputType="password", **kw) + @classmethod def hidden(cls, name, value, **kw): # type: (str, Text, **Any) -> Field @@ -279,13 +292,13 @@ class RenderableForm(object): An L{IRenderable} representing a renderable form. @ivar prevalidationValues: a L{dict} mapping {L{Field}: L{list} of - L{unicode}}, representing the value that each field received as part of + L{Text}}, representing the value that each field received as part of the request. @ivar validationErrors: a L{dict} mapping {L{Field}: L{ValidationError}} """ _form = attr.ib(type='Form') - _session = attr.ib(type=ISession) + _session = attr.ib(type=ISession) # type: ignore[misc] _action = attr.ib(type=str) _method = attr.ib(type=str) _enctype = attr.ib(type=str) @@ -340,8 +353,10 @@ def _fieldsToRender(self): if self._method.lower() == 'post': yield self._fieldForCSRF() + # Public interface below. + def lookupRenderMethod(self, name): # type: (str) -> NoReturn """ @@ -413,7 +428,7 @@ def defaultValidationFailureHandler( @return: Any object acceptable from a Klein route. """ - session = request.getComponent(ISession) + session = request.getComponent(ISession) # type: ignore[misc] request.setResponseCode(400) enctype = ( (request.getHeader(b'content-type') or @@ -450,6 +465,7 @@ class IProtoForm(Interface): Marker interface for L{ProtoForm}. """ + class IForm(Interface): """ Marker interface for form attached to dependency injection components. @@ -463,7 +479,7 @@ class ProtoForm(object): Form-builder. """ _componentized = attr.ib(type=Componentized) - _lifecycle = attr.ib(type=IRequestLifecycle) + _lifecycle = attr.ib(type=IRequestLifecycle) # type: ignore[misc] _fields = attr.ib(type=List[Field], default=attr.Factory(list)) @classmethod @@ -472,7 +488,7 @@ def fromComponentized(cls, componentized): """ Create a ProtoForm from a componentized object. """ - rl = IRequestLifecycle(componentized) + rl = IRequestLifecycle(componentized) # type: ignore[misc] assert rl is not None return cls(componentized, rl) @@ -524,7 +540,7 @@ def validate(self, instance, request): -@implementer(IDependencyInjector) +@implementer(IDependencyInjector) # type: ignore[misc] @attr.s class FieldInjector(object): """ @@ -532,7 +548,7 @@ class FieldInjector(object): """ _componentized = attr.ib(type=Componentized) _field = attr.ib(type=Field) - _lifecycle = attr.ib(type=IRequestLifecycle) + _lifecycle = attr.ib(type=IRequestLifecycle) # type: ignore[misc] def injectValue(self, instance, request, routeParams): # type: (Any, IRequest, Dict[str, Any]) -> Any @@ -543,15 +559,18 @@ def injectValue(self, instance, request, routeParams): self._field.pythonArgumentName ) + def finalize(self): # type: () -> None """ Finalize this ProtoForm into a real form. """ - finalForm = IForm(self._componentized, None) - if finalForm is not None: + if IForm(self._componentized, None) is not None: return - finalForm = Form(IProtoForm(self._componentized)._fields) + + finalForm = cast( + IForm, Form(IProtoForm(self._componentized)._fields) + ) self._componentized.setComponent(IForm, finalForm) # XXX set requiresComponents argument here to ISession if CSRF is @@ -566,7 +585,7 @@ def populateValuesHook(instance, request): ) self._lifecycle.addPrepareHook( populateValuesHook, provides=[IFieldValues], - requires=[ISession] + requires=[ISession] # type: ignore[misc] ) @@ -581,6 +600,7 @@ class IValidationFailureHandler(Interface): """ + def checkCSRF(request): # type: (IRequest) -> None """ @@ -588,7 +608,7 @@ def checkCSRF(request): it is found. """ # TODO: optionalize CSRF protection for GET forms - session = ISession(request, None) + session = ISession(request, None) # type: ignore[misc] token = None if request.method in (b'GET', b'HEAD'): # Idempotent requests don't require CRSF validation. (Don't have @@ -734,14 +754,16 @@ def showForm(self, form): As a L{RenderableForm} provides L{IRenderable}, you may return the parameter directly """ - form = IForm(decoratedFunction.injectionComponents, None) + form = IForm( + decoratedFunction.injectionComponents, None + ) # type: Optional[Form] if form is None: form = Form([]) return RenderableFormParam(form, action, method, enctype, encoding) -@implementer(IRequiredParameter, IDependencyInjector) +@implementer(IRequiredParameter, IDependencyInjector) # type: ignore[misc] @attr.s class RenderableFormParam(object): """ @@ -760,17 +782,20 @@ def registerInjector(self, injectionComponents, parameterName, # type: (Componentized, str, IRequestLifecycle) -> RenderableFormParam return self + def injectValue(self, instance, request, routeParams): # type: (Any, IRequest, Dict[str, Any]) -> RenderableForm """ Create the renderable form from the request. """ return RenderableForm( - self._form, ISession(request), self._action, self._method, - self._enctype, self._encoding, prevalidationValues={}, - validationErrors={} + self._form, + ISession(request), # type: ignore[misc] + self._action, self._method, self._enctype, self._encoding, + prevalidationValues={}, validationErrors={}, ) + def finalize(self): # type: () -> None """ diff --git a/src/klein/_headers.py b/src/klein/_headers.py index 2eebcb3a..a1078aca 100644 --- a/src/klein/_headers.py +++ b/src/klein/_headers.py @@ -14,9 +14,6 @@ from ._imessage import MutableRawHeaders, RawHeader, RawHeaders from ._interfaces import IHTTPHeaders, IMutableHTTPHeaders -# Silence linter -AnyStr, Iterable, Tuple, MutableRawHeaders, RawHeader, RawHeaders - __all__ = () @@ -162,7 +159,7 @@ def rawHeaderNameAndValue(name, value): # Implementation -@implementer(IHTTPHeaders) +@implementer(IHTTPHeaders) # type: ignore[misc] @attrs(frozen=True) class FrozenHTTPHeaders(object): """ @@ -181,7 +178,7 @@ def getValues(self, name): -@implementer(IMutableHTTPHeaders) +@implementer(IMutableHTTPHeaders) # type: ignore[misc] @attrs(frozen=True) class MutableHTTPHeaders(object): """ diff --git a/src/klein/_headers_compat.py b/src/klein/_headers_compat.py index 2a90c1af..163b5ed2 100644 --- a/src/klein/_headers_compat.py +++ b/src/klein/_headers_compat.py @@ -5,7 +5,7 @@ Support for interoperability with L{twisted.web.http_headers.Headers}. """ -from typing import AnyStr, Iterable, Text, Tuple +from typing import AnyStr, Iterable, Text, Tuple, cast from attr import attrib, attrs from attr.validators import instance_of @@ -20,14 +20,12 @@ rawHeaderNameAndValue, ) -AnyStr, Iterable, RawHeaders, String, Tuple # Silence linter - __all__ = () -@implementer(IMutableHTTPHeaders) +@implementer(IMutableHTTPHeaders) # type: ignore[misc] @attrs(frozen=True) class HTTPHeadersWrappingHeaders(object): """ @@ -59,7 +57,9 @@ def pairs(): def getValues(self, name): # type: (AnyStr) -> Iterable[AnyStr] if isinstance(name, bytes): - values = self._headers.getRawHeaders(name, default=()) + values = cast( + Iterable[AnyStr], self._headers.getRawHeaders(name, default=()) + ) elif isinstance(name, Text): values = ( headerValueAsText(value) diff --git a/src/klein/_iform.py b/src/klein/_iform.py index 058e6436..40bd795d 100644 --- a/src/klein/_iform.py +++ b/src/klein/_iform.py @@ -1,5 +1,3 @@ - - class ValidationError(Exception): """ A L{ValidationError} is raised by L{Field.extractValue}. diff --git a/src/klein/_imessage.py b/src/klein/_imessage.py index 45f10dee..394460b7 100644 --- a/src/klein/_imessage.py +++ b/src/klein/_imessage.py @@ -23,8 +23,6 @@ from ._typing import ifmethod -AnyStr, DecodedURL, Deferred, Iterable, IFount, Text # Silence linter - __all__ = () diff --git a/src/klein/_interfaces.py b/src/klein/_interfaces.py index 1a9d1638..ebd92d32 100644 --- a/src/klein/_interfaces.py +++ b/src/klein/_interfaces.py @@ -18,7 +18,7 @@ IMutableHTTPHeaders as _IMutableHTTPHeaders, ) -IKleinRequest # Silence linter +IKleinRequest if TYPE_CHECKING: # pragma: no cover diff --git a/src/klein/_isession.py b/src/klein/_isession.py index 538a9e83..9ffa78ef 100644 --- a/src/klein/_isession.py +++ b/src/klein/_isession.py @@ -15,12 +15,11 @@ if TYPE_CHECKING: # pragma: no cover from twisted.internet.defer import Deferred from twisted.python.components import Componentized - from typing import Dict, Iterable, List, Text, Type, Sequence + from typing import Dict, Iterable, Text, Sequence from twisted.web.iweb import IRequest from zope.interface.interfaces import IInterface - Deferred, Text, Componentized, Sequence, IRequest, List, Type - Iterable, IInterface, Dict + class NoSuchSession(Exception): """ diff --git a/src/klein/_message.py b/src/klein/_message.py index d6db4e6d..96fee6b6 100644 --- a/src/klein/_message.py +++ b/src/klein/_message.py @@ -15,11 +15,8 @@ from twisted.internet.defer import Deferred, succeed from ._imessage import FountAlreadyAccessedError -from ._interfaces import IHTTPMessage from ._tubes import bytesToFount, fountToBytes -Any, Deferred, IHTTPMessage, Optional # Silence linter - __all__ = () diff --git a/src/klein/_plating.py b/src/klein/_plating.py index 90eeb83a..1171c3f9 100644 --- a/src/klein/_plating.py +++ b/src/klein/_plating.py @@ -35,6 +35,7 @@ cast(Tuple[Any, ...], (float, None.__class__)) ) + def _should_return_json(request): # type: (IRequest) -> bool """ diff --git a/src/klein/_request.py b/src/klein/_request.py index 9544bba8..9ed15205 100644 --- a/src/klein/_request.py +++ b/src/klein/_request.py @@ -21,15 +21,12 @@ from ._interfaces import IHTTPHeaders, IHTTPRequest from ._message import MessageState, bodyAsBytes, bodyAsFount, validateBody -# Silence linter -Deferred, IFount, IHTTPHeaders, Text, Union - __all__ = () -@implementer(IHTTPRequest) +@implementer(IHTTPRequest) # type: ignore[misc] @attrs(frozen=True) class FrozenHTTPRequest(object): """ @@ -38,7 +35,9 @@ class FrozenHTTPRequest(object): method = attrib(validator=instance_of(Text)) # type: Text uri = attrib(validator=instance_of(DecodedURL)) # type: DecodedURL - headers = attrib(validator=provides(IHTTPHeaders)) # type: IHTTPHeaders + headers = attrib( + validator=provides(IHTTPHeaders) # type: ignore[misc] + ) # type: IHTTPHeaders _body = attrib(validator=validateBody) # type: Union[bytes, IFount] diff --git a/src/klein/_request_compat.py b/src/klein/_request_compat.py index ede9b3a3..35afd2d7 100644 --- a/src/klein/_request_compat.py +++ b/src/klein/_request_compat.py @@ -6,7 +6,7 @@ """ from io import BytesIO -from typing import Text +from typing import Text, cast from attr import Factory, attrib, attrs from attr.validators import provides @@ -27,9 +27,6 @@ from ._request import IHTTPRequest from ._tubes import IOFount, fountToBytes -# Silence linter -Deferred, IFount, IHTTPHeaders, Text - __all__ = () @@ -38,7 +35,7 @@ -@implementer(IHTTPRequest) +@implementer(IHTTPRequest) # type: ignore[misc] @attrs(frozen=True) class HTTPRequestWrappingIRequest(object): """ @@ -57,7 +54,7 @@ class HTTPRequestWrappingIRequest(object): @property def method(self): # type: () -> Text - return self._request.method.decode("ascii") + return cast(Text, self._request.method.decode("ascii")) @property diff --git a/src/klein/_requirer.py b/src/klein/_requirer.py index 62db6cc7..e132d5e8 100644 --- a/src/klein/_requirer.py +++ b/src/klein/_requirer.py @@ -12,16 +12,16 @@ from ._decorators import bindable, modified from .interfaces import EarlyExit, IRequestLifecycle -if TYPE_CHECKING: # pragma: no cover - from typing import Dict, Tuple, Sequence +if TYPE_CHECKING: # pragma: no cover + from typing import Dict, Sequence from twisted.web.iweb import IRequest from twisted.internet.defer import Deferred from zope.interface.interfaces import IInterface from .interfaces import IDependencyInjector, IRequiredParameter - IDependencyInjector, IRequiredParameter, IRequest, Dict, Tuple - Deferred, IInterface, Sequence -@implementer(IRequestLifecycle) + + +@implementer(IRequestLifecycle) # type: ignore[misc] @attr.s class RequestLifecycle(object): """ @@ -63,6 +63,8 @@ def runPrepareHooks(self, instance, request): _prerequisiteCallback = Callable[[IRequestLifecycle], None] + + @attr.s class Requirer(object): """ @@ -115,10 +117,12 @@ def require(self, routeDecorator, **requiredParameters): """ def decorator(functionWithRequirements): - # type: (Any) -> Callable + # type: (Callable) -> Callable injectionComponents = Componentized() lifecycle = RequestLifecycle() - injectionComponents.setComponent(IRequestLifecycle, lifecycle) + injectionComponents.setComponent( + IRequestLifecycle, lifecycle # type: ignore[misc] + ) injectors = {} # type: Dict[str, IDependencyInjector] @@ -149,12 +153,12 @@ def router(instance, request, *args, **routeParams): result = ee.alternateReturnValue else: result = yield _call( - instance, functionWithRequirements, *args, - **injected + instance, functionWithRequirements, *args, **injected ) returnValue(result) - functionWithRequirements.injectionComponents = injectionComponents + fWR, iC = functionWithRequirements, injectionComponents + fWR.injectionComponents = iC # type: ignore[attr-defined] routeDecorator(router) return functionWithRequirements diff --git a/src/klein/_response.py b/src/klein/_response.py index 0eccd734..8b700712 100644 --- a/src/klein/_response.py +++ b/src/klein/_response.py @@ -19,14 +19,12 @@ from ._interfaces import IHTTPHeaders, IHTTPResponse from ._message import MessageState, bodyAsBytes, bodyAsFount, validateBody -Deferred, IFount, Union # Silence linter - __all__ = () -@implementer(IHTTPResponse) +@implementer(IHTTPResponse) # type: ignore[misc] @attrs(frozen=True) class FrozenHTTPResponse(object): """ @@ -35,7 +33,9 @@ class FrozenHTTPResponse(object): status = attrib(validator=instance_of(int)) # type: int - headers = attrib(validator=provides(IHTTPHeaders)) # type: IHTTPHeaders + headers = attrib( + validator=provides(IHTTPHeaders) # type: ignore[misc] + ) # type: IHTTPHeaders _body = attrib(validator=validateBody) # type: Union[bytes, IFount] diff --git a/src/klein/_session.py b/src/klein/_session.py index b65e13bb..c9c175e0 100644 --- a/src/klein/_session.py +++ b/src/klein/_session.py @@ -20,20 +20,18 @@ TooLateForCookies ) -if TYPE_CHECKING: # pragma: no cover - from mypy_extensions import Arg, KwArg, VarArg +if TYPE_CHECKING: # pragma: no cover from twisted.web.iweb import IRequest from twisted.python.components import Componentized - from typing import Awaitable, Dict, Sequence, Text, TypeVar + from mypy_extensions import KwArg, Arg + from typing import Dict, Sequence, Text, TypeVar T = TypeVar('T') - (IRequest, Arg, KwArg, VarArg, Callable, Dict, IInterface, Awaitable, - Componentized, IRequestLifecycle, Text) else: Arg = KwArg = lambda t, *x: t -@implementer(ISessionProcurer) +@implementer(ISessionProcurer) # type: ignore[misc] @attr.s class SessionProcurer(object): """ @@ -76,7 +74,7 @@ class SessionProcurer(object): @type _setCookieOnGET: L{bool} """ - _store = attr.ib(type=ISessionStore) + _store = attr.ib(type=ISessionStore) # type: ignore[misc] _maxAge = attr.ib(type=int, default=3600) _secureCookie = attr.ib(type=bytes, default=b"Klein-Secure-Session") @@ -92,7 +90,7 @@ class SessionProcurer(object): @inlineCallbacks def procureSession(self, request, forceInsecure=False): # type: (IRequest, bool) -> Any - alreadyProcured = request.getComponent(ISession) + alreadyProcured = request.getComponent(ISession) # type: ignore[misc] if alreadyProcured is not None: if not forceInsecure or not request.isSecure(): returnValue(alreadyProcured) @@ -110,12 +108,22 @@ def procureSession(self, request, forceInsecure=False): # Have we inadvertently disclosed a secure token over an insecure # transport, for example, due to a buggy client? allPossibleSentTokens = ( - sum([request.requestHeaders.getRawHeaders(header, []) - for header in [self._secureTokenHeader, - self._insecureTokenHeader]], []) + - [it for it in [request.getCookie(cookie) - for cookie in [self._secureCookie, - self._insecureCookie]] if it] + sum( + [ + request.requestHeaders.getRawHeaders(header, []) + for header in [ + self._secureTokenHeader, self._insecureTokenHeader + ] + ], + [] + ) + + [ + it for it in [ + request.getCookie(cookie) for cookie in + [self._secureCookie, self._insecureCookie] + ] + if it + ] ) # type: Sequence[Text] # Does it seem like this check is expensive? It sure is! Don't want # to do it? Turn on your dang HTTPS! @@ -185,7 +193,7 @@ def procureSession(self, request, forceInsecure=False): ) if sentSecurely or not request.isSecure(): # Do not cache the insecure session on the secure request, thanks. - request.setComponent(ISession, session) + request.setComponent(ISession, session) # type: ignore[misc] returnValue(session) @@ -214,7 +222,7 @@ def render(self, request): return "{} DENIED".format(qual(self._interface)).encode('utf-8') -@implementer(IDependencyInjector, IRequiredParameter) +@implementer(IDependencyInjector, IRequiredParameter) # type: ignore[misc] @attr.s class Authorization(object): """ @@ -290,8 +298,10 @@ def injectValue(self, instance, request, routeParams): # TODO: this could be optimized to do fewer calls to 'authorize' by # collecting all the interfaces that are necessary and then using # addBeforeHook; the interface would not need to change. - provider = ((yield ISession(request).authorize([self._interface])) - .get(self._interface)) + session = ISession(request) # type: ignore[misc] + provider = ( + (yield session.authorize([self._interface])).get(self._interface) + ) if self._required and provider is None: raise EarlyExit(self._whenDenied(self._interface, instance)) # TODO: CSRF protection should probably go here diff --git a/src/klein/_tubes.py b/src/klein/_tubes.py index c0493fb6..1a7c6181 100644 --- a/src/klein/_tubes.py +++ b/src/klein/_tubes.py @@ -5,8 +5,7 @@ """ from io import BytesIO -from typing import Iterable -from typing.io import BinaryIO +from typing import Any as UnknownType, BinaryIO, Iterable from attr import attrib, attrs from attr.validators import instance_of, optional, provides @@ -20,8 +19,6 @@ from zope.interface import implementer -BinaryIO, Deferred, Iterable # Silence linter - __all__ = () @@ -85,12 +82,12 @@ def flowTo(self, drain): def pauseFlow(self): - # type: () -> None + # type: () -> UnknownType return self._pauser.pause() def stopFlow(self): - # type: () -> None + # type: () -> UnknownType return self._pauser.resume() diff --git a/src/klein/_typing.py b/src/klein/_typing.py index 2670e6b5..4db35b39 100644 --- a/src/klein/_typing.py +++ b/src/klein/_typing.py @@ -1,11 +1,15 @@ -from typing import TYPE_CHECKING +from typing import Callable, TYPE_CHECKING __all__ = () -if TYPE_CHECKING: # pragma: no cover +def _ifmethod(method): + # type: (Callable) -> Callable + return method + + +if TYPE_CHECKING: # pragma: no cover ifmethod = staticmethod else: - def ifmethod(method): - return method + ifmethod = _ifmethod diff --git a/src/klein/interfaces.py b/src/klein/interfaces.py index e40218bd..e07cd314 100644 --- a/src/klein/interfaces.py +++ b/src/klein/interfaces.py @@ -24,7 +24,7 @@ ) if TYPE_CHECKING: # pragma: no cover - from ._storage.memory import MemorySessionStore, MemorySession + from .storage._memory import MemorySessionStore, MemorySession from ._session import SessionProcurer, Authorization from ._form import Field, RenderableFormParam, FieldInjector from ._isession import IRequestLifecycleT as _IRequestLifecycleT diff --git a/src/klein/resource.py b/src/klein/resource.py index e9d5bbfd..980a1a7c 100644 --- a/src/klein/resource.py +++ b/src/klein/resource.py @@ -15,10 +15,11 @@ from ._resource import KleinResource as _KleinResource, ensure_utf8_bytes if TYPE_CHECKING: - from typing import AnyStr, Callable, Text - AnyStr, Callable, Text + from typing import Any, AnyStr, Callable, Text KleinResource = _KleinResource + + class _SpecialModuleObject(object): """ See the test in @@ -33,11 +34,18 @@ class _SpecialModuleObject(object): KleinResource = _KleinResource + + def __init__(self, preserve): + # type: (Any) -> None + self.__preserve__ = preserve + + @property def ensure_utf8_bytes(self): # type: () -> Callable[[AnyStr], Text] return ensure_utf8_bytes + def __call__(self): # type: () -> _KleinResource """ @@ -58,6 +66,5 @@ def __repr__(self): return "" -preserve = modules[__name__] -modules[__name__] = _SpecialModuleObject() # type: ignore -modules[__name__].__preserve__ = preserve # type: ignore +module = _SpecialModuleObject(modules[__name__]) +modules[__name__] = module # type: ignore[assignment] diff --git a/src/klein/storage/_memory.py b/src/klein/storage/_memory.py index 63ee71cf..da45cb8f 100644 --- a/src/klein/storage/_memory.py +++ b/src/klein/storage/_memory.py @@ -1,7 +1,7 @@ # -*- test-case-name: klein.test.test_memory -*- from binascii import hexlify from os import urandom -from typing import Any, Callable, Dict, List, TYPE_CHECKING, Text, cast +from typing import Any, Callable, Dict, Iterable, Text, cast import attr from attr import Factory @@ -16,12 +16,11 @@ ISession, ISessionStore, NoSuchSession, SessionMechanism ) -if TYPE_CHECKING: # pragma: no cover - List, Deferred, IInterface, Any, Callable, Dict, SessionMechanism - _authCB = Callable[[IInterface, ISession, Componentized], Any] -@implementer(ISession) + + +@implementer(ISession) # type: ignore[misc] @attr.s class MemorySession(object): """ @@ -31,12 +30,12 @@ class MemorySession(object): identifier = attr.ib(type=Text) isConfidential = attr.ib(type=bool) authenticatedBy = attr.ib(type=SessionMechanism) - _authorizationCallback = attr.ib(type=_authCB) + _authorizationCallback = attr.ib(type=_authCB) # type: ignore[misc] _components = attr.ib(default=Factory(Componentized), type=Componentized) def authorize(self, interfaces): - # type: (List[IInterface]) -> Deferred + # type: (Iterable[IInterface]) -> Deferred """ Authorize each interface by calling back to the session store's authorization callback. @@ -50,6 +49,7 @@ def authorize(self, interfaces): return succeed(result) + class _MemoryAuthorizerFunction(object): """ Type shadow for function with the given attribute. @@ -81,11 +81,13 @@ def _noAuthorization(interface, session, data): # type: (IInterface, ISession, Componentized) -> None return None -@implementer(ISessionStore) + + +@implementer(ISessionStore) # type: ignore[misc] @attr.s class MemorySessionStore(object): authorizationCallback = attr.ib( - type=_authFn, + type=_authFn, # type: ignore[misc] default=_noAuthorization ) _secureStorage = attr.ib(type=Dict[str, Any], @@ -95,7 +97,7 @@ class MemorySessionStore(object): @classmethod def fromAuthorizers(cls, authorizers): - # type: (List[_MemoryAuthorizerFunction]) -> MemorySessionStore + # type: (Iterable[_MemoryAuthorizerFunction]) -> MemorySessionStore """ Create a L{MemorySessionStore} from a collection of callbacks which can do authorization. @@ -147,5 +149,5 @@ def loadSession(self, identifier, isConfidential, authenticatedBy): def sentInsecurely(self, tokens): - # type: (List[str]) -> None + # type: (Iterable[str]) -> None return diff --git a/src/klein/test/_strategies.py b/src/klein/test/_strategies.py index 09a586fd..1f14bbe1 100644 --- a/src/klein/test/_strategies.py +++ b/src/klein/test/_strategies.py @@ -8,7 +8,7 @@ from os.path import dirname, join from string import ascii_letters, digits from sys import maxunicode -from typing import Callable, Iterable, Optional, Sequence, Text, TypeVar +from typing import Callable, Iterable, Optional, Sequence, Text, TypeVar, cast from hyperlink import DecodedURL, EncodedURL @@ -21,8 +21,6 @@ from twisted.python.compat import _PY3, unicode -Iterable, Optional, Sequence, Text # Silence linter - __all__ = () @@ -91,9 +89,12 @@ def ascii_text(draw, min_size=0, max_size=None): # pragma: no cover @param max_size: The maximum number of characters in the text. Use C{None} for an unbounded size. """ - return draw(text( - min_size=min_size, max_size=max_size, alphabet=ascii_letters - )) + return cast( + Text, + draw(text( + min_size=min_size, max_size=max_size, alphabet=ascii_letters + )) + ) @composite # pragma: no cover @@ -126,9 +127,12 @@ def idna_text(draw, min_size=0, max_size=None): # pragma: no cover @param max_size: The maximum number of characters in the text. Use C{None} for an unbounded size. """ - return draw(text( - min_size=min_size, max_size=max_size, alphabet=idna_characters() - )) + return cast( + Text, + draw(text( + min_size=min_size, max_size=max_size, alphabet=idna_characters() + )) + ) @composite @@ -144,7 +148,9 @@ def port_numbers(draw, allow_zero=False): # pragma: no cover else: min_value = 1 - return draw(integers(min_value=min_value, max_value=65535)) + return cast( + int, draw(integers(min_value=min_value, max_value=65535)) + ) @composite @@ -157,7 +163,7 @@ def hostname_labels(draw, allow_idn=True): # pragma: no cover internationalized domain names (IDNs). """ if allow_idn: - label = draw(idna_text(min_size=1, max_size=63)) + label = cast(Text, draw(idna_text(min_size=1, max_size=63))) try: label.encode("ascii") @@ -172,10 +178,13 @@ def hostname_labels(draw, allow_idn=True): # pragma: no cover label = label[:-1] else: - label = draw(text( - min_size=1, max_size=63, - alphabet=unicode(ascii_letters + digits + u"-") - )) + label = cast( + Text, + draw(text( + min_size=1, max_size=63, + alphabet=unicode(ascii_letters + digits + u"-") + )) + ) # Filter invalid labels. # It would be better not to generate bogus labels in the first place... but @@ -202,9 +211,12 @@ def hostnames( @param allow_idn: Whether to allow non-ASCII characters as allowed by internationalized domain names (IDNs). """ - labels = draw( - lists(hostname_labels(allow_idn=allow_idn), min_size=1, max_size=5) - .filter(lambda ls: sum(len(l) for l in ls) + len(ls) - 1 <= 252) + labels = cast( + Sequence[Text], + draw( + lists(hostname_labels(allow_idn=allow_idn), min_size=1, max_size=5) + .filter(lambda ls: sum(len(l) for l in ls) + len(ls) - 1 <= 252) + ) ) name = u".".join(labels) @@ -255,8 +267,11 @@ def chars(): @composite def paths(draw): # pragma: no cover # type: (DrawCallable) -> Sequence[Text] - return draw( - lists(text(min_size=1, alphabet=path_characters()), max_size=10) + return cast( + Sequence[Text], + draw( + lists(text(min_size=1, alphabet=path_characters()), max_size=10) + ) ) @@ -268,15 +283,15 @@ def encoded_urls(draw): # pragma: no cover Call the L{EncodedURL.to_uri} method on each URL to get an HTTP protocol-friendly URI. """ - port = draw(port_numbers(allow_zero=True)) - host = draw(hostnames()) - path = draw(paths()) + port = cast(Optional[int], draw(port_numbers(allow_zero=True))) + host = cast(Text, draw(hostnames())) + path = cast(Sequence[Text], draw(paths())) if port == 0: port = None args = dict( - scheme=draw(sampled_from((u"http", u"https"))), + scheme=cast(Text, draw(sampled_from((u"http", u"https")))), host=host, port=port, path=path, ) diff --git a/src/klein/test/_trial.py b/src/klein/test/_trial.py index 82883770..6e00c053 100644 --- a/src/klein/test/_trial.py +++ b/src/klein/test/_trial.py @@ -5,7 +5,7 @@ """ import sys -from typing import Any +from typing import Any, Optional from twisted import version as twistedVersion from twisted.trial.unittest import SynchronousTestCase @@ -14,8 +14,6 @@ from zope.interface.exceptions import Invalid from zope.interface.verify import verifyObject -Any, Interface # Silence linter - __all__ = () @@ -28,7 +26,7 @@ class TestCase(SynchronousTestCase): if (twistedVersion.major, twistedVersion.minor) < (16, 4): def assertRegex(self, text, regex, msg=None): - # type: (str, Any, str) -> None + # type: (str, Any, Optional[str]) -> None """ Fail the test if a C{regexp} search of C{text} fails. diff --git a/src/klein/test/test_app.py b/src/klein/test/test_app.py index 99d955ed..23f88392 100644 --- a/src/klein/test/test_app.py +++ b/src/klein/test/test_app.py @@ -5,7 +5,7 @@ try: from unittest.mock import Mock, patch except Exception: - from mock import Mock, patch # type:ignore + from mock import Mock, patch # type: ignore[misc] from twisted.python.components import registerAdapter from twisted.trial import unittest diff --git a/src/klein/test/test_form.py b/src/klein/test/test_form.py index afb46947..23a35e14 100644 --- a/src/klein/test/test_form.py +++ b/src/klein/test/test_form.py @@ -19,11 +19,10 @@ ) from klein.storage.memory import MemorySessionStore -if TYPE_CHECKING: # pragma: no cover - from typing import Any, Dict, Tuple, Union +if TYPE_CHECKING: # pragma: no cover + from typing import Any, Tuple from twisted.web.iweb import IRequest from klein import RenderableForm - Any, IRequest, Text, Union, Dict, Tuple, RenderableForm @@ -40,13 +39,13 @@ def maybeNamed(self, name): @attr.s(hash=False) class TestObject(object): - sessionStore = attr.ib(type=ISessionStore) + sessionStore = attr.ib(type=ISessionStore) # type: ignore[misc] calls = attr.ib(attr.Factory(list), type=List) router = Klein() requirer = Requirer() - @requirer.prerequisite([ISession]) + @requirer.prerequisite([ISession]) # type: ignore[misc] @inlineCallbacks def procureASession(self, request): # type: (IRequest) -> Any diff --git a/src/klein/test/test_headers.py b/src/klein/test/test_headers.py index 9edc5049..dbf2b58e 100644 --- a/src/klein/test/test_headers.py +++ b/src/klein/test/test_headers.py @@ -23,9 +23,6 @@ normalizeHeaderName, normalizeRawHeaders, normalizeRawHeadersFrozen, ) -# Silence linter -AnyStr, Dict, Iterable, List, Optional, RawHeaders, Text, Tuple - __all__ = () @@ -371,7 +368,7 @@ def test_interface(self): L{FrozenHTTPHeaders} implements L{IHTTPHeaders}. """ headers = FrozenHTTPHeaders(rawHeaders=()) - self.assertProvides(IHTTPHeaders, headers) + self.assertProvides(IHTTPHeaders, headers) # type: ignore[misc] def test_defaultHeaders(self): @@ -413,7 +410,9 @@ def test_interface(self): Class implements L{IMutableHTTPHeaders}. """ headers = self.headers(rawHeaders=()) - cast(TestCase, self).assertProvides(IMutableHTTPHeaders, headers) + cast(TestCase, self).assertProvides( + IMutableHTTPHeaders, headers # type: ignore[misc] + ) def test_rawHeaders(self): diff --git a/src/klein/test/test_headers_compat.py b/src/klein/test/test_headers_compat.py index d52e06e9..e4adfde5 100644 --- a/src/klein/test/test_headers_compat.py +++ b/src/klein/test/test_headers_compat.py @@ -5,7 +5,7 @@ Tests for L{klein._headers}. """ -from typing import Text +from typing import Text, cast from twisted.web.http_headers import Headers @@ -16,15 +16,12 @@ ) from .._headers_compat import HTTPHeadersWrappingHeaders -# Silence linter -IMutableHTTPHeaders, RawHeaders, Text - - try: from twisted.web.http_headers import _sanitizeLinearWhitespace except ImportError: _sanitizeLinearWhitespace = None + def _twistedHeaderNormalize(value): # type: (Text) -> Text """ @@ -34,7 +31,10 @@ def _twistedHeaderNormalize(value): if _sanitizeLinearWhitespace is None: return value else: - return _sanitizeLinearWhitespace(value.encode("utf-8")).decode("utf-8") + return cast( + Text, + _sanitizeLinearWhitespace(value.encode("utf-8")).decode("utf-8") + ) __all__ = () diff --git a/src/klein/test/test_memory.py b/src/klein/test/test_memory.py index 6cc8c51f..60581c8f 100644 --- a/src/klein/test/test_memory.py +++ b/src/klein/test/test_memory.py @@ -9,7 +9,7 @@ from klein.interfaces import ISession, ISessionStore, SessionMechanism from klein.storage.memory import MemorySessionStore, declareMemoryAuthorizer -Any + class IFoo(Interface): """ @@ -36,9 +36,9 @@ def test_interfaceCompliance(self): Verify that the session store complies with the relevant interfaces. """ store = MemorySessionStore() - verifyObject(ISessionStore, store) + verifyObject(ISessionStore, store) # type: ignore[misc] verifyObject( - ISession, self.successResultOf( + ISession, self.successResultOf( # type: ignore[misc] store.newSession(True, SessionMechanism.Header) ) ) diff --git a/src/klein/test/test_message.py b/src/klein/test/test_message.py index a994ad20..57bf03b4 100644 --- a/src/klein/test/test_message.py +++ b/src/klein/test/test_message.py @@ -11,9 +11,8 @@ from hypothesis.strategies import binary from ._trial import TestCase -from .._message import ( - FountAlreadyAccessedError, IHTTPMessage, bytesToFount, fountToBytes -) +from .._interfaces import IHTTPMessage +from .._message import FountAlreadyAccessedError, bytesToFount, fountToBytes __all__ = () @@ -53,7 +52,9 @@ def test_interface_message(self): Message instance implements L{IHTTPMessage}. """ message = self.messageFromBytes() - cast(TestCase, self).assertProvides(IHTTPMessage, message) + cast(TestCase, self).assertProvides( + IHTTPMessage, message # type: ignore[misc] + ) @given(binary()) diff --git a/src/klein/test/test_request.py b/src/klein/test/test_request.py index 272547bd..bfe50809 100644 --- a/src/klein/test/test_request.py +++ b/src/klein/test/test_request.py @@ -10,11 +10,9 @@ from ._trial import TestCase from .test_message import FrozenHTTPMessageTestsMixIn from .._headers import FrozenHTTPHeaders -from .._message import IHTTPMessage +from .._interfaces import IHTTPMessage from .._request import FrozenHTTPRequest, IHTTPRequest -IHTTPMessage # Silence linter - __all__ = () @@ -48,7 +46,7 @@ def test_interface(self): L{FrozenHTTPRequest} implements L{IHTTPRequest}. """ request = self.requestFromBytes() - self.assertProvides(IHTTPRequest, request) + self.assertProvides(IHTTPRequest, request) # type: ignore[misc] def test_initInvalidBodyType(self): diff --git a/src/klein/test/test_request_compat.py b/src/klein/test/test_request_compat.py index 9daa60ac..9bd5cf68 100644 --- a/src/klein/test/test_request_compat.py +++ b/src/klein/test/test_request_compat.py @@ -24,8 +24,6 @@ from .._request import IHTTPRequest from .._request_compat import HTTPRequestWrappingIRequest -DecodedURL, Headers, IRequest, Optional, Text # Silence linter - __all__ = () @@ -59,7 +57,7 @@ def test_interface(self): L{HTTPRequestWrappingIRequest} implements L{IHTTPRequest}. """ request = HTTPRequestWrappingIRequest(request=self.legacyRequest()) - self.assertProvides(IHTTPRequest, request) + self.assertProvides(IHTTPRequest, request) # type: ignore[misc] @given(text(alphabet=ascii_uppercase, min_size=1)) @@ -127,7 +125,9 @@ def test_headers(self): """ legacyRequest = self.legacyRequest() request = HTTPRequestWrappingIRequest(request=legacyRequest) - self.assertProvides(IHTTPHeaders, request.headers) + self.assertProvides( + IHTTPHeaders, request.headers # type: ignore[misc] + ) def test_bodyAsFountTwice(self): diff --git a/src/klein/test/test_requirer.py b/src/klein/test/test_requirer.py index ddca8e9c..dbad07e6 100644 --- a/src/klein/test/test_requirer.py +++ b/src/klein/test/test_requirer.py @@ -1,5 +1,4 @@ - -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Text from treq.testing import StubTreq @@ -11,10 +10,11 @@ from klein import Klein, RequestComponent, RequestURL, Requirer, Response if TYPE_CHECKING: # pragma: no cover - from typing import Text, Iterable, List, Tuple + from typing import Iterable, List, Tuple from hyperlink import DecodedURL from twisted.web.iweb import IRequest - DecodedURL, Text, Iterable, List, Tuple, IRequest + + class BadlyBehavedHeaders(Headers): """ @@ -34,6 +34,8 @@ def getAllRawHeaders(self): router = Klein() requirer = Requirer() + + @requirer.require(router.route("/hello/world", methods=['GET']), url=RequestURL()) def requiresURL(url): @@ -41,13 +43,18 @@ def requiresURL(url): """ This is a route that requires a URL. """ - return url.child(u"hello/ world").asText() + text = url.child(u"hello/ world").asText() # type: Text + return text + + class ISample(Interface): """ Interface for testing. """ + + @requirer.prerequisite([ISample]) def provideSample(request): # type: (IRequest) -> None @@ -56,6 +63,7 @@ def provideSample(request): """ request.setComponent(ISample, "sample component") + @requirer.require(router.route("/retrieve/component", methods=['GET']), component=RequestComponent(ISample)) def needsComponent(component): @@ -65,6 +73,7 @@ def needsComponent(component): """ return component + @requirer.require(router.route("/set/headers")) def someHeaders(): # type: () -> Response @@ -78,6 +87,7 @@ def someHeaders(): ) + class RequireURLTests(SynchronousTestCase): """ Tests for RequestURL() required parameter. @@ -98,6 +108,7 @@ def test_requiresURL(self): self.assertEqual(response, "https://example.com/hello/world/hello%2F%20world") + def test_requiresURLNonStandardPort(self): # type: () -> None """ @@ -115,6 +126,7 @@ def test_requiresURLNonStandardPort(self): "http://example.com:8080/hello/world/hello%2F%20world" ) + def test_requiresURLBadlyBehavedClient(self): # type: () -> None """ @@ -132,6 +144,8 @@ def test_requiresURLBadlyBehavedClient(self): "https://127.0.0.1:31337/hello/world/hello%2F%20world" ) + + class RequireComponentTests(SynchronousTestCase): """ Tests for RequestComponent. @@ -152,6 +166,7 @@ def test_requestComponent(self): ) + class ResponseTests(SynchronousTestCase): """ Tests for L{klein.Response}. diff --git a/src/klein/test/test_resource.py b/src/klein/test/test_resource.py index 0ecc2c69..cdca3f8a 100644 --- a/src/klein/test/test_resource.py +++ b/src/klein/test/test_resource.py @@ -1117,9 +1117,13 @@ def test_urlDecodeErrorReprPy3(self): ) if _PY3: - test_urlDecodeErrorReprPy2.skip = "Only works on Py2" # type: ignore + test_urlDecodeErrorReprPy2.skip = ( # type: ignore[attr-defined] + "Only works on Py2" + ) else: - test_urlDecodeErrorReprPy3.skip = "Only works on Py3" # type: ignore + test_urlDecodeErrorReprPy3.skip = ( # type: ignore[attr-defined] + "Only works on Py3" + ) def test_subroutedBranch(self): diff --git a/src/klein/test/test_response.py b/src/klein/test/test_response.py index c839f0d8..188bc29b 100644 --- a/src/klein/test/test_response.py +++ b/src/klein/test/test_response.py @@ -8,11 +8,9 @@ from ._trial import TestCase from .test_message import FrozenHTTPMessageTestsMixIn from .._headers import FrozenHTTPHeaders -from .._message import IHTTPMessage +from .._interfaces import IHTTPMessage from .._response import FrozenHTTPResponse, IHTTPResponse -IHTTPMessage # Silence linter - __all__ = () @@ -45,7 +43,7 @@ def test_interface(self): L{FrozenHTTPResponse} implements L{IHTTPResponse}. """ response = self.responseFromBytes() - self.assertProvides(IHTTPResponse, response) + self.assertProvides(IHTTPResponse, response) # type: ignore[misc] def test_initInvalidBodyType(self): diff --git a/src/klein/test/test_session.py b/src/klein/test/test_session.py index b606320f..9ecab3be 100644 --- a/src/klein/test/test_session.py +++ b/src/klein/test/test_session.py @@ -16,7 +16,7 @@ from klein.interfaces import ISession, NoSuchSession, TooLateForCookies from klein.storage.memory import MemorySessionStore, declareMemoryAuthorizer -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from twisted.web.iweb import IRequest from twisted.internet.defer import Deferred from twisted.python.components import Componentized @@ -24,7 +24,8 @@ from typing import Tuple, List sessions = List[ISession] errors = List[NoSuchSession] - IRequest, Deferred, sessions, errors, Tuple, Componentized, IInterface + + class ISimpleTest(Interface): """ @@ -97,7 +98,7 @@ def route(request): requirer = Requirer() - @requirer.prerequisite([ISession]) + @requirer.prerequisite([ISession]) # type: ignore[misc] def procure(request): # type: (IRequest) -> Deferred return sproc.procureSession(request) diff --git a/src/klein/test/test_trial.py b/src/klein/test/test_trial.py index 38c72a8f..b29f1813 100644 --- a/src/klein/test/test_trial.py +++ b/src/klein/test/test_trial.py @@ -7,6 +7,7 @@ from zope.interface import Interface, implementer from ._trial import TestCase +from .._typing import ifmethod __all__ = () @@ -22,6 +23,7 @@ class IFrobbable(Interface): """ Frobbable object. """ + @ifmethod def frob(): # type: () -> None """ diff --git a/tox.ini b/tox.ini index 3fb50104..0d04dab5 100644 --- a/tox.ini +++ b/tox.ini @@ -133,6 +133,12 @@ ignore = # expected 2 blank lines after class or function definition E305, + # syntax error in type comment + F723, + + # undefined name + F821 + # function name should be lowercase N802, @@ -165,11 +171,13 @@ max-complexity = 60 ## -# Mypy linting +# Mypy static type checking ## [testenv:mypy] +description = run Mypy (static type checker) + basepython = python3.7 skip_install = True @@ -181,24 +189,99 @@ deps = commands = "{toxinidir}/.travis/environment" - "{toxinidir}/.travis/mypy" --config-file="{toxinidir}/tox.ini" {posargs:src} + mypy \ + --config-file="{toxinidir}/tox.ini" \ + --cache-dir="{toxworkdir}/mypy_cache" \ + {tty:--pretty:} \ + {posargs:src} [mypy] # Global settings -warn_redundant_casts = True -warn_unused_ignores = True -strict_optional = True -show_column_numbers = True +disallow_incomplete_defs = True +disallow_untyped_defs = True +no_implicit_optional = True +show_column_numbers = True +show_error_codes = True +strict_optional = True +warn_no_return = True +warn_redundant_casts = True +warn_return_any = True +warn_unreachable = True +warn_unused_ignores = True + +# Enable these over time +check_untyped_defs = False + +# Disable some checks until effected files fully adopt mypy + +[mypy-klein._app] +allow_untyped_defs = True + +[mypy-klein._decorators] +allow_untyped_defs = True + +[mypy-klein._iapp] +allow_untyped_defs = True + +[mypy-klein._plating] +allow_untyped_defs = True + +[mypy-klein._resource] +allow_untyped_defs = True + +[mypy-klein.test._trial] +allow_untyped_defs = True + +[mypy-klein.test.py3_test_resource] +allow_untyped_defs = True + +[mypy-klein.test.test_app] +allow_untyped_defs = True + +[mypy-klein.test.test_plating] +allow_untyped_defs = True + +[mypy-klein.test.test_resource] +allow_untyped_defs = True + +[mypy-klein.test.util] +allow_untyped_defs = True + +# Don't complain about dependencies known to lack type hints -# Module default settings -# disallow_untyped_calls = True -disallow_untyped_defs = True -# warn_return_any = True +[mypy-constantly] +ignore_missing_imports = True + +[mypy-incremental] +ignore_missing_imports = True + +[mypy-zope.interface] +[mypy-zope.interface.*] +ignore_missing_imports = True + +[mypy-treq] +ignore_missing_imports = True +[mypy-treq.*] +ignore_missing_imports = True + +[mypy-hyperlink] +ignore_missing_imports = True + +[mypy-hypothesis] +ignore_missing_imports = True +[mypy-hypothesis.*] +ignore_missing_imports = True + +[mypy-idna] +ignore_missing_imports = True + +[mypy-tubes.*] +ignore_missing_imports = True -# Need some stub files to get rid of this +[mypy-twisted.*] ignore_missing_imports = True From 0c84fb62499b178cea452f411c2f6449bc2188c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 27 Oct 2019 23:57:34 -0700 Subject: [PATCH 02/19] pin pyflakes pending a release with https://github.com/PyCQA/pyflakes/pull/455 --- tox.ini | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tox.ini b/tox.ini index 0d04dab5..6626f630 100644 --- a/tox.ini +++ b/tox.ini @@ -97,11 +97,14 @@ deps = flake8-import-order==0.18.1 flake8-mutable==1.2.0 flake8-pep3101==1.2.1 + mccabe==0.6.1 + pep8-naming==0.8.2 + pycodestyle==2.5.0 # Remove this pin when https://gitlab.com/pycqa/flake8-docstrings/issues/36 # is fixed pydocstyle<4.0.0 - pep8-naming==0.8.2 - mccabe==0.6.1 + # pin pyflakes pending a release with https://github.com/PyCQA/pyflakes/pull/455 + git+git://github.com/PyCQA/pyflakes@ffe9386#egg=pyflakes basepython = python3.7 @@ -133,12 +136,6 @@ ignore = # expected 2 blank lines after class or function definition E305, - # syntax error in type comment - F723, - - # undefined name - F821 - # function name should be lowercase N802, From 96d796d8f85f3cc4826819dd17fef2fbef24e247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Mon, 28 Oct 2019 00:02:38 -0700 Subject: [PATCH 03/19] value is optional --- src/klein/_form.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/klein/_form.py b/src/klein/_form.py index 88b227c4..43d17e2f 100644 --- a/src/klein/_form.py +++ b/src/klein/_form.py @@ -97,7 +97,7 @@ class Field(object): default = attr.ib(type=Optional[Any], default=None, cmp=False) required = attr.ib(type=bool, default=True) noLabel = attr.ib(type=bool, default=False) - value = attr.ib(type=Text, default=u"") + value = attr.ib(type=Optional[Text], default=u"") error = attr.ib(type=ValidationError, default=None) # IRequiredParameter @@ -146,9 +146,7 @@ def asTags(self): """ value = self.value if self.value is None: - # FIXME: mypy says this is unreachable because self.value is not - # optional. Should it be? - value = "" # type: ignore[misc] + value = "" input_tag = tags.input( type=self.formInputType, name=self.formFieldName, value=value ) From 4e77d89381f94445e7687acc442750a208f15f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Mon, 4 Nov 2019 14:58:58 -0800 Subject: [PATCH 04/19] Add more type hints --- src/klein/__init__.py | 28 ++++---- src/klein/_app.py | 113 +++++++++++++++++++++++++------- src/klein/_decorators.py | 24 +++++-- src/klein/_form.py | 13 +--- src/klein/_iapp.py | 18 ----- src/klein/_interfaces.py | 27 +++++++- src/klein/_session.py | 16 ++--- src/klein/_typing.py | 15 ++++- src/klein/test/test_resource.py | 6 +- tox.ini | 6 -- 10 files changed, 176 insertions(+), 90 deletions(-) delete mode 100644 src/klein/_iapp.py diff --git a/src/klein/__init__.py b/src/klein/__init__.py index 96eb3961..889fce48 100644 --- a/src/klein/__init__.py +++ b/src/klein/__init__.py @@ -2,7 +2,10 @@ from typing import TYPE_CHECKING -from ._app import Klein, handle_errors, route, run, urlFor, url_for +from ._app import ( + Klein, KleinErrorHandler, KleinRenderable, KleinRoute, + handle_errors, route, run, urlFor, url_for, +) from ._dihttp import RequestComponent, RequestURL, Response from ._form import Field, FieldValues, Form, RenderableForm from ._plating import Plating @@ -19,17 +22,20 @@ __all__ = ( "Klein", + "KleinErrorHandler", + "KleinRenderable", + "KleinRoute", "Plating", - 'Field', - 'FieldValues', - 'Form', - 'RequestComponent', - 'RequestURL', - 'Response', - 'RenderableForm', - 'SessionProcurer', - 'Authorization', - 'Requirer', + "Field", + "FieldValues", + "Form", + "RequestComponent", + "RequestURL", + "Response", + "RenderableForm", + "SessionProcurer", + "Authorization", + "Requirer", "__author__", "__copyright__", "__license__", diff --git a/src/klein/_app.py b/src/klein/_app.py index 6997e0e6..dc756ceb 100644 --- a/src/klein/_app.py +++ b/src/klein/_app.py @@ -11,33 +11,53 @@ try: from inspect import iscoroutine except ImportError: - def iscoroutine(*args, **kwargs): # type: ignore[misc] + def iscoroutine(obj): # type: ignore[misc] + # type: (object) -> bool return False -from typing import IO, Optional +from typing import ( + Any, Callable, Dict, IO, List, Mapping, Optional, Text, Union, cast +) from weakref import ref from twisted.internet import endpoints, reactor +from twisted.internet.defer import Deferred from twisted.python import log from twisted.python.components import registerAdapter +from twisted.python.failure import Failure +from twisted.web.iweb import IRenderable, IRequest +from twisted.web.resource import IResource from twisted.web.server import Request, Site try: from twisted.internet.defer import ensureDeferred except ImportError: - def ensureDeferred(*args, **kwagrs): + def ensureDeferred(coro): + # type: (Awaitable) -> Deferred raise NotImplementedError("Coroutines support requires Twisted>=16.6") -from werkzeug.routing import Map, Rule, Submount +from werkzeug.routing import Map, MapAdapter, Rule, Submount from zope.interface import implementer from ._decorators import modified, named from ._interfaces import IKleinRequest from ._resource import KleinResource +from ._typing import Awaitable, KwArg, VarArg +KleinSynchronousRenderable = Union[str, bytes, IResource, IRenderable] +KleinRenderable = Union[ + KleinSynchronousRenderable, Awaitable[KleinSynchronousRenderable] +] +KleinRoute = Callable[ + [Any, IRequest, VarArg(Any), KwArg(Any)], + KleinRenderable +] +KleinErrorHandler = Callable[["Klein", IRequest, Failure], KleinRenderable] + def _call(__klein_instance__, __klein_f__, *args, **kwargs): + # type: (Optional[Klein], Callable, Any, Any) -> Deferred """ Call C{__klein_f__} with the given C{*args} and C{**kwargs}. @@ -50,7 +70,7 @@ def _call(__klein_instance__, __klein_f__, *args, **kwargs): C{ensureDeferred} on it. """ if __klein_instance__ is not None or getattr( - __klein_f__, "__klein_bound__", False + __klein_f__, "__klein_bound__", False ): args = (__klein_instance__,) + args result = __klein_f__(*args, **kwargs) @@ -64,11 +84,30 @@ def _call(__klein_instance__, __klein_f__, *args, **kwargs): class KleinRequest(object): def __init__(self, request): + # type: (Request) -> None self.branch_segments = [''] - self.mapper = None - def url_for(self, *args, **kwargs): - return self.mapper.build(*args, **kwargs) + # Don't annotate as optional, since you should never set this to None + self.mapper = None # type: MapAdapter # type: ignore[assignment] + + + def url_for( + self, + endpoint, # type: Text + values=None, # type: Optional[Mapping[Text, Text]] + method=None, # type: Optional[Text] + force_external=False, # type: bool + append_unknown=True, # type: bool + ): + # type: (...) -> Text + assert self.mapper is not None + return cast(Text, self.mapper.build( + endpoint=endpoint, + values=values, + method=method, + force_external=force_external, + append_unknown=append_unknown, + )) registerAdapter(KleinRequest, Request, IKleinRequest) @@ -88,30 +127,32 @@ class Klein(object): _subroute_segments = 0 def __init__(self): + # type: () -> None self._url_map = Map() - self._endpoints = {} - self._error_handlers = [] - self._instance = None - self._boundAs = None + self._endpoints = {} # type: Dict[Text, KleinRoute] + self._error_handlers = [] # type: List[KleinErrorHandler] + self._instance = None # type: Klein # type: ignore[assignment] + self._boundAs = None # type: Optional[Text] def __eq__(self, other): + # type: (Any) -> bool if isinstance(other, Klein): return vars(self) == vars(other) return NotImplemented def __ne__(self, other): + # type: (Any) -> bool result = self.__eq__(other) if result is NotImplemented: return result return not result - - @property def url_map(self): + # type: () -> Map """ Read only property exposing L{Klein._url_map}. """ @@ -120,22 +161,25 @@ def url_map(self): @property def endpoints(self): + # type: () -> Dict[Text, KleinRoute] """ Read only property exposing L{Klein._endpoints}. """ return self._endpoints - def execute_endpoint(self, endpoint, *args, **kwargs): + def execute_endpoint(self, endpoint, request, *args, **kwargs): + # type: (Text, IRequest, Any, Any) -> KleinRenderable """ Execute the named endpoint with all arguments and possibly a bound instance. """ endpoint_f = self._endpoints[endpoint] - return endpoint_f(self._instance, *args, **kwargs) + return endpoint_f(self._instance, request, *args, **kwargs) def execute_error_handler(self, handler, request, failure): + # type: (KleinErrorHandler, IRequest, Failure) -> KleinRenderable """ Execute the passed error handler, possibly with a bound instance. """ @@ -154,6 +198,7 @@ def resource(self): def __get__(self, instance, owner): + # type: (Any, object) -> Klein """ Get an instance of L{Klein} bound to C{instance}. """ @@ -173,7 +218,10 @@ def __get__(self, instance, owner): self._boundAs = 'unknown_' + str(id(self)) boundName = "__klein_bound_{}__".format(self._boundAs) - k = getattr(instance, boundName, lambda: None)() + k = cast( + Optional["Klein"], + getattr(instance, boundName, lambda: None)() + ) if k is None: k = self.__class__() @@ -192,6 +240,7 @@ def __get__(self, instance, owner): @staticmethod def _segments_in_url(url): + # type: (Text) -> int segment_count = url.count('/') if url.endswith('/'): segment_count -= 1 @@ -225,6 +274,7 @@ def index(request): @named("router for '" + url + "'") def deco(f): + # type: (KleinRoute) -> KleinRoute kwargs.setdefault('endpoint', f.__name__) if kwargs.pop('branch', False): branchKwargs = kwargs.copy() @@ -232,12 +282,17 @@ def deco(f): @modified("branch route '{url}' executor".format(url=url), f) def branch_f(instance, request, *a, **kw): + # type: (Any, IRequest, Any, Any) -> KleinRenderable IKleinRequest(request).branch_segments = ( kw.pop('__rest__', '').split('/') ) return _call(instance, f, request, *a, **kw) - branch_f.segment_count = segment_count + branch_f = cast(KleinRoute, branch_f) + + branch_f.segment_count = ( # type: ignore[attr-defined] + segment_count + ) self._endpoints[branchKwargs['endpoint']] = branch_f self._url_map.add( @@ -249,9 +304,12 @@ def branch_f(instance, request, *a, **kw): @modified("route '{url}' executor".format(url=url), f) def _f(instance, request, *a, **kw): + # type: (Any, IRequest, Any, Any) -> KleinRenderable return _call(instance, f, request, *a, **kw) - _f.segment_count = segment_count + _f = cast(KleinRoute, _f) + + _f.segment_count = segment_count # type: ignore[attr-defined] self._endpoints[kwargs['endpoint']] = _f self._url_map.add(Rule(url, *args, **kwargs)) @@ -377,16 +435,25 @@ def _f(instance, request, failure): return deco - def urlFor(self, request, endpoint, values=None, method=None, - force_external=False, append_unknown=True): + def urlFor( + self, + request, # type: IKleinRequest + endpoint, # type: Text + values=None, # type: Optional[Mapping[Text, Text]] + method=None, # type: Optional[Text] + force_external=False, # type: bool + append_unknown=True, # type: bool + ): + # type: (...) -> Text host = request.getHeader(b'host') if host is None: if force_external: raise ValueError("Cannot build external URL if request" " doesn't contain Host header") host = b'' - return self.url_map.bind(host).build(endpoint, values, method, - force_external, append_unknown) + return cast(Text, self.url_map.bind(host).build( + endpoint, values, method, force_external, append_unknown) + ) url_for = urlFor diff --git a/src/klein/_decorators.py b/src/klein/_decorators.py index 44e49afe..75e742eb 100644 --- a/src/klein/_decorators.py +++ b/src/klein/_decorators.py @@ -1,7 +1,11 @@ from functools import wraps +from typing import Callable, Text, TypeVar, cast + +C = TypeVar("C", bound=Callable) def bindable(bindable): + # type: (C) -> C """ Mark a method as a "bindable" method. @@ -20,11 +24,15 @@ def bindable(bindable): @return: its argument, modified to mark it as unconditinally requiring an instance argument. """ - bindable.__klein_bound__ = True + bindable.__klein_bound__ = True # type: ignore[attr-defined] return bindable -def modified(modification, original, modifier=None): +def modified( # type: ignore[no-untyped-def] + modification, original, modifier=None +): + # FIXME: This maybe isn't quite right + # __type: (Text, C, Optional[Callable[[C], C]]) -> Callable[[C], C] """ Annotate a callable as a modified wrapper of an original callable. @@ -44,9 +52,10 @@ def modified(modification, original, modifier=None): likely calls it. """ def decorator(wrapper): - result = (named(modification + ' for ' + original.__name__) - (wraps(original)(wrapper))) - result.__original__ = original + # type: (C) -> C + namer = named(modification + ' for ' + original.__name__) + result = cast(C, namer(wraps(original)(wrapper))) + result.__original__ = original # type: ignore[attr-defined] if modifier is not None: before = set(wrapper.__dict__.keys()) result = modifier(result) @@ -58,10 +67,12 @@ def decorator(wrapper): def named(name): + # type: (Text) -> Callable[[C], C] """ Change the name of a function to the given name. """ def decorator(original): + # type: (C) -> C original.__name__ = str(name) original.__qualname__ = str(name) return original @@ -69,6 +80,7 @@ def decorator(original): def originalName(function): + # type: (Callable) -> Text """ Get the original, user-specified name of C{function}, chasing back any wrappers applied with C{modified}. @@ -77,4 +89,4 @@ def originalName(function): while fnext is not None: function = fnext fnext = getattr(function, "__original__", None) - return function.__name__ + return function.__name__ # type: ignore[misc] diff --git a/src/klein/_form.py b/src/klein/_form.py index 43d17e2f..45035bde 100644 --- a/src/klein/_form.py +++ b/src/klein/_form.py @@ -5,12 +5,12 @@ import json from typing import ( Any, AnyStr, Callable, Dict, Iterable, List, Optional, Sequence, - TYPE_CHECKING, Text, cast, + Text, Type, cast, ) import attr -from twisted.internet.defer import inlineCallbacks +from twisted.internet.defer import Deferred, inlineCallbacks from twisted.python.components import Componentized, registerAdapter from twisted.web.error import MissingRenderMethod from twisted.web.http import FORBIDDEN @@ -22,19 +22,12 @@ from ._app import _call from ._decorators import bindable +from ._typing import DefaultNamedArg, NoReturn from .interfaces import ( EarlyExit, IDependencyInjector, IRequestLifecycle, IRequiredParameter, ISession, SessionMechanism, ValidationError, ValueAbsent, ) -if TYPE_CHECKING: # pragma: no cover - from typing import Type - from mypy_extensions import DefaultNamedArg, NoReturn - from twisted.internet.defer import Deferred -else: - def DefaultNamedArg(*ignore): - pass - class CrossSiteRequestForgery(Resource, object): diff --git a/src/klein/_iapp.py b/src/klein/_iapp.py deleted file mode 100644 index 0c2a7abc..00000000 --- a/src/klein/_iapp.py +++ /dev/null @@ -1,18 +0,0 @@ -from zope.interface import Attribute, Interface - -from ._typing import ifmethod - - - -class IKleinRequest(Interface): - branch_segments = Attribute("Segments consumed by a branch route.") - mapper = Attribute("L{werkzeug.routing.MapAdapter}") - - @ifmethod - def url_for( - self, endpoint, values=None, method=None, - force_external=False, append_unknown=True, - ): - """ - L{werkzeug.routing.MapAdapter.build} - """ diff --git a/src/klein/_interfaces.py b/src/klein/_interfaces.py index ebd92d32..78ad89ee 100644 --- a/src/klein/_interfaces.py +++ b/src/klein/_interfaces.py @@ -7,9 +7,10 @@ works, since mypy doesn't otherwise get along with Zope Interface. """ -from typing import TYPE_CHECKING +from typing import Mapping, Optional, TYPE_CHECKING, Text + +from zope.interface import Attribute, Interface -from ._iapp import IKleinRequest from ._imessage import ( IHTTPHeaders as _IHTTPHeaders, IHTTPMessage as _IHTTPMessage, @@ -17,8 +18,28 @@ IHTTPResponse as _IHTTPResponse, IMutableHTTPHeaders as _IMutableHTTPHeaders, ) +from ._typing import ifmethod + + + +class IKleinRequest(Interface): + branch_segments = Attribute("Segments consumed by a branch route.") + mapper = Attribute("L{werkzeug.routing.MapAdapter}") + + @ifmethod + def url_for( + request, # type: IKleinRequest + endpoint, # type: Text + values=None, # type: Optional[Mapping[Text, Text]] + method=None, # type: Optional[Text] + force_external=False, # type: bool + append_unknown=True, # type: bool + ): + # type: (...) -> Text + """ + L{werkzeug.routing.MapAdapter.build} + """ -IKleinRequest if TYPE_CHECKING: # pragma: no cover diff --git a/src/klein/_session.py b/src/klein/_session.py index c9c175e0..95eed63b 100644 --- a/src/klein/_session.py +++ b/src/klein/_session.py @@ -1,33 +1,29 @@ # -*- test-case-name: klein.test.test_session -*- from typing import ( - Any, Callable, Optional as _Optional, TYPE_CHECKING, Union + Any, Callable, Dict, Optional, Sequence, Text, TypeVar, Union ) import attr from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.python.components import Componentized from twisted.python.reflect import qual from twisted.web.http import UNAUTHORIZED +from twisted.web.iweb import IRequest from twisted.web.resource import Resource from zope.interface import implementer from zope.interface.interfaces import IInterface +from ._typing import Arg, KwArg from .interfaces import ( EarlyExit, IDependencyInjector, IRequestLifecycle, IRequiredParameter, ISession, ISessionProcurer, ISessionStore, NoSuchSession, SessionMechanism, TooLateForCookies ) -if TYPE_CHECKING: # pragma: no cover - from twisted.web.iweb import IRequest - from twisted.python.components import Componentized - from mypy_extensions import KwArg, Arg - from typing import Dict, Sequence, Text, TypeVar - T = TypeVar('T') -else: - Arg = KwArg = lambda t, *x: t +T = TypeVar('T') @@ -79,7 +75,7 @@ class SessionProcurer(object): _maxAge = attr.ib(type=int, default=3600) _secureCookie = attr.ib(type=bytes, default=b"Klein-Secure-Session") _insecureCookie = attr.ib(type=bytes, default=b"Klein-INSECURE-Session") - _cookieDomain = attr.ib(type=_Optional[bytes], default=None) + _cookieDomain = attr.ib(type=Optional[bytes], default=None) _cookiePath = attr.ib(type=bytes, default=b"/") _secureTokenHeader = attr.ib(type=bytes, default=b"X-Auth-Token") diff --git a/src/klein/_typing.py b/src/klein/_typing.py index 4db35b39..32e25480 100644 --- a/src/klein/_typing.py +++ b/src/klein/_typing.py @@ -1,4 +1,8 @@ -from typing import Callable, TYPE_CHECKING +from typing import Callable, TYPE_CHECKING, Union +try: + from typing import Awaitable +except ImportError: + Awaitable = Union # type: ignore __all__ = () @@ -10,6 +14,15 @@ def _ifmethod(method): if TYPE_CHECKING: # pragma: no cover + from mypy_extensions import Arg, DefaultNamedArg, KwArg, NoReturn, VarArg + ifmethod = staticmethod else: + Arg, KwArg = VarArg = lambda t, *x: t + + def DefaultNamedArg(*ignore): + pass + + NoReturn = None + ifmethod = _ifmethod diff --git a/src/klein/test/test_resource.py b/src/klein/test/test_resource.py index cdca3f8a..90ba37a1 100644 --- a/src/klein/test/test_resource.py +++ b/src/klein/test/test_resource.py @@ -17,6 +17,7 @@ from twisted.trial.unittest import SynchronousTestCase from twisted.web import server from twisted.web.http_headers import Headers +from twisted.web.iweb import IRequest from twisted.web.resource import Resource from twisted.web.static import File from twisted.web.template import Element, XMLString, renderer @@ -25,7 +26,7 @@ from werkzeug.exceptions import NotFound from .util import EqualityTestsMixin -from .. import Klein +from .. import Klein, KleinRenderable from .._interfaces import IKleinRequest from .._resource import ( KleinResource, _URLDecodeError, _extractURLparts, ensure_utf8_bytes @@ -208,7 +209,8 @@ class _One(object): oneKlein = Klein() @oneKlein.route("/foo") - def foo(self): + def foo(self, resource): + # type: (IRequest) -> KleinRenderable pass _one = _One() diff --git a/tox.ini b/tox.ini index 6626f630..b689d7e6 100644 --- a/tox.ini +++ b/tox.ini @@ -217,12 +217,6 @@ check_untyped_defs = False [mypy-klein._app] allow_untyped_defs = True -[mypy-klein._decorators] -allow_untyped_defs = True - -[mypy-klein._iapp] -allow_untyped_defs = True - [mypy-klein._plating] allow_untyped_defs = True From 86a4ad1805fc7f4c3192c0371d459ab9c994214a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Mon, 4 Nov 2019 19:05:49 -0800 Subject: [PATCH 05/19] Extra space, ew --- src/klein/_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/klein/_app.py b/src/klein/_app.py index dc756ceb..d95379b8 100644 --- a/src/klein/_app.py +++ b/src/klein/_app.py @@ -437,7 +437,7 @@ def _f(instance, request, failure): def urlFor( self, - request, # type: IKleinRequest + request, # type: IKleinRequest endpoint, # type: Text values=None, # type: Optional[Mapping[Text, Text]] method=None, # type: Optional[Text] From 51d24e84ff91b2a625cc2b24a3ae59bc92273cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 5 Nov 2019 08:21:15 -0800 Subject: [PATCH 06/19] On review, looks value=None isn't meant to be allowed and the check for it is more a belt-and-suspenders thing. --- src/klein/_form.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/klein/_form.py b/src/klein/_form.py index 45035bde..933871d8 100644 --- a/src/klein/_form.py +++ b/src/klein/_form.py @@ -90,7 +90,7 @@ class Field(object): default = attr.ib(type=Optional[Any], default=None, cmp=False) required = attr.ib(type=bool, default=True) noLabel = attr.ib(type=bool, default=False) - value = attr.ib(type=Optional[Text], default=u"") + value = attr.ib(type=Text, default=u"") error = attr.ib(type=ValidationError, default=None) # IRequiredParameter @@ -138,8 +138,8 @@ def asTags(self): @rtype: iterable of L{twisted.web.template.Tag} """ value = self.value - if self.value is None: - value = "" + if value is None: + value = "" # type: ignore[misc] input_tag = tags.input( type=self.formInputType, name=self.formFieldName, value=value ) From e88b73aa6110545f66c0fc824eb33d80fe2a24a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 5 Nov 2019 11:57:07 -0800 Subject: [PATCH 07/19] typo --- src/klein/_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/klein/_typing.py b/src/klein/_typing.py index 32e25480..ac38936e 100644 --- a/src/klein/_typing.py +++ b/src/klein/_typing.py @@ -18,7 +18,7 @@ def _ifmethod(method): ifmethod = staticmethod else: - Arg, KwArg = VarArg = lambda t, *x: t + Arg = KwArg = VarArg = lambda t, *x: t def DefaultNamedArg(*ignore): pass From f8358532c53b80c02b17a7506ab6dab3bc4a98e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 20 Nov 2019 13:46:50 -0800 Subject: [PATCH 08/19] Add disallow_any_generics to TODO list --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index b689d7e6..6289c4d4 100644 --- a/tox.ini +++ b/tox.ini @@ -211,6 +211,7 @@ warn_unused_ignores = True # Enable these over time check_untyped_defs = False +disallow_any_generics = False # Disable some checks until effected files fully adopt mypy From 47f5e53cfda286838d90f6a78066c9514cf371f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 20 Nov 2019 14:13:06 -0800 Subject: [PATCH 09/19] Make `self._instance` optional. --- src/klein/_app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/klein/_app.py b/src/klein/_app.py index d95379b8..cf8522d0 100644 --- a/src/klein/_app.py +++ b/src/klein/_app.py @@ -53,7 +53,9 @@ def ensureDeferred(coro): [Any, IRequest, VarArg(Any), KwArg(Any)], KleinRenderable ] -KleinErrorHandler = Callable[["Klein", IRequest, Failure], KleinRenderable] +KleinErrorHandler = Callable[ + [Optional["Klein"], IRequest, Failure], KleinRenderable +] def _call(__klein_instance__, __klein_f__, *args, **kwargs): @@ -131,7 +133,7 @@ def __init__(self): self._url_map = Map() self._endpoints = {} # type: Dict[Text, KleinRoute] self._error_handlers = [] # type: List[KleinErrorHandler] - self._instance = None # type: Klein # type: ignore[assignment] + self._instance = None # type: Optional[Klein] self._boundAs = None # type: Optional[Text] From 8b85c82faead1b0eacacbc597ee76510fafe1abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 20 Nov 2019 15:29:54 -0800 Subject: [PATCH 10/19] Less of `if TYPE_CHECKING` --- src/klein/_dihttp.py | 18 ++++++++---------- src/klein/_plating.py | 16 ++++++---------- src/klein/test/test_form.py | 11 +++-------- src/klein/test/test_session.py | 20 +++++++++----------- 4 files changed, 26 insertions(+), 39 deletions(-) diff --git a/src/klein/_dihttp.py b/src/klein/_dihttp.py index c6a441ee..f40ab6b0 100644 --- a/src/klein/_dihttp.py +++ b/src/klein/_dihttp.py @@ -2,25 +2,23 @@ Dependency-Injected HTTP metadata. """ -from typing import Any, Mapping, Sequence, TYPE_CHECKING, Text, Union +from typing import Any, Dict, Mapping, Sequence, Text, Union import attr -from hyperlink import parse +from hyperlink import DecodedURL, parse from six import text_type +from twisted.python.components import Componentized +from twisted.web.iweb import IRequest + from zope.interface import implementer, provider from zope.interface.interfaces import IInterface -from .interfaces import IDependencyInjector, IRequiredParameter - -if TYPE_CHECKING: # pragma: no cover - from hyperlink import DecodedURL - from typing import Dict - from klein.interfaces import IRequestLifecycle - from twisted.web.iweb import IRequest - from twisted.python.components import Componentized +from .interfaces import ( + IDependencyInjector, IRequestLifecycle, IRequiredParameter +) def urlFromRequest(request): diff --git a/src/klein/_plating.py b/src/klein/_plating.py index 1171c3f9..1f231bf1 100644 --- a/src/klein/_plating.py +++ b/src/klein/_plating.py @@ -7,26 +7,22 @@ from functools import partial from json import dumps from operator import setitem -from typing import Any, Callable, TYPE_CHECKING, Tuple, cast +from typing import Any, Callable, List, Tuple, cast import attr from six import integer_types, string_types, text_type -from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.defer import Deferred, inlineCallbacks, returnValue from twisted.web.error import MissingRenderMethod -from twisted.web.template import Element, TagLoader +from twisted.web.iweb import IRequest +from twisted.web.template import Element, Tag, TagLoader from ._app import _call from ._decorators import bindable, modified, originalName -if TYPE_CHECKING: # pragma: no cover - from twisted.internet.defer import Deferred - from twisted.web.iweb import IRequest - from twisted.web.template import Tag - from typing import List - Deferred, IRequest, Tag - StackType = List[Tuple[Any, Callable[[Any], None]]] + +StackType = List[Tuple[Any, Callable[[Any], None]]] # https://github.com/python/mypy/issues/224 ATOM_TYPES = ( diff --git a/src/klein/test/test_form.py b/src/klein/test/test_form.py index 23a35e14..93828958 100644 --- a/src/klein/test/test_form.py +++ b/src/klein/test/test_form.py @@ -1,5 +1,4 @@ - -from typing import List, TYPE_CHECKING, Text, cast +from typing import Any, List, Text, Tuple, cast from xml.etree import ElementTree import attr @@ -10,20 +9,16 @@ from twisted.internet.defer import inlineCallbacks from twisted.python.compat import nativeString from twisted.trial.unittest import SynchronousTestCase +from twisted.web.iweb import IRequest from twisted.web.template import Element, TagLoader, renderer, tags -from klein import Field, Form, Klein, Requirer, SessionProcurer +from klein import Field, Form, Klein, RenderableForm, Requirer, SessionProcurer from klein.interfaces import ( ISession, ISessionStore, NoSuchSession, SessionMechanism, ValidationError ) from klein.storage.memory import MemorySessionStore -if TYPE_CHECKING: # pragma: no cover - from typing import Any, Tuple - from twisted.web.iweb import IRequest - from klein import RenderableForm - class DanglingField(Field): diff --git a/src/klein/test/test_session.py b/src/klein/test/test_session.py index 9ecab3be..9565a22f 100644 --- a/src/klein/test/test_session.py +++ b/src/klein/test/test_session.py @@ -2,28 +2,26 @@ Tests for L{klein._session}. """ -from typing import TYPE_CHECKING +from typing import List, Tuple from treq.testing import StubTreq -from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.defer import Deferred, inlineCallbacks, returnValue +from twisted.python.components import Componentized from twisted.trial.unittest import SynchronousTestCase +from twisted.web.iweb import IRequest from zope.interface import Interface, implementer +from zope.interface.interfaces import IInterface from klein import Authorization, Klein, Requirer, SessionProcurer from klein._typing import ifmethod from klein.interfaces import ISession, NoSuchSession, TooLateForCookies from klein.storage.memory import MemorySessionStore, declareMemoryAuthorizer -if TYPE_CHECKING: # pragma: no cover - from twisted.web.iweb import IRequest - from twisted.internet.defer import Deferred - from twisted.python.components import Componentized - from zope.interface.interfaces import IInterface - from typing import Tuple, List - sessions = List[ISession] - errors = List[NoSuchSession] + +Sessions = List[ISession] +Errors = List[NoSuchSession] @@ -73,7 +71,7 @@ def memoryAuthorizer(interface, session, data): def simpleSessionRouter(): - # type: () -> Tuple[sessions, errors, str, str, StubTreq] + # type: () -> Tuple[Sessions, Errors, str, str, StubTreq] """ Construct a simple router. """ From 8aeef1bfd4b935b5ff23547f9b79bbeafdae5076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 20 Nov 2019 15:40:37 -0800 Subject: [PATCH 11/19] Less `if TYPE_CHECKING` --- src/klein/_isession.py | 17 +++++++---------- src/klein/_requirer.py | 17 +++++++---------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/klein/_isession.py b/src/klein/_isession.py index 9ffa78ef..c6b3432d 100644 --- a/src/klein/_isession.py +++ b/src/klein/_isession.py @@ -1,24 +1,22 @@ -from typing import Any, TYPE_CHECKING +from typing import Any, Dict, Iterable, Sequence, TYPE_CHECKING, Text, Union import attr try: from constantly import NamedConstant, Names -except ImportError: # pragma: no cover +except ImportError: # pragma: no cover from twisted.python.constants import NamedConstant, Names +from twisted.internet.defer import Deferred +from twisted.python.components import Componentized +from twisted.web.iweb import IRequest + from zope.interface import Attribute, Interface +from zope.interface.interfaces import IInterface from ._typing import ifmethod -if TYPE_CHECKING: # pragma: no cover - from twisted.internet.defer import Deferred - from twisted.python.components import Componentized - from typing import Dict, Iterable, Text, Sequence - from twisted.web.iweb import IRequest - from zope.interface.interfaces import IInterface - class NoSuchSession(Exception): @@ -375,7 +373,6 @@ class IRequestLifecycle(Interface): if TYPE_CHECKING: # pragma: no cover - from typing import Union from ._requirer import RequestLifecycle IRequestLifecycleT = Union[RequestLifecycle, IRequestLifecycle] diff --git a/src/klein/_requirer.py b/src/klein/_requirer.py index e132d5e8..395fce36 100644 --- a/src/klein/_requirer.py +++ b/src/klein/_requirer.py @@ -1,23 +1,20 @@ -from typing import Any, Callable, List, TYPE_CHECKING +from typing import Any, Callable, Dict, List, Sequence import attr -from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.defer import Deferred, inlineCallbacks, returnValue from twisted.python.components import Componentized +from twisted.web.iweb import IRequest from zope.interface import implementer +from zope.interface.interfaces import IInterface from ._app import _call from ._decorators import bindable, modified -from .interfaces import EarlyExit, IRequestLifecycle - -if TYPE_CHECKING: # pragma: no cover - from typing import Dict, Sequence - from twisted.web.iweb import IRequest - from twisted.internet.defer import Deferred - from zope.interface.interfaces import IInterface - from .interfaces import IDependencyInjector, IRequiredParameter +from .interfaces import ( + EarlyExit, IDependencyInjector, IRequestLifecycle, IRequiredParameter +) From 74310705348d172e677c1f5c26fca1de11622a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 20 Nov 2019 15:47:14 -0800 Subject: [PATCH 12/19] Unrelated. --- src/klein/_form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/klein/_form.py b/src/klein/_form.py index 933871d8..3ccb8817 100644 --- a/src/klein/_form.py +++ b/src/klein/_form.py @@ -1,6 +1,6 @@ # -*- test-case-name: klein.test.test_form -*- -from __future__ import unicode_literals +from __future__ import print_function, unicode_literals import json from typing import ( From 7e04736b8bfa8a644bafc0baba8bcdee9312917e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 20 Nov 2019 16:12:35 -0800 Subject: [PATCH 13/19] Add type hints for txrequest files. --- src/klein/_headers_compat.py | 6 ++++-- src/klein/_request_compat.py | 4 ++-- src/klein/_tubes.py | 6 +++--- src/klein/test/test_headers_compat.py | 9 ++++++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/klein/_headers_compat.py b/src/klein/_headers_compat.py index 427b5212..275c1954 100644 --- a/src/klein/_headers_compat.py +++ b/src/klein/_headers_compat.py @@ -5,7 +5,7 @@ Support for interoperability with L{twisted.web.http_headers.Headers}. """ -from typing import AnyStr, Iterable, Text, Tuple +from typing import AnyStr, Iterable, Text, Tuple, cast from attr import attrib, attrs from attr.validators import instance_of @@ -57,7 +57,9 @@ def pairs(): def getValues(self, name): # type: (AnyStr) -> Iterable[AnyStr] if isinstance(name, bytes): - values = self._headers.getRawHeaders(name, default=()) + values = cast( + Iterable[AnyStr], self._headers.getRawHeaders(name, default=()) + ) elif isinstance(name, Text): values = ( headerValueAsText(value) diff --git a/src/klein/_request_compat.py b/src/klein/_request_compat.py index c3108a9e..e6b3c0ee 100644 --- a/src/klein/_request_compat.py +++ b/src/klein/_request_compat.py @@ -6,7 +6,7 @@ """ from io import BytesIO -from typing import Text +from typing import Text, cast from attr import Factory, attrib, attrs from attr.validators import provides @@ -54,7 +54,7 @@ class HTTPRequestWrappingIRequest(object): @property def method(self): # type: () -> Text - return self._request.method.decode("ascii") + return cast(Text, self._request.method.decode("ascii")) @property diff --git a/src/klein/_tubes.py b/src/klein/_tubes.py index 07784e9c..c1e72642 100644 --- a/src/klein/_tubes.py +++ b/src/klein/_tubes.py @@ -5,7 +5,7 @@ """ from io import BytesIO -from typing import BinaryIO, Iterable +from typing import Any as UnknownType, BinaryIO, Iterable from attr import attrib, attrs from attr.validators import instance_of, optional, provides @@ -82,12 +82,12 @@ def flowTo(self, drain): def pauseFlow(self): - # type: () -> None + # type: () -> UnknownType return self._pauser.pause() def stopFlow(self): - # type: () -> None + # type: () -> UnknownType return self._pauser.resume() diff --git a/src/klein/test/test_headers_compat.py b/src/klein/test/test_headers_compat.py index 672a8744..381e183a 100644 --- a/src/klein/test/test_headers_compat.py +++ b/src/klein/test/test_headers_compat.py @@ -5,7 +5,7 @@ Tests for L{klein._headers}. """ -from typing import Text +from typing import Text, cast from twisted.web.http_headers import Headers @@ -16,12 +16,12 @@ ) from .._headers_compat import HTTPHeadersWrappingHeaders - try: from twisted.web.http_headers import _sanitizeLinearWhitespace except ImportError: _sanitizeLinearWhitespace = None + def _twistedHeaderNormalize(value): # type: (Text) -> Text """ @@ -31,7 +31,10 @@ def _twistedHeaderNormalize(value): if _sanitizeLinearWhitespace is None: return value else: - return _sanitizeLinearWhitespace(value.encode("utf-8")).decode("utf-8") + return cast( + Text, + _sanitizeLinearWhitespace(value.encode("utf-8")).decode("utf-8") + ) __all__ = () From 28f5ee76327040c66d6c037d13ef0a66b171a948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 20 Nov 2019 16:17:18 -0800 Subject: [PATCH 14/19] Remove cast --- src/klein/test/test_headers_compat.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/klein/test/test_headers_compat.py b/src/klein/test/test_headers_compat.py index 381e183a..95901ac9 100644 --- a/src/klein/test/test_headers_compat.py +++ b/src/klein/test/test_headers_compat.py @@ -5,7 +5,7 @@ Tests for L{klein._headers}. """ -from typing import Text, cast +from typing import Text from twisted.web.http_headers import Headers @@ -28,13 +28,12 @@ def _twistedHeaderNormalize(value): Normalize the given header value according to the rules of the installed Twisted version. """ - if _sanitizeLinearWhitespace is None: - return value - else: - return cast( - Text, - _sanitizeLinearWhitespace(value.encode("utf-8")).decode("utf-8") - ) + if _sanitizeLinearWhitespace is not None: + valueBytes = value.encode("utf-8") + valueBytes = _sanitizeLinearWhitespace(valueBytes) + value = valueBytes.decode("utf-8") + + return value __all__ = () From e237808d4025aab8959a48ac83c080fce1b7536e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 20 Nov 2019 16:41:12 -0800 Subject: [PATCH 15/19] Add type hints for _decorators.py --- src/klein/_decorators.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/klein/_decorators.py b/src/klein/_decorators.py index 528ecc5c..1c22b25e 100644 --- a/src/klein/_decorators.py +++ b/src/klein/_decorators.py @@ -1,6 +1,11 @@ from functools import wraps +from typing import Callable, Text, TypeVar, cast + +C = TypeVar("C", bound=Callable) + def bindable(bindable): + # type: (C) -> C """ Mark a method as a "bindable" method. @@ -19,11 +24,15 @@ def bindable(bindable): @return: its argument, modified to mark it as unconditinally requiring an instance argument. """ - bindable.__klein_bound__ = True + bindable.__klein_bound__ = True # type: ignore[attr-defined] return bindable -def modified(modification, original, modifier=None): +def modified( + modification, original, modifier=None +): + # FIXME: This maybe isn't quite right + # __type: (Text, C, Optional[Callable[[C], C]]) -> Callable[[C], C] """ Annotate a callable as a modified wrapper of an original callable. @@ -43,9 +52,10 @@ def modified(modification, original, modifier=None): likely calls it. """ def decorator(wrapper): - result = (named(modification + ' for ' + original.__name__) - (wraps(original)(wrapper))) - result.__original__ = original + # type: (C) -> C + namer = named(modification + ' for ' + original.__name__) + result = cast(C, namer(wraps(original)(wrapper))) + result.__original__ = original # type: ignore[attr-defined] if modifier is not None: before = set(wrapper.__dict__.keys()) result = modifier(result) @@ -57,10 +67,12 @@ def decorator(wrapper): def named(name): + # type: (Text) -> Callable[[C], C] """ Change the name of a function to the given name. """ def decorator(original): + # type: (C) -> C original.__name__ = str(name) original.__qualname__ = str(name) return original @@ -68,6 +80,7 @@ def decorator(original): def originalName(function): + # type: (Callable) -> Text """ Get the original, user-specified name of C{function}, chasing back any wrappers applied with C{modified}. From ebc9597c876451e7af3e14ea9d4acb1709df121d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 10 Dec 2019 13:35:42 -0800 Subject: [PATCH 16/19] Nix Python 2.7 --- .travis.yml | 26 ++--------------- .travis/twistedchecker-diff | 27 ----------------- CONTRIBUTING.rst | 4 +-- setup.py | 1 - src/klein/test/_trial.py | 23 --------------- src/klein/test/py3_test_resource.py | 21 ++------------ tox.ini | 45 ++--------------------------- 7 files changed, 9 insertions(+), 138 deletions(-) delete mode 100755 .travis/twistedchecker-diff diff --git a/.travis.yml b/.travis.yml index da65055a..74012704 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,13 +26,6 @@ matrix: - python: 3.8 env: TOXENV=mypy - - python: 2.7 - env: TOXENV=coverage-py27-tw171,codecov-py27 - - python: 2.7 - env: TOXENV=coverage-py27-tw184,codecov-py27 - - python: 2.7 - env: TOXENV=coverage-py27-twcurrent,codecov-py27 - - python: 3.5 env: TOXENV=coverage-py35-tw171,codecov-py35 - python: 3.5 @@ -61,13 +54,6 @@ matrix: - python: 3.8 env: TOXENV=coverage-py38-twcurrent,codecov-py38 - - python: pypy - env: TOXENV=coverage-pypy2-tw171,codecov-pypy2 - - python: pypy - env: TOXENV=coverage-pypy2-tw184,codecov-pypy2 - - python: pypy - env: TOXENV=coverage-pypy2-twcurrent,codecov-pypy2 - - python: pypy3 env: TOXENV=coverage-pypy3-tw171,codecov-pypy3 - python: pypy3 @@ -77,26 +63,18 @@ matrix: # Test against Twisted trunk in case something in development breaks us. # This is allowed to fail below, since the bug may be in Twisted. - - python: 2.7 - env: TOXENV=coverage-py27-twtrunk,codecov-py27 - python: 3.8 env: TOXENV=coverage-py38-twtrunk,codecov-py38 - - python: 2.7 - env: TOXENV=twistedchecker-diff - - python: 2.7 + - python: 3.8 env: TOXENV=docs - - python: 2.7 + - python: 3.8 env: TOXENV=docs-linkcheck allow_failures: # Tests against Twisted trunk are allow to fail, as they are not supported. - - env: TOXENV=coverage-py27-twtrunk,codecov-py27 - env: TOXENV=coverage-py38-twtrunk,codecov-py38 - # This is not yet required. - - env: TOXENV=twistedchecker-diff - # This depends on external web sites, so it's allowed to fail. - env: TOXENV=docs-linkcheck diff --git a/.travis/twistedchecker-diff b/.travis/twistedchecker-diff deleted file mode 100755 index 12fa9e48..00000000 --- a/.travis/twistedchecker-diff +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh - -set -e -set -u - -target="$1"; shift; - -report_file="$(mktemp)"; - -git fetch origin "+refs/heads/master:refs/remotes/origin/master"; - -# Run twistedchecker and store the output to a file -type twistedchecker 2> /dev/null; # Error out if executable isn't found. -twistedchecker \ - --output-format=parseable \ - "${target}" \ - > "${report_file}" \ - || true; - -# Diff the result against master -diff-quality \ - --violations=pylint \ - --fail-under=100 \ - --compare-branch=origin/master \ - "${report_file}"; - -rm "${report_file}"; diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8b094712..4f25daf9 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -13,12 +13,12 @@ Klein is an open source project that welcomes contributions of all kinds coming Getting started =============== -Here is a list of shell commands that will install the dependencies of Klein, run the test suite with Python 2.7 and the current version of Twisted, compile the documentation, and check for coding style issues with flake8. +Here is a list of shell commands that will install the dependencies of Klein, run the test suite with Python 3.8 and the current version of Twisted, compile the documentation, and check for coding style issues with flake8. .. code-block:: shell pip install --user tox - tox -e py27-twcurrent + tox -e py38-twcurrent tox -e docs tox -e flake8 diff --git a/setup.py b/setup.py index fe1bb57a..f3269844 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,6 @@ 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', diff --git a/src/klein/test/_trial.py b/src/klein/test/_trial.py index 68ea6184..b28809fe 100644 --- a/src/klein/test/_trial.py +++ b/src/klein/test/_trial.py @@ -4,10 +4,8 @@ Extensions to L{twisted.trial}. """ -import sys from typing import Any -from twisted import version as twistedVersion from twisted.trial.unittest import SynchronousTestCase from zope.interface import Interface @@ -24,27 +22,6 @@ class TestCase(SynchronousTestCase): Extensions to L{SynchronousTestCase}. """ - if (twistedVersion.major, twistedVersion.minor) < (16, 4): - def assertRegex(self, text, regex, msg=None): - # type: (str, Any, str) -> None - """ - Fail the test if a C{regexp} search of C{text} fails. - - @param text: Text which is under test. - - @param regex: A regular expression object or a string containing a - regular expression suitable for use by re.search(). - - @param msg: Text used as the error message on failure. - """ - if sys.version_info[:2] > (2, 7): - super(TestCase, self).assertRegex(text, regex, msg) - else: - # Python 2.7 has unittest.assertRegexpMatches() which was - # renamed to unittest.assertRegex() in Python 3.2 - super(TestCase, self).assertRegexpMatches(text, regex, msg) - - def assertProvides(self, interface, obj): # type: (Interface, Any) -> None """ diff --git a/src/klein/test/py3_test_resource.py b/src/klein/test/py3_test_resource.py index 10cc36ed..9d56d469 100644 --- a/src/klein/test/py3_test_resource.py +++ b/src/klein/test/py3_test_resource.py @@ -1,4 +1,3 @@ -import twisted from twisted.trial.unittest import TestCase as AsynchronousTestCase from .test_resource import LeafResource, _render, requestMock @@ -25,25 +24,9 @@ def test_asyncResourceRendering(self): async def leaf(request): return LeafResource() - if (twisted.version.major, twisted.version.minor) >= (16, 6): - expected = b"I am a leaf in the wind." + expected = b"I am a leaf in the wind." - d = _render(resource, request) - - else: - expected = b"** Twisted>=16.6 is required **" - - # Twisted version in use does not have ensureDeferred, so - # attempting to use an async resource will raise - # NotImplementedError. - # resource.render(), and therefore _render(), does not return the - # deferred object that does the rendering, so we need to check for - # errors indirectly via handle_errors(). - @app.handle_errors(NotImplementedError) - def notImplementedError(request, failure): - return expected - - d = _render(resource, request) + d = _render(resource, request) def assertResult(_): self.assertEqual(request.getWrittenData(), expected) diff --git a/tox.ini b/tox.ini index 37bcc9a9..c3e476c5 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = flake8 mypy - coverage-py{27,35,36,37,38,py2,py3}-tw{171,184,current,trunk} + coverage-py{35,36,37,38,py3}-tw{171,184,current,trunk} docs, docs-linkcheck @@ -18,14 +18,12 @@ skip_missing_interpreters = {env:TOX_SKIP_MISSING_INTERPRETERS:True} [testenv] basepython = - py27: python2.7 py35: python3.5 py36: python3.6 py37: python3.7 py38: python3.8 py39: python3.9 - pypy2: pypy pypy3: pypy3 deps = @@ -58,8 +56,6 @@ deps = {trial,coverage}: treq==18.6.0 {trial,coverage}: hypothesis==4.41.2 {trial,coverage}: idna==2.8 - {trial,coverage}-py{27,py2}: mock==3.0.5 - {trial,coverage}-py{27,py2}: typing==3.7.4.1 coverage: coverage==4.5.4 @@ -282,41 +278,6 @@ ignore_missing_imports = True ignore_missing_imports = True -## -# Run twistedchecker -## - -[testenv:twistedchecker] - -deps = - twistedchecker==0.7.2 - -basepython = python2.7 - -commands = - "{toxinidir}/.travis/environment" - - twistedchecker {posargs:klein} - - -## -# Run twistedchecker on changes relative to master -## - -[testenv:twistedchecker-diff] - -deps = - {[testenv:twistedchecker]deps} - diff_cover==2.4.0 - -basepython = python2.7 - -commands = - "{toxinidir}/.travis/environment" - - "{toxinidir}/.travis/twistedchecker-diff" {posargs:klein} - - ## # Publish to Codecov ## @@ -347,7 +308,7 @@ deps = sphinx==1.8.5 sphinx_rtd_theme==0.4.3 -basepython = python2.7 +basepython = python3.8 commands = "{toxinidir}/.travis/environment" @@ -363,7 +324,7 @@ commands = deps = {[testenv:docs]deps} -basepython = python2.7 +basepython = python3.8 commands = "{toxinidir}/.travis/environment" From 6b500235ed2a57bd6d304e7ed39d53d33d7292d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 17 Dec 2019 11:46:49 -0800 Subject: [PATCH 17/19] Missed merge item --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 7f8a83bb..ccf14f19 100644 --- a/tox.ini +++ b/tox.ini @@ -64,9 +64,9 @@ deps = Werkzeug==0.16.0 zope.interface==4.6.0 - {trial,coverage}: treq==18.6.0 - {trial,coverage}: hypothesis==4.41.2 - {trial,coverage}: idna==2.8 + {test,coverage}: treq==18.6.0 + {test,coverage}: hypothesis==4.41.2 + {test,coverage}: idna==2.8 coverage: coverage==4.5.4 From 1dc4033ccca384c928abbe47b5197817c4fa61a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 17 Dec 2019 11:58:18 -0800 Subject: [PATCH 18/19] lint --- src/klein/_decorators.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/klein/_decorators.py b/src/klein/_decorators.py index 4274140e..2d96278f 100644 --- a/src/klein/_decorators.py +++ b/src/klein/_decorators.py @@ -1,10 +1,9 @@ from functools import wraps -from typing import Callable, Text, TypeVar, cast +from typing import Callable, Text, TypeVar C = TypeVar("C", bound=Callable) - def bindable(bindable): # type: (C) -> C """ @@ -29,9 +28,7 @@ def bindable(bindable): return bindable -def modified( - modification, original, modifier=None -): +def modified(modification, original, modifier=None): # FIXME: This maybe isn't quite right # __type: (Text, C, Optional[Callable[[C], C]]) -> Callable[[C], C] """ @@ -54,11 +51,11 @@ def modified( """ def decorator(wrapper): - # type: (C) -> C + # type: (Callable) -> Callable result = named(modification + " for " + original.__name__)( wraps(original)(wrapper) ) - result.__original__ = original + result.__original__ = original # type: ignore[attr-defined] if modifier is not None: before = set(wrapper.__dict__.keys()) result = modifier(result) From fb402e74ad33e4e9c2605489bbffc00b4d045bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 29 Jan 2020 15:52:28 -0800 Subject: [PATCH 19/19] Remove Python 2-only code paths. --- src/klein/_app.py | 10 +---- src/klein/_dihttp.py | 6 +-- src/klein/_session.py | 6 +-- src/klein/test/_strategies.py | 10 ++--- src/klein/test/py3_test_resource.py | 34 ----------------- src/klein/test/test_app.py | 6 +-- src/klein/test/test_form.py | 4 +- src/klein/test/test_resource.py | 59 ++++++++++++++++------------- 8 files changed, 40 insertions(+), 95 deletions(-) delete mode 100644 src/klein/test/py3_test_resource.py diff --git a/src/klein/_app.py b/src/klein/_app.py index b422a6bd..4956a310 100644 --- a/src/klein/_app.py +++ b/src/klein/_app.py @@ -8,15 +8,7 @@ import sys from collections import namedtuple from contextlib import contextmanager - -try: - from inspect import iscoroutine -except ImportError: - - def iscoroutine(*args, **kwargs): # type: ignore - return False - - +from inspect import iscoroutine from weakref import ref from twisted.internet import endpoints, reactor diff --git a/src/klein/_dihttp.py b/src/klein/_dihttp.py index 3101dba1..cbfc19f2 100644 --- a/src/klein/_dihttp.py +++ b/src/klein/_dihttp.py @@ -8,8 +8,6 @@ from hyperlink import parse -from six import text_type - from zope.interface import implementer, provider from zope.interface.interfaces import IInterface @@ -37,8 +35,6 @@ def urlFromRequest(request): else: host = request.client.host port = request.client.port - if not isinstance(host, text_type): - host = host.decode("ascii") return parse(request.uri.decode("charmap")).replace( scheme=u"https" if request.isSecure() else u"http", @@ -141,7 +137,7 @@ def _applyToRequest(self, request): """ request.setResponseCode(self.code) for headerName, headerValueOrValues in self.headers.items(): - if not isinstance(headerValueOrValues, (text_type, bytes)): + if not isinstance(headerValueOrValues, (str, bytes)): headerValues = headerValueOrValues else: headerValues = [headerValueOrValues] diff --git a/src/klein/_session.py b/src/klein/_session.py index ad47ec16..bf711762 100644 --- a/src/klein/_session.py +++ b/src/klein/_session.py @@ -132,7 +132,7 @@ def procureSession(self, request, forceInsecure=False): # to do it? Turn on your dang HTTPS! yield self._store.sentInsecurely(allPossibleSentTokens) tokenHeader = self._insecureTokenHeader - cookieName = self._insecureCookie + cookieName = self._insecureCookie.decode("ascii") sentSecurely = False # Fun future feature: honeypot that does this over HTTPS, but sets # isSecure() to return false because it serves up a cert for the @@ -186,10 +186,6 @@ def procureSession(self, request, forceInsecure=False): ) session = yield self._store.newSession(sentSecurely, mechanism) identifierInCookie = session.identifier - if not isinstance(identifierInCookie, str): - identifierInCookie = identifierInCookie.encode("ascii") - if not isinstance(cookieName, str): - cookieName = cookieName.decode("ascii") request.addCookie( cookieName, identifierInCookie, diff --git a/src/klein/test/_strategies.py b/src/klein/test/_strategies.py index 8a29e230..d48eef51 100644 --- a/src/klein/test/_strategies.py +++ b/src/klein/test/_strategies.py @@ -24,7 +24,7 @@ from idna import IDNAError, check_label, encode as idna_encode -from twisted.python.compat import _PY3, unicode +from twisted.python.compat import unicode __all__ = () @@ -34,10 +34,6 @@ DrawCallable = Callable[[Callable[..., T]], T] -if _PY3: - unichr = chr - - def idna_characters(): # pragma: no cover # type: () -> str """ @@ -73,7 +69,7 @@ def idna_characters(): # pragma: no cover for i in range(start, end + 1): if i > maxunicode: break - result.append(unichr(i)) + result.append(chr(i)) _idnaCharacters = u"".join(result) @@ -259,7 +255,7 @@ def path_characters(): def chars(): # type: () -> Iterable[Text] for i in range(maxunicode): - c = unichr(i) + c = chr(i) # Exclude reserved characters if c in "#/?": diff --git a/src/klein/test/py3_test_resource.py b/src/klein/test/py3_test_resource.py deleted file mode 100644 index a5fd33c3..00000000 --- a/src/klein/test/py3_test_resource.py +++ /dev/null @@ -1,34 +0,0 @@ -from twisted.trial.unittest import TestCase as AsynchronousTestCase - -from .test_resource import LeafResource, _render, requestMock -from .. import Klein -from .._resource import KleinResource - - -class PY3KleinResourceTests(AsynchronousTestCase): - def assertFired(self, deferred, result=None): - """ - Assert that the given deferred has fired with the given result. - """ - self.assertEqual(self.successResultOf(deferred), result) - - def test_asyncResourceRendering(self): - app = Klein() - resource = KleinResource(app) - - request = requestMock(b"/resource/leaf") - - @app.route("/resource/leaf") - async def leaf(request): - return LeafResource() - - expected = b"I am a leaf in the wind." - - d = _render(resource, request) - - def assertResult(_): - self.assertEqual(request.getWrittenData(), expected) - - d.addCallback(assertResult) - - return d diff --git a/src/klein/test/test_app.py b/src/klein/test/test_app.py index ee407f86..3eb25354 100644 --- a/src/klein/test/test_app.py +++ b/src/klein/test/test_app.py @@ -1,11 +1,7 @@ from __future__ import absolute_import, division import sys - -try: - from unittest.mock import Mock, patch -except Exception: - from mock import Mock, patch # type:ignore[misc] +from unittest.mock import Mock, patch from twisted.python.components import registerAdapter from twisted.trial import unittest diff --git a/src/klein/test/test_form.py b/src/klein/test/test_form.py index bf5484aa..3cdb6382 100644 --- a/src/klein/test/test_form.py +++ b/src/klein/test/test_form.py @@ -720,9 +720,7 @@ def test_renderingWithNoSessionYet(self): self.assertEqual(response.code, 200) setCookie = response.cookies()["Klein-Secure-Session"] expected = 'value="{}"'.format(setCookie) - actual = self.successResultOf(content(response)) - if not isinstance(expected, bytes): - actual = actual.decode("utf-8") + actual = self.successResultOf(content(response)).decode("utf-8") self.assertIn(expected, actual) def test_noSessionPOST(self): diff --git a/src/klein/test/test_resource.py b/src/klein/test/test_resource.py index d90dba50..e312dfed 100644 --- a/src/klein/test/test_resource.py +++ b/src/klein/test/test_resource.py @@ -2,19 +2,18 @@ import os from io import BytesIO - -try: - from unittest.mock import Mock, call -except Exception: - from mock import Mock, call +from unittest.mock import Mock, call from six.moves.urllib.parse import parse_qs from twisted.internet.defer import CancelledError, Deferred, fail, succeed from twisted.internet.error import ConnectionLost from twisted.internet.unix import Server -from twisted.python.compat import _PY3, unicode -from twisted.trial.unittest import SynchronousTestCase +from twisted.python.compat import unicode +from twisted.trial.unittest import ( + SynchronousTestCase, + TestCase as AsynchronousTestCase, +) from twisted.web import server from twisted.web.http_headers import Headers from twisted.web.resource import Resource @@ -1098,16 +1097,7 @@ def test_failedDecodePathInfo(self): self.assertEqual(b"Non-UTF-8 encoding in URL.", rv) self.assertEqual(1, len(self.flushLoggedErrors(UnicodeDecodeError))) - def test_urlDecodeErrorReprPy2(self): - """ - URLDecodeError.__repr__ formats properly. - """ - self.assertEqual( - ")>", - repr(_URLDecodeError(ValueError)), - ) - - def test_urlDecodeErrorReprPy3(self): + def test_urlDecodeErrorRepr(self): """ URLDecodeError.__repr__ formats properly. """ @@ -1116,11 +1106,6 @@ def test_urlDecodeErrorReprPy3(self): repr(_URLDecodeError(ValueError)), ) - if _PY3: - test_urlDecodeErrorReprPy2.skip = "Only works on Py2" # type: ignore - else: - test_urlDecodeErrorReprPy3.skip = "Only works on Py3" # type: ignore - def test_subroutedBranch(self): subapp = Klein() @@ -1319,10 +1304,30 @@ def test_weird_resource_situation(self): self.assertIdentical(resource.ensure_utf8_bytes, ensure_utf8_bytes) -if _PY3: - import sys +class PY3KleinResourceTests(AsynchronousTestCase): + def assertFired(self, deferred, result=None): + """ + Assert that the given deferred has fired with the given result. + """ + self.assertEqual(self.successResultOf(deferred), result) + + def test_asyncResourceRendering(self): + app = Klein() + resource = KleinResource(app) + + request = requestMock(b"/resource/leaf") + + @app.route("/resource/leaf") + async def leaf(request): + return LeafResource() + + expected = b"I am a leaf in the wind." + + d = _render(resource, request) + + def assertResult(_): + self.assertEqual(request.getWrittenData(), expected) - if sys.version_info >= (3, 5): - from .py3_test_resource import PY3KleinResourceTests + d.addCallback(assertResult) - PY3KleinResourceTests # shh pyflakes + return d