diff --git a/.travis.yml b/.travis.yml index a50524f3..bfa9afbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,13 +28,6 @@ matrix: - python: 3.8 env: TOXENV=mypy - - python: 2.7 - env: TOXENV=coverage-py27-tw171,codecov - - python: 2.7 - env: TOXENV=coverage-py27-tw184,codecov - - python: 2.7 - env: TOXENV=coverage-py27-twcurrent,codecov - - python: 3.5 env: TOXENV=coverage-py35-tw171,codecov - python: 3.5 @@ -63,13 +56,6 @@ matrix: - python: 3.8 env: TOXENV=coverage-py38-twcurrent,codecov - - python: pypy - env: TOXENV=coverage-pypy2-tw171,codecov - - python: pypy - env: TOXENV=coverage-pypy2-tw184,codecov - - python: pypy - env: TOXENV=coverage-pypy2-twcurrent,codecov - - python: pypy3 env: TOXENV=coverage-pypy3-tw171,codecov - python: pypy3 @@ -79,19 +65,16 @@ 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 - python: 3.8 env: TOXENV=coverage-py38-twtrunk,codecov - - 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 - env: TOXENV=coverage-py38-twtrunk,codecov # This depends on external web sites, so it's allowed to fail. 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/__init__.py b/src/klein/__init__.py index 00840d94..07fe6b9c 100644 --- a/src/klein/__init__.py +++ b/src/klein/__init__.py @@ -2,7 +2,18 @@ from typing import TYPE_CHECKING -from ._app import Klein, handle_errors, route, run, subroute, urlFor, url_for +from ._app import ( + Klein, + KleinErrorHandler, + KleinRenderable, + KleinRoute, + handle_errors, + route, + run, + subroute, + urlFor, + url_for, +) from ._dihttp import RequestComponent, RequestURL, Response from ._form import Field, FieldValues, Form, RenderableForm from ._plating import Plating @@ -14,12 +25,15 @@ # Inform mypy of import shenanigans. from .resource import _SpecialModuleObject - resource = _SpecialModuleObject() + resource = _SpecialModuleObject(None) else: from . import resource __all__ = ( "Klein", + "KleinErrorHandler", + "KleinRenderable", + "KleinRoute", "Plating", "Field", "FieldValues", diff --git a/src/klein/_app.py b/src/klein/_app.py index b422a6bd..0c52c1f9 100644 --- a/src/klein/_app.py +++ b/src/klein/_app.py @@ -8,40 +8,61 @@ 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 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[ + [Optional["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}. @@ -66,11 +87,32 @@ def _call(__klein_instance__, __klein_f__, *args, **kwargs): @implementer(IKleinRequest) 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) @@ -89,18 +131,21 @@ 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: Optional[Klein] + 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 @@ -108,6 +153,7 @@ def __ne__(self, other): @property def url_map(self): + # type: () -> Map """ Read only property exposing L{Klein._url_map}. """ @@ -115,26 +161,30 @@ 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. """ return handler(self._instance, request, failure) def resource(self): + # type: () -> KleinResource """ Return an L{IResource} which suitably wraps this app. @@ -144,6 +194,7 @@ def resource(self): return KleinResource(self) def __get__(self, instance, owner): + # type: (Any, object) -> Klein """ Get an instance of L{Klein} bound to C{instance}. """ @@ -163,7 +214,9 @@ 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__() @@ -181,6 +234,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 @@ -213,6 +267,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() @@ -220,12 +275,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( @@ -238,9 +298,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)) @@ -364,13 +427,14 @@ def _f(instance, request, failure): def urlFor( self, - request, - endpoint, - values=None, - method=None, - force_external=False, - append_unknown=True, + 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: @@ -379,20 +443,24 @@ def urlFor( " 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 def run( self, - host=None, - port=None, - logFile=None, - endpoint_description=None, - displayTracebacks=True, + 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}. @@ -405,22 +473,17 @@ def run( 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 acef094c..c2b45b6c 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 + +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. @@ -45,10 +53,10 @@ def modified(modification, original, modifier=None): """ def decorator(wrapper): - result = named(modification + " for " + original.__name__)( - wraps(original)(wrapper) - ) - result.__original__ = original + # type: (Callable) -> Callable + namer = named(modification + " for " + original.__name__) + result = namer(wraps(original)(wrapper)) + result.__original__ = original # type: ignore[attr-defined] if modifier is not None: before = set(wrapper.__dict__.keys()) result = modifier(result) @@ -61,11 +69,13 @@ 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 @@ -74,6 +84,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}. @@ -82,4 +93,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/_dihttp.py b/src/klein/_dihttp.py index ce01fad7..0e893686 100644 --- a/src/klein/_dihttp.py +++ b/src/klein/_dihttp.py @@ -2,23 +2,23 @@ Dependency-Injected HTTP metadata. """ -from typing import Any, Dict, Mapping, Sequence, TYPE_CHECKING, Text, Union +from typing import Any, Dict, Mapping, Sequence, Text, Union import attr from hyperlink import DecodedURL -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 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): @@ -35,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") url = DecodedURL.fromText(request.uri.decode("charmap")) url = url.replace( @@ -141,7 +139,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/_form.py b/src/klein/_form.py index 5ef008bb..dbf20f16 100644 --- a/src/klein/_form.py +++ b/src/klein/_form.py @@ -154,7 +154,7 @@ def asTags(self): """ value = self.value if value is None: - value = "" + value = "" # type: ignore[misc] input_tag = tags.input( type=self.formInputType, name=self.formFieldName, value=value ) diff --git a/src/klein/_headers_compat.py b/src/klein/_headers_compat.py index c9c73ee6..e8d79842 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 @@ -59,7 +59,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/_iapp.py b/src/klein/_iapp.py deleted file mode 100644 index fe66fa8d..00000000 --- a/src/klein/_iapp.py +++ /dev/null @@ -1,21 +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 d90695ba..3f28f1d5 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,26 @@ IHTTPResponse as _IHTTPResponse, IMutableHTTPHeaders as _IMutableHTTPHeaders, ) - -IKleinRequest # Silence linter +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} + """ if TYPE_CHECKING: # pragma: no cover diff --git a/src/klein/_isession.py b/src/klein/_isession.py index eb0e7989..dcf1dc7d 100644 --- a/src/klein/_isession.py +++ b/src/klein/_isession.py @@ -1,4 +1,4 @@ -from typing import Any, TYPE_CHECKING +from typing import Any, Dict, Iterable, Sequence, TYPE_CHECKING, Text, Union import attr @@ -7,17 +7,15 @@ 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): """ @@ -363,7 +361,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/_plating.py b/src/klein/_plating.py index 1b8a8d79..b21498b5 100644 --- a/src/klein/_plating.py +++ b/src/klein/_plating.py @@ -7,27 +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/_request_compat.py b/src/klein/_request_compat.py index f4502f53..c9ab1aac 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 @@ -52,7 +52,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 def uri(self): diff --git a/src/klein/_requirer.py b/src/klein/_requirer.py index 991331e1..ea4e24b4 100644 --- a/src/klein/_requirer.py +++ b/src/klein/_requirer.py @@ -1,22 +1,22 @@ -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, +) @implementer(IRequestLifecycle) # type: ignore[misc] diff --git a/src/klein/_session.py b/src/klein/_session.py index fbd120e2..8784bbf7 100644 --- a/src/klein/_session.py +++ b/src/klein/_session.py @@ -125,7 +125,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 @@ -179,10 +179,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/_tubes.py b/src/klein/_tubes.py index 06e61f81..494d3141 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 @@ -77,11 +77,11 @@ def flowTo(self, drain): return result def pauseFlow(self): - # type: () -> None + # type: () -> UnknownType return self._pauser.pause() def stopFlow(self): - # type: () -> None + # type: () -> UnknownType return self._pauser.resume() def _pause(self): diff --git a/src/klein/resource.py b/src/klein/resource.py index bf5294c5..80b9c72d 100644 --- a/src/klein/resource.py +++ b/src/klein/resource.py @@ -9,15 +9,12 @@ - It's the module where L{KleinResource} is defined. """ from sys import modules -from typing import TYPE_CHECKING +from typing import Any, AnyStr, Callable, Text from ._app import resource as _globalResourceMethod from ._resource import KleinResource as _KleinResource, ensure_utf8_bytes -if TYPE_CHECKING: - from typing import AnyStr, Callable, Text - - KleinResource = _KleinResource +KleinResource = _KleinResource class _SpecialModuleObject(object): @@ -34,6 +31,10 @@ 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] @@ -58,6 +59,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 fccc281e..a37c2b28 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, Iterable, List, Text, cast +from typing import Any, Callable, Dict, Iterable, Text, cast import attr from attr import Factory @@ -36,7 +36,7 @@ class MemorySession(object): _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. @@ -105,7 +105,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. diff --git a/src/klein/test/_strategies.py b/src/klein/test/_strategies.py index 35600503..12e65809 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/_trial.py b/src/klein/test/_trial.py index 612e25ec..b79aee0d 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 @@ -23,27 +21,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 deleted file mode 100644 index b5c2cb29..00000000 --- a/src/klein/test/py3_test_resource.py +++ /dev/null @@ -1,51 +0,0 @@ -import twisted -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() - - if (twisted.version.major, twisted.version.minor) >= (16, 6): - 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) - - 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 f1ea3cee..dbf86b12 100644 --- a/src/klein/test/test_form.py +++ b/src/klein/test/test_form.py @@ -1,4 +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 @@ -9,9 +9,10 @@ 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, @@ -23,11 +24,6 @@ from .._form import textConverter -if TYPE_CHECKING: # pragma: no cover - from typing import Any, Tuple - from twisted.web.iweb import IRequest - from klein import RenderableForm - class DanglingField(Field): """ @@ -733,9 +729,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_headers_compat.py b/src/klein/test/test_headers_compat.py index c218f348..d139678e 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 @@ -18,7 +18,6 @@ ) from .._headers_compat import HTTPHeadersWrappingHeaders - try: from twisted.web.http_headers import _sanitizeLinearWhitespace except ImportError: @@ -34,7 +33,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_requirer.py b/src/klein/test/test_requirer.py index e693dc53..0c16b74f 100644 --- a/src/klein/test/test_requirer.py +++ b/src/klein/test/test_requirer.py @@ -41,7 +41,8 @@ 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): diff --git a/src/klein/test/test_resource.py b/src/klein/test/test_resource.py index d90dba50..abbfdab4 100644 --- a/src/klein/test/test_resource.py +++ b/src/klein/test/test_resource.py @@ -2,21 +2,21 @@ 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.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 +25,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, @@ -222,7 +222,8 @@ class _One(object): oneKlein = Klein() @oneKlein.route("/foo") - def foo(self): + def foo(self, resource): + # type: (IRequest) -> KleinRenderable pass _one = _One() @@ -1098,16 +1099,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 +1108,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 +1306,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 diff --git a/src/klein/test/test_session.py b/src/klein/test/test_session.py index e68005d8..85af358a 100644 --- a/src/klein/test/test_session.py +++ b/src/klein/test/test_session.py @@ -2,29 +2,25 @@ 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] class ISimpleTest(Interface): @@ -70,7 +66,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. """ diff --git a/tox.ini b/tox.ini index 15aaecae..8c8d06f0 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = flake8, black, 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} coverage_report docs, docs-linkcheck packaging @@ -14,6 +14,11 @@ skip_missing_interpreters = {tty:True:False} basepython = python3.8 +setenv = + PY_MODULE=klein + + PYTHONPYCACHEPREFIX={envtmpdir}/pycache + deps = tw160: Twisted==16.0.0 tw161: Twisted==16.1.1 @@ -44,15 +49,10 @@ deps = {test,coverage}: treq==18.6.0 {test,coverage}: hypothesis==4.56.0 {test,coverage}: idna==2.8 - {test,coverage}-py{27,py2}: mock==3.0.5 - {test,coverage}-py{27,py2}: typing==3.7.4.1 coverage: {[testenv:coverage_report]deps} -setenv = - PY_MODULE=klein - - PYTHONPYCACHEPREFIX={envtmpdir}/pycache +setenv = {[default]setenv} ## @@ -282,33 +282,27 @@ commands = # Global settings +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 -disallow_incomplete_defs = False -no_implicit_optional = False -warn_return_any = False -warn_unreachable = False +disallow_any_generics = 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