diff --git a/.gitignore b/.gitignore index 8305314e..5b429268 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,11 @@ *.pyc *~ .DS_Store -/.coverage -/.coverage.* /.eggs /.hypothesis/ -/.mypy_cache /.tox/ -/_trial_temp/ /build/ /dist/ /docs/_build/ +/htmlcov/ /src/*.egg-info -htmlcov diff --git a/.travis.yml b/.travis.yml index da65055a..eab0eb31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,67 +23,66 @@ matrix: include: - python: 3.8 env: TOXENV=flake8 + - python: 3.8 + env: TOXENV=black - python: 3.8 env: TOXENV=mypy - python: 2.7 - env: TOXENV=coverage-py27-tw171,codecov-py27 + env: TOXENV=coverage-py27-tw171,codecov - python: 2.7 - env: TOXENV=coverage-py27-tw184,codecov-py27 + env: TOXENV=coverage-py27-tw184,codecov - python: 2.7 - env: TOXENV=coverage-py27-twcurrent,codecov-py27 + env: TOXENV=coverage-py27-twcurrent,codecov - python: 3.5 - env: TOXENV=coverage-py35-tw171,codecov-py35 + env: TOXENV=coverage-py35-tw171,codecov - python: 3.5 - env: TOXENV=coverage-py35-tw184,codecov-py35 + env: TOXENV=coverage-py35-tw184,codecov - python: 3.5 - env: TOXENV=coverage-py35-twcurrent,codecov-py35 - python: 3.6 - env: TOXENV=coverage-py36-tw171,codecov-py36 + env: TOXENV=coverage-py36-tw171,codecov - python: 3.6 - env: TOXENV=coverage-py36-tw184,codecov-py36 + env: TOXENV=coverage-py36-tw184,codecov - python: 3.6 - env: TOXENV=coverage-py36-twcurrent,codecov-py36 + env: TOXENV=coverage-py36-twcurrent,codecov - python: 3.7 - env: TOXENV=coverage-py37-tw171,codecov-py37 + env: TOXENV=coverage-py37-tw171,codecov - python: 3.7 - env: TOXENV=coverage-py37-tw184,codecov-py37 + env: TOXENV=coverage-py37-tw184,codecov - python: 3.7 - env: TOXENV=coverage-py37-twcurrent,codecov-py37 + env: TOXENV=coverage-py37-twcurrent,codecov - python: 3.8 - env: TOXENV=coverage-py38-tw171,codecov-py38 + env: TOXENV=coverage-py38-tw171,codecov - python: 3.8 - env: TOXENV=coverage-py38-tw184,codecov-py38 + env: TOXENV=coverage-py38-tw184,codecov - python: 3.8 - env: TOXENV=coverage-py38-twcurrent,codecov-py38 + env: TOXENV=coverage-py38-twcurrent,codecov - python: pypy - env: TOXENV=coverage-pypy2-tw171,codecov-pypy2 + env: TOXENV=coverage-pypy2-tw171,codecov - python: pypy - env: TOXENV=coverage-pypy2-tw184,codecov-pypy2 + env: TOXENV=coverage-pypy2-tw184,codecov - python: pypy - env: TOXENV=coverage-pypy2-twcurrent,codecov-pypy2 + env: TOXENV=coverage-pypy2-twcurrent,codecov - python: pypy3 - env: TOXENV=coverage-pypy3-tw171,codecov-pypy3 + env: TOXENV=coverage-pypy3-tw171,codecov - python: pypy3 - env: TOXENV=coverage-pypy3-tw184,codecov-pypy3 + env: TOXENV=coverage-pypy3-tw184,codecov - python: pypy3 - env: TOXENV=coverage-pypy3-twcurrent,codecov-pypy3 + env: TOXENV=coverage-pypy3-twcurrent,codecov # 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 + env: TOXENV=coverage-py27-twtrunk,codecov - python: 3.8 - env: TOXENV=coverage-py38-twtrunk,codecov-py38 + env: TOXENV=coverage-py38-twtrunk,codecov - - python: 2.7 - env: TOXENV=twistedchecker-diff - python: 2.7 env: TOXENV=docs - python: 2.7 @@ -91,19 +90,16 @@ matrix: 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 + - env: TOXENV=coverage-py27-twtrunk,codecov + - env: TOXENV=coverage-py38-twtrunk,codecov # This depends on external web sites, so it's allowed to fail. - env: TOXENV=docs-linkcheck install: - - ./.travis/install + - pip install tox script: - - ./.travis/run tox + - tox diff --git a/.travis/environment b/.travis/environment deleted file mode 100755 index 8f5e2dfc..00000000 --- a/.travis/environment +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python - -""" -Print information about the environment. -This is used by CI tools. -""" - -from sys import stdout, version, executable -from os import environ as environment, getcwd - -try: - from twisted import __version__ as twistedVersion -except ImportError: - twistedVersion = None - - -class Writer(object): - indents = " " * 4 - - write = stdout.write - - def emit(self, level, text): - self.write(self.indents * level) - self.write(text) - self.write("\n") - - def heading(self, text): - self.emit(0, text + ":") - - def line(self, text): - self.emit(1, text) - - -out = Writer() - -out.heading("Working directory") -out.line(getcwd()) - -out.heading("OS environment") -for key in sorted(environment): - out.line(key + " = " + environment[key]) - -out.heading("Python executable") -out.line(executable) - -out.heading("Python version") -for line in version.split("\n"): - out.line(line) - -out.heading("Twisted version") -if twistedVersion is None: - out.line("Twisted is not installed.") -else: - out.line(twistedVersion) diff --git a/.travis/install b/.travis/install deleted file mode 100755 index 39628cdf..00000000 --- a/.travis/install +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -set -e -set -u - -pip install tox; diff --git a/.travis/run b/.travis/run deleted file mode 100755 index 11b3214e..00000000 --- a/.travis/run +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -set -e -set -u - -export TOX_SKIP_MISSING_INTERPRETERS="False"; - -exec "$@"; 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/MANIFEST.in b/MANIFEST.in index 9dff8059..01a7f583 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,13 @@ -include LICENSE -include README.rst +include .coveragerc +include .travis/twistedchecker-diff include AUTHORS +include CONTRIBUTING.rst +include docs/Makefile +include LICENSE include NEWS.rst +include pyproject.toml +include README.rst +include tox.ini +recursive-include docs *.py +recursive-include docs *.rst +recursive-include docs *.txt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..e7efe6ae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] + +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + + +[tool.black] + +line-length = 80 +target-version = ["py27"] diff --git a/src/klein/__init__.py b/src/klein/__init__.py index 9c42b1fc..dd831ce9 100644 --- a/src/klein/__init__.py +++ b/src/klein/__init__.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: # Inform mypy of import shenanigans. from .resource import _SpecialModuleObject + resource = _SpecialModuleObject() else: from . import resource @@ -20,16 +21,16 @@ __all__ = ( "Klein", "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 e21691f4..e8ac3a95 100644 --- a/src/klein/_app.py +++ b/src/klein/_app.py @@ -8,11 +8,15 @@ 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 weakref import ref from twisted.internet import endpoints, reactor @@ -23,9 +27,11 @@ def iscoroutine(*args, **kwargs): # type: ignore try: from twisted.internet.defer import ensureDeferred except ImportError: + def ensureDeferred(*args, **kwagrs): raise NotImplementedError("Coroutines support requires Twisted>=16.6") + from werkzeug.routing import Map, Rule, Submount from zope.interface import implementer @@ -35,7 +41,6 @@ def ensureDeferred(*args, **kwagrs): from ._resource import KleinResource - def _call(__klein_instance__, __klein_f__, *args, **kwargs): """ Call C{__klein_f__} with the given C{*args} and C{**kwargs}. @@ -49,7 +54,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) @@ -58,12 +63,10 @@ def _call(__klein_instance__, __klein_f__, *args, **kwargs): return result - @implementer(IKleinRequest) class KleinRequest(object): - def __init__(self, request): - self.branch_segments = [''] + self.branch_segments = [""] self.mapper = None def url_for(self, *args, **kwargs): @@ -73,7 +76,6 @@ def url_for(self, *args, **kwargs): registerAdapter(KleinRequest, Request, IKleinRequest) - class Klein(object): """ L{Klein} is an object which is responsible for maintaining the routing @@ -93,22 +95,17 @@ def __init__(self): self._instance = None self._boundAs = None - def __eq__(self, other): if isinstance(other, Klein): return vars(self) == vars(other) return NotImplemented - def __ne__(self, other): result = self.__eq__(other) if result is NotImplemented: return result return not result - - - @property def url_map(self): """ @@ -116,7 +113,6 @@ def url_map(self): """ return self._url_map - @property def endpoints(self): """ @@ -124,7 +120,6 @@ def endpoints(self): """ return self._endpoints - def execute_endpoint(self, endpoint, *args, **kwargs): """ Execute the named endpoint with all arguments and possibly a bound @@ -133,14 +128,12 @@ def execute_endpoint(self, endpoint, *args, **kwargs): endpoint_f = self._endpoints[endpoint] return endpoint_f(self._instance, *args, **kwargs) - def execute_error_handler(self, handler, request, failure): """ Execute the passed error handler, possibly with a bound instance. """ return handler(self._instance, request, failure) - def resource(self): """ Return an L{IResource} which suitably wraps this app. @@ -150,7 +143,6 @@ def resource(self): return KleinResource(self) - def __get__(self, instance, owner): """ Get an instance of L{Klein} bound to C{instance}. @@ -168,7 +160,7 @@ def __get__(self, instance, owner): self._boundAs = name break else: - self._boundAs = 'unknown_' + str(id(self)) + self._boundAs = "unknown_" + str(id(self)) boundName = "__klein_bound_{}__".format(self._boundAs) k = getattr(instance, boundName, lambda: None)() @@ -187,15 +179,13 @@ def __get__(self, instance, owner): return k - @staticmethod def _segments_in_url(url): - segment_count = url.count('/') - if url.endswith('/'): + segment_count = url.count("/") + if url.endswith("/"): segment_count -= 1 return segment_count - def route(self, url, *args, **kwargs): """ Add a new handler for C{url} passing C{args} and C{kwargs} directly to @@ -223,25 +213,26 @@ def index(request): @named("router for '" + url + "'") def deco(f): - kwargs.setdefault('endpoint', f.__name__) - if kwargs.pop('branch', False): + kwargs.setdefault("endpoint", f.__name__) + if kwargs.pop("branch", False): branchKwargs = kwargs.copy() - branchKwargs['endpoint'] = branchKwargs['endpoint'] + '_branch' + branchKwargs["endpoint"] = branchKwargs["endpoint"] + "_branch" @modified("branch route '{url}' executor".format(url=url), f) def branch_f(instance, request, *a, **kw): - IKleinRequest(request).branch_segments = ( - kw.pop('__rest__', '').split('/') - ) + IKleinRequest(request).branch_segments = kw.pop( + "__rest__", "" + ).split("/") return _call(instance, f, request, *a, **kw) branch_f.segment_count = segment_count - self._endpoints[branchKwargs['endpoint']] = branch_f + self._endpoints[branchKwargs["endpoint"]] = branch_f self._url_map.add( Rule( - url.rstrip('/') + '/' + '', - *args, **branchKwargs + url.rstrip("/") + "/" + "", + *args, + **branchKwargs ) ) @@ -251,11 +242,11 @@ def _f(instance, request, *a, **kw): _f.segment_count = segment_count - self._endpoints[kwargs['endpoint']] = _f + self._endpoints[kwargs["endpoint"]] = _f self._url_map.add(Rule(url, *args, **kwargs)) return f - return deco + return deco @contextmanager def subroute(self, prefix): @@ -288,21 +279,19 @@ def foo_handler(request): segments = self._segments_in_url(prefix) - submount_map = namedtuple( - 'submount', ['rules', 'add'])( - [], lambda r: submount_map.rules.append(r)) + submount_map = namedtuple("submount", ["rules", "add"])( + [], lambda r: submount_map.rules.append(r) + ) try: self._url_map = submount_map self._subroute_segments += segments yield self - _map_before_submount.add( - Submount(prefix, submount_map.rules)) + _map_before_submount.add(Submount(prefix, submount_map.rules)) finally: self._url_map = _map_before_submount self._subroute_segments -= segments - def handle_errors(self, f_or_exception, *additional_exceptions): """ Register an error handler. This decorator supports two syntaxes. The @@ -356,9 +345,8 @@ def error_handler(request, failure): # Try to detect calls using the "simple" @app.handle_error syntax by # introspecting the first argument - if it isn't a type which # subclasses Exception we assume the simple syntax was used. - if ( - not isinstance(f_or_exception, type) or - not issubclass(f_or_exception, Exception) + if not isinstance(f_or_exception, type) or not issubclass( + f_or_exception, Exception ): return self.handle_errors(Exception)(f_or_exception) @@ -374,23 +362,37 @@ def _f(instance, request, failure): return deco - - def urlFor(self, request, endpoint, values=None, method=None, - force_external=False, append_unknown=True): - host = request.getHeader(b'host') + def urlFor( + self, + request, + endpoint, + values=None, + method=None, + force_external=False, + append_unknown=True, + ): + 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) + 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 + ) url_for = urlFor - - def run(self, host=None, port=None, logFile=None, - endpoint_description=None, displayTracebacks=True): + def run( + self, + host=None, + port=None, + logFile=None, + endpoint_description=None, + displayTracebacks=True, + ): """ Run a minimal twisted.web server on the specified C{port}, bound to the interface specified by C{host} and logging to C{logFile}. @@ -426,8 +428,9 @@ def run(self, host=None, port=None, logFile=None, log.startLogging(logFile) if not endpoint_description: - endpoint_description = "tcp:port={0}:interface={1}".format(port, - host) + endpoint_description = "tcp:port={0}:interface={1}".format( + port, host + ) endpoint = endpoints.serverFromString(reactor, endpoint_description) @@ -438,7 +441,6 @@ def run(self, host=None, port=None, logFile=None, reactor.run() - _globalKleinApp = Klein() route = _globalKleinApp.route diff --git a/src/klein/_decorators.py b/src/klein/_decorators.py index 528ecc5c..acef094c 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. @@ -42,9 +43,11 @@ def modified(modification, original, modifier=None): return value, and is only related to C{original} in the sense that it likely calls it. """ + def decorator(wrapper): - result = (named(modification + ' for ' + original.__name__) - (wraps(original)(wrapper))) + result = named(modification + " for " + original.__name__)( + wraps(original)(wrapper) + ) result.__original__ = original if modifier is not None: before = set(wrapper.__dict__.keys()) @@ -53,6 +56,7 @@ def decorator(wrapper): for key in after - before: setattr(original, key, wrapper.__dict__[key]) return result + return decorator @@ -60,10 +64,12 @@ def named(name): """ Change the name of a function to the given name. """ + def decorator(original): original.__name__ = str(name) original.__qualname__ = str(name) return original + return decorator diff --git a/src/klein/_dihttp.py b/src/klein/_dihttp.py index c6a441ee..3101dba1 100644 --- a/src/klein/_dihttp.py +++ b/src/klein/_dihttp.py @@ -56,8 +56,9 @@ class RequestURL(object): """ @classmethod - def registerInjector(cls, injectionComponents, parameterName, - requestLifecycle): + def registerInjector( + cls, injectionComponents, parameterName, requestLifecycle + ): # type: (Componentized, str, IRequestLifecycle) -> IDependencyInjector return cls() @@ -72,7 +73,6 @@ def finalize(cls): "Nothing to do upon finalization." - @implementer(IRequiredParameter, IDependencyInjector) # type: ignore[misc] @attr.s(frozen=True) class RequestComponent(object): @@ -84,8 +84,9 @@ class RequestComponent(object): interface = attr.ib(type=IInterface) - def registerInjector(self, injectionComponents, parameterName, - requestLifecycle): + def registerInjector( + self, injectionComponents, parameterName, requestLifecycle + ): # type: (Componentized, str, IRequestLifecycle) -> IDependencyInjector return self @@ -98,8 +99,6 @@ def finalize(cls): "Nothing to do upon finalization." - - @attr.s(frozen=True) class Response(object): """ @@ -117,15 +116,15 @@ class Response(object): @since: Klein NEXT """ + code = attr.ib(type=int, default=200) headers = attr.ib( type=Mapping[ - Union[Text, bytes], - Union[Text, bytes, Sequence[Union[Text, bytes]]] + Union[Text, bytes], Union[Text, bytes, Sequence[Union[Text, bytes]]] ], default=attr.Factory(dict), ) - body = attr.ib(type=Any, default=u'') + body = attr.ib(type=Any, default=u"") def _applyToRequest(self, request): # type: (IRequest) -> Any diff --git a/src/klein/_form.py b/src/klein/_form.py index 0622b13c..a337d1bf 100644 --- a/src/klein/_form.py +++ b/src/klein/_form.py @@ -4,8 +4,18 @@ import json from typing import ( - Any, AnyStr, Callable, Dict, Iterable, List, Optional, Sequence, - TYPE_CHECKING, Text, Union, cast + Any, + AnyStr, + Callable, + Dict, + Iterable, + List, + Optional, + Sequence, + TYPE_CHECKING, + Text, + Union, + cast, ) import attr @@ -23,25 +33,51 @@ 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 +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) + ( + 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. """ + def __init__(self, message): # type: (str) -> None super(CrossSiteRequestForgery, self).__init__() @@ -55,28 +91,27 @@ def render(self, request): request.setResponseCode(FORBIDDEN, b"FAILURECSRF") return ("CSRF TOKEN FAILURE: " + self.message).encode("utf-8") + 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") - ) - + return value if isinstance(value, unicode) else unicode(value, "utf-8") class IParsedJSONBody(Interface): """ Marker interface for the dict parsed from the request body's JSON contents. """ + # TODO: how to allow applications to pass options to loads, such as # parse_float? - @implementer(IRequiredParameter) # type: ignore[misc] @attr.s(frozen=True) class Field(object): @@ -94,12 +129,13 @@ 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=Text, default="") error = attr.ib(type=ValidationError, default=None) # IRequiredParameter - def registerInjector(self, injectionComponents, parameterName, - requestLifecycle): + def registerInjector( + self, injectionComponents, parameterName, requestLifecycle + ): # type: (Componentized, str, IRequestLifecycle) -> IDependencyInjector """ Register this form field as a dependency injector. @@ -107,7 +143,6 @@ def registerInjector(self, injectionComponents, parameterName, protoForm = IProtoForm(injectionComponents) return protoForm.addField(self.maybeNamed(parameterName)) - def maybeNamed(self, name): # type: (str) -> Field """ @@ -117,18 +152,20 @@ def maybeNamed(self, name): @param name: the name. @type name: a native L{str} """ + def maybe(it, that=name): # type: (Optional[str], Optional[str]) -> Optional[str] return that if it is None else it + return attr.assoc( self, pythonArgumentName=maybe(self.pythonArgumentName), formFieldName=maybe(self.formFieldName), - formLabel=maybe(self.formLabel, - name.capitalize() if not self.noLabel else None), + formLabel=maybe( + self.formLabel, name.capitalize() if not self.noLabel else None + ), ) - def asTags(self): # type: () -> Iterable[Tag] """ @@ -139,13 +176,17 @@ def asTags(self): @rtype: iterable of L{twisted.web.template.Tag} """ 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=(self.value if self.value is not None else ""), ) error_tags = [] if self.error: - error_tags.append(tags.div(class_="klein-form-validation-error") - (self.error.message)) + error_tags.append( + tags.div(class_="klein-form-validation-error")( + self.error.message + ) + ) if self.formLabel: yield tags.label(self.formLabel, ": ", input_tag, *error_tags) else: @@ -166,9 +207,8 @@ def extractValue(self, request): if fieldName is None: raise ValueError("Cannot extract unnamed form field.") contentType = request.getHeader(b"content-type") - if ( - contentType is not None and - contentType.startswith(b'application/json') + if contentType is not None and contentType.startswith( + b"application/json" ): # TODO: parse only once, please. parsed = request.getComponent(IParsedJSONBody) @@ -183,11 +223,10 @@ def extractValue(self, request): return parsed[fieldName] allValues = request.args.get(fieldName.encode("utf-8")) if allValues: - return allValues[0].decode('utf-8') + return allValues[0].decode("utf-8") else: return None - def validateValue(self, value): # type: (Any) -> Any """ @@ -208,9 +247,8 @@ def validateValue(self, value): except ValueError as ve: raise ValidationError(str(ve)) - @classmethod - def text(cls, **kw): # type: (**Any) -> Field + def text(cls, **kw): # type: (**Any) -> Field """ Shorthand for a form field that contains a short string, and will be rendered as a plain . @@ -218,14 +256,13 @@ def text(cls, **kw): # type: (**Any) -> Field return cls(converter=textConverter, formInputType="text", **kw) @classmethod - def password(cls, **kw): # type: (**Any) -> Field + def password(cls, **kw): # type: (**Any) -> Field """ Shorthand for a form field that, like L{text}, contains a short string, but should be obscured when typed (and, to the extent possible, obscured in other sensitive contexts, such as logging.) """ - return cls(converter=textConverter, - formInputType="password", **kw) + return cls(converter=textConverter, formInputType="password", **kw) @classmethod def hidden(cls, name, value, **kw): @@ -233,11 +270,13 @@ def hidden(cls, name, value, **kw): """ Shorthand for a hidden field. """ - return cls(converter=textConverter, - formInputType="hidden", - noLabel=True, - value=value, **kw).maybeNamed(name) - + return cls( + converter=textConverter, + formInputType="hidden", + noLabel=True, + value=value, + **kw + ).maybeNamed(name) @classmethod def number(cls, minimum=None, maximum=None, kind=float, **kw): @@ -245,6 +284,7 @@ def number(cls, minimum=None, maximum=None, kind=float, **kw): """ An integer within the range [minimum, maximum]. """ + def bounded_number(text): # type: (AnyStr) -> Any try: @@ -257,8 +297,8 @@ def bounded_number(text): if maximum is not None and value > maximum: raise ValidationError("value must be <=" + repr(maximum)) return value - return cls(converter=bounded_number, formInputType="number", **kw) + return cls(converter=bounded_number, formInputType="number", **kw) @classmethod def submit(cls, value): @@ -267,9 +307,12 @@ def submit(cls, value): A field representing a submit button, with a value (displayed on the button). """ - return cls(converter=textConverter, formInputType="submit", - noLabel=True, default=value) - + return cls( + converter=textConverter, + formInputType="submit", + noLabel=True, + default=value, + ) @implementer(IRenderable) @@ -284,7 +327,8 @@ class RenderableForm(object): @ivar validationErrors: a L{dict} mapping {L{Field}: L{ValidationError}} """ - _form = attr.ib(type='Form') + + _form = attr.ib(type="Form") _session = attr.ib(type=ISession) # type: ignore[misc] _action = attr.ib(type=str) _method = attr.ib(type=str) @@ -292,15 +336,15 @@ class RenderableForm(object): _encoding = attr.ib(type=str) prevalidationValues = attr.ib( type=Dict[Field, Optional[Text]], - default=cast(Dict[Field, Optional[Text]], attr.Factory(dict)) + default=cast(Dict[Field, Optional[Text]], attr.Factory(dict)), ) validationErrors = attr.ib( type=Dict[Field, ValidationError], - default=cast(Dict[Field, ValidationError], attr.Factory(dict)) + default=cast(Dict[Field, ValidationError], attr.Factory(dict)), ) - ENCTYPE_FORM_DATA = 'multipart/form-data' - ENCTYPE_URL_ENCODED = 'application/x-www-form-urlencoded' + ENCTYPE_FORM_DATA = "multipart/form-data" + ENCTYPE_URL_ENCODED = "application/x-www-form-urlencoded" def _fieldForCSRF(self): # type: () -> Field @@ -310,7 +354,6 @@ def _fieldForCSRF(self): """ return Field.hidden(CSRF_PROTECTION, self._session.identifier) - def _fieldsToRender(self): # type: () -> Iterable[Field] """ @@ -330,14 +373,18 @@ def _fieldsToRender(self): yield attr.assoc( field, value=self.prevalidationValues.get(field, field.value), - error=self.validationErrors.get(field, None) + error=self.validationErrors.get(field, None), ) if field.formInputType == "submit": anySubmit = True if not anySubmit: - yield Field(converter=str, formInputType="submit", value=u"submit", - formFieldName="__klein_auto_submit__") - if self._method.lower() == 'post': + yield Field( + converter=str, + formInputType="submit", + value="submit", + formFieldName="__klein_auto_submit__", + ) + if self._method.lower() == "post": yield self._fieldForCSRF() # Public interface below. @@ -350,25 +397,21 @@ def lookupRenderMethod(self, name): """ raise MissingRenderMethod(self, name) - def render(self, request): # type: (IRequest) -> Tag """ Render this form to the given request. """ - formAttributes = {"accept-charset": self._encoding, - "class": "klein-form"} - if self._method.lower() == 'post': + formAttributes = { + "accept-charset": self._encoding, + "class": "klein-form", + } + if self._method.lower() == "post": # Enctype has no meaning on method="GET" forms. formAttributes.update(enctype=self._enctype) - return ( - tags.form(action=self._action, method=self._method, - **formAttributes) - ( - field.asTags() for field in self._fieldsToRender() - ) - ) - + return tags.form( + action=self._action, method=self._method, **formAttributes + )(field.asTags() for field in self._fieldsToRender()) def glue(self): # type: () -> Iterable[Tag] @@ -387,12 +430,11 @@ def glue(self): return self._fieldForCSRF().asTags() - @bindable def defaultValidationFailureHandler( - instance, # type: Optional[object] - request, # type: IRequest - fieldValues, # type: FieldValues + instance, # type: Optional[object] + request, # type: IRequest + fieldValues, # type: FieldValues ): # type: (...) -> Element """ @@ -416,16 +458,24 @@ def defaultValidationFailureHandler( session = request.getComponent(ISession) # type: ignore[misc] request.setResponseCode(400) enctype = ( - (request.getHeader(b'content-type') or - RenderableForm.ENCTYPE_URL_ENCODED.encode("ascii")) - .split(b';')[0].decode("charmap") + ( + request.getHeader(b"content-type") + or RenderableForm.ENCTYPE_URL_ENCODED.encode("ascii") + ) + .split(b";")[0] + .decode("charmap") ) renderable = RenderableForm( - fieldValues.form, session, u"/".join( - segment.decode("utf-8", errors='replace') + fieldValues.form, + session, + "/".join( + segment.decode("utf-8", errors="replace") for segment in request.prepath ), - request.method, enctype, "utf-8", fieldValues.prevalidationValues, + request.method, + enctype, + "utf-8", + fieldValues.prevalidationValues, fieldValues.validationErrors, ) @@ -435,11 +485,10 @@ def defaultValidationFailureHandler( _requirerFunctionWithForm = Any _routeCallable = Any _routeDecorator = Callable[ - [_routeCallable, DefaultNamedArg(Any, '__session__')], - _routeCallable + [_routeCallable, DefaultNamedArg(Any, "__session__")], _routeCallable ] _validationFailureHandler = Callable[ - [Optional[object], IRequest, 'Form', Dict[str, str]], Element + [Optional[object], IRequest, "Form", Dict[str, str]], Element ] validationFailureHandlerAttribute = "__kleinFormValidationFailureHandlers__" @@ -450,6 +499,7 @@ class IProtoForm(Interface): Marker interface for L{ProtoForm}. """ + class IForm(Interface): """ Marker interface for form attached to dependency injection components. @@ -462,6 +512,7 @@ class ProtoForm(object): """ Form-builder. """ + _componentized = attr.ib(type=Componentized) _lifecycle = attr.ib(type=IRequestLifecycle) # type: ignore[misc] _fields = attr.ib(type=List[Field], default=attr.Factory(list)) @@ -476,7 +527,6 @@ def fromComponentized(cls, componentized): assert rl is not None return cls(componentized, rl) - def addField(self, field): # type: (Field) -> FieldInjector """ @@ -486,14 +536,12 @@ def addField(self, field): return FieldInjector(self._componentized, field, self._lifecycle) - class IFieldValues(Interface): """ Marker interface for parsed fields. """ - @implementer(IFieldValues) @attr.s class FieldValues(object): @@ -501,7 +549,7 @@ class FieldValues(object): Reified post-parsing values for HTTP form submission. """ - form = attr.ib(type='Form') + form = attr.ib(type="Form") arguments = attr.ib(type=Dict[str, Any]) prevalidationValues = attr.ib(type=Dict[Field, Optional[Text]]) validationErrors = attr.ib(type=Dict[Field, ValidationError]) @@ -516,20 +564,22 @@ def validate(self, instance, request): if self.validationErrors: result = yield _call( instance, - IValidationFailureHandler(self._injectionComponents, - defaultValidationFailureHandler), - request, self + IValidationFailureHandler( + self._injectionComponents, defaultValidationFailureHandler + ), + request, + self, ) raise EarlyExit(result) - @implementer(IDependencyInjector) # type: ignore[misc] @attr.s class FieldInjector(object): """ Field injector. """ + _componentized = attr.ib(type=Componentized) _field = attr.ib(type=Field) _lifecycle = attr.ib(type=IRequestLifecycle) # type: ignore[misc] @@ -564,17 +614,15 @@ def populateValuesHook(instance, request): return finalForm.populateRequestValues( self._componentized, instance, request ) + self._lifecycle.addPrepareHook( - populateValuesHook, provides=[IFieldValues], - requires=[ISession] + populateValuesHook, provides=[IFieldValues], requires=[ISession] ) - registerAdapter(ProtoForm.fromComponentized, Componentized, IProtoForm) - class IValidationFailureHandler(Interface): """ Validation failure handler callable interface. @@ -590,7 +638,7 @@ def checkCSRF(request): # TODO: optionalize CSRF protection for GET forms session = ISession(request, None) # type: ignore[misc] token = None - if request.method in (b'GET', b'HEAD'): + if request.method in (b"GET", b"HEAD"): # Idempotent requests don't require CRSF validation. (Don't have # destructive GETs or bad stuff will happen to you in general!) return @@ -602,17 +650,17 @@ def checkCSRF(request): return # We have a session, we weren't authenticated by a header... time to # check that token. - token = (request.args.get(CSRF_PROTECTION.encode("ascii"), [b""])[0] - .decode("ascii")) + token = request.args.get(CSRF_PROTECTION.encode("ascii"), [b""])[ + 0 + ].decode("ascii") if token == session.identifier: # The token matches. We're OK. return # leak only the value passed, not the actual token, just in # case there's some additional threat vector there - raise EarlyExit(CrossSiteRequestForgery( - "Invalid CSRF token: {!r}".format(token) - )) - + raise EarlyExit( + CrossSiteRequestForgery("Invalid CSRF token: {!r}".format(token)) + ) @attr.s(hash=False) @@ -620,6 +668,7 @@ class Form(object): """ A L{Form} is a collection of fields attached to a function. """ + fields = attr.ib(type=Sequence[Field]) @staticmethod @@ -656,13 +705,15 @@ def handleValidationFailures(request, fieldValues): @return: a decorator that decorates a function with the signature C{(request, form) -> thing klein can render}. """ + def decorate(decoratee): # type: (Callable) -> Callable - handler.injectionComponents.setComponent(IValidationFailureHandler, - decoratee) + handler.injectionComponents.setComponent( + IValidationFailureHandler, decoratee + ) return decoratee - return decorate + return decorate @inlineCallbacks def populateRequestValues(self, injectionComponents, instance, request): @@ -691,20 +742,24 @@ def populateRequestValues(self, injectionComponents, instance, request): validationErrors[field] = ve else: arguments[argName] = value - values = FieldValues(self, arguments, prevalidationValues, - validationErrors, injectionComponents) + values = FieldValues( + self, + arguments, + prevalidationValues, + validationErrors, + injectionComponents, + ) yield values.validate(instance, request) request.setComponent(IFieldValues, values) - @classmethod def rendererFor( cls, decoratedFunction, # type: _requirerFunctionWithForm - action, # type: Text - method=u"POST", # type: Text + action, # type: Text + method="POST", # type: Text enctype=RenderableForm.ENCTYPE_FORM_DATA, # type: Text - encoding="utf-8" # type: str + encoding="utf-8", # type: str ): # type: (...) -> RenderableFormParam """ @@ -740,7 +795,6 @@ def showForm(self, form): return RenderableFormParam(form, action, method, enctype, encoding) - @implementer(IRequiredParameter, IDependencyInjector) # type: ignore[misc] @attr.s class RenderableFormParam(object): @@ -755,8 +809,9 @@ class RenderableFormParam(object): _enctype = attr.ib(type=Text) _encoding = attr.ib(type=Text) - def registerInjector(self, injectionComponents, parameterName, - requestLifecycle): + def registerInjector( + self, injectionComponents, parameterName, requestLifecycle + ): # type: (Componentized, str, IRequestLifecycle) -> RenderableFormParam return self @@ -768,8 +823,12 @@ def injectValue(self, instance, request, routeParams): return RenderableForm( self._form, ISession(request), # type: ignore[misc] - self._action, self._method, self._enctype, self._encoding, - prevalidationValues={}, validationErrors={}, + self._action, + self._method, + self._enctype, + self._encoding, + prevalidationValues={}, + validationErrors={}, ) def finalize(self): diff --git a/src/klein/_headers.py b/src/klein/_headers.py index 985a2220..29661df3 100644 --- a/src/klein/_headers.py +++ b/src/klein/_headers.py @@ -23,7 +23,7 @@ # Encoding/decoding header data -HEADER_NAME_ENCODING = "iso-8859-1" +HEADER_NAME_ENCODING = "iso-8859-1" HEADER_VALUE_ENCODING = "iso-8859-1" @@ -81,6 +81,7 @@ def normalizeHeaderName(name): # Internal data representation + def normalizeRawHeaders(headerPairs): # type: (Iterable[Iterable[String]]) -> Iterable[RawHeader] for pair in headerPairs: @@ -116,10 +117,7 @@ def getFromRawHeaders(rawHeaders, name): if isinstance(name, Text): rawName = headerNameAsBytes(normalizeHeaderName(name)) - return( - headerValueAsText(v) - for n, v in rawHeaders if rawName == n - ) + return (headerValueAsText(v) for n, v in rawHeaders if rawName == n) raise TypeError("name {!r} must be text or bytes".format(name)) @@ -139,16 +137,16 @@ def rawHeaderNameAndValue(name, value): if isinstance(name, bytes): if not isinstance(value, bytes): raise TypeError( - "value {!r} must be bytes to match name {!r}" - .format(value, name) + "value {!r} must be bytes to match name {!r}".format( + value, name + ) ) return (name, value) elif isinstance(name, Text): if not isinstance(value, Text): raise TypeError( - "value {!r} must be text to match name {!r}" - .format(value, name) + "value {!r} must be text to match name {!r}".format(value, name) ) return (headerNameAsBytes(name), headerValueAsBytes(value)) @@ -156,9 +154,9 @@ def rawHeaderNameAndValue(name, value): raise TypeError("name {!r} must be text or bytes".format(name)) - # Implementation + @implementer(IHTTPHeaders) # type: ignore[misc] @attrs(frozen=True) class FrozenHTTPHeaders(object): @@ -167,17 +165,14 @@ class FrozenHTTPHeaders(object): """ rawHeaders = attrib( - converter=normalizeRawHeadersFrozen, - default=(), + converter=normalizeRawHeadersFrozen, default=(), ) # type: RawHeaders - def getValues(self, name): # type: (AnyStr) -> Iterable[AnyStr] return getFromRawHeaders(self.rawHeaders, name) - @implementer(IMutableHTTPHeaders) # type: ignore[misc] @attrs(frozen=True) class MutableHTTPHeaders(object): @@ -186,29 +181,24 @@ class MutableHTTPHeaders(object): """ _rawHeaders = attrib( - converter=normalizeRawHeadersMutable, - default=Factory(list), + converter=normalizeRawHeadersMutable, default=Factory(list), ) # type: MutableRawHeaders - @property def rawHeaders(self): # type: () -> RawHeaders return tuple(self._rawHeaders) - def getValues(self, name): # type: (AnyStr) -> Iterable[AnyStr] return getFromRawHeaders(self._rawHeaders, name) - def remove(self, name): # type: (String) -> None rawName = rawHeaderName(name) self._rawHeaders[:] = [p for p in self._rawHeaders if p[0] != rawName] - def addValue(self, name, value): # type: (AnyStr, AnyStr) -> None self._rawHeaders.append(rawHeaderNameAndValue(name, value)) diff --git a/src/klein/_headers_compat.py b/src/klein/_headers_compat.py index 427b5212..c9c73ee6 100644 --- a/src/klein/_headers_compat.py +++ b/src/klein/_headers_compat.py @@ -15,8 +15,13 @@ from zope.interface import implementer from ._headers import ( - IMutableHTTPHeaders, RawHeaders, String, headerNameAsBytes, - headerValueAsText, normalizeHeaderName, rawHeaderName, + IMutableHTTPHeaders, + RawHeaders, + String, + headerNameAsBytes, + headerValueAsText, + normalizeHeaderName, + rawHeaderName, rawHeaderNameAndValue, ) @@ -24,7 +29,6 @@ __all__ = () - @implementer(IMutableHTTPHeaders) # type: ignore[misc] @attrs(frozen=True) class HTTPHeadersWrappingHeaders(object): @@ -40,7 +44,6 @@ class HTTPHeadersWrappingHeaders(object): _headers = attrib(validator=instance_of(Headers)) # type: Headers - @property def rawHeaders(self): # type: () -> RawHeaders @@ -53,7 +56,6 @@ def pairs(): return tuple(pairs()) - def getValues(self, name): # type: (AnyStr) -> Iterable[AnyStr] if isinstance(name, bytes): @@ -70,12 +72,10 @@ def getValues(self, name): return values - def remove(self, name): # type: (String) -> None self._headers.removeHeader(rawHeaderName(name)) - def addValue(self, name, value): # type: (AnyStr, AnyStr) -> None rawName, rawValue = rawHeaderNameAndValue(name, value) diff --git a/src/klein/_iapp.py b/src/klein/_iapp.py index 0c2a7abc..fe66fa8d 100644 --- a/src/klein/_iapp.py +++ b/src/klein/_iapp.py @@ -3,15 +3,18 @@ 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, + self, + endpoint, + values=None, + method=None, + force_external=False, + append_unknown=True, ): """ L{werkzeug.routing.MapAdapter.build} diff --git a/src/klein/_imessage.py b/src/klein/_imessage.py index fa3b0aa2..74ab4e8a 100644 --- a/src/klein/_imessage.py +++ b/src/klein/_imessage.py @@ -32,7 +32,6 @@ MutableRawHeaders = MutableSequence[RawHeader] - class FountAlreadyAccessedError(Exception): """ The HTTP message's fount has already been accessed and is no longer @@ -40,7 +39,6 @@ class FountAlreadyAccessedError(Exception): """ - class IHTTPHeaders(Interface): """ HTTP entity headers. @@ -80,7 +78,6 @@ class IHTTPHeaders(Interface): """ ) # type: RawHeaders - @ifmethod def getValues(name): # type: (AnyStr) -> Iterable[AnyStr] @@ -100,7 +97,6 @@ def getValues(name): """ - class IMutableHTTPHeaders(IHTTPHeaders): """ Mutable HTTP entity headers. @@ -118,7 +114,6 @@ def remove(name): @param name: The name of the header to remove. """ - @ifmethod def addValue(name, value): # type: (AnyStr, AnyStr) -> None @@ -132,7 +127,6 @@ def addValue(name, value): """ - class IHTTPMessage(Interface): """ HTTP entity. @@ -140,7 +134,6 @@ class IHTTPMessage(Interface): headers = Attribute("Entity headers.") # type: IHTTPHeaders - @ifmethod def bodyAsFount(): # type: () -> IFount @@ -159,7 +152,6 @@ def bodyAsFount(): accessed. """ - @ifmethod def bodyAsBytes(): # type: () -> Deferred[bytes] @@ -183,15 +175,13 @@ def bodyAsBytes(): """ - class IHTTPRequest(IHTTPMessage): """ HTTP request. """ method = Attribute("Request method.") # type: Text - uri = Attribute("Request URI.") # type: DecodedURL - + uri = Attribute("Request URI.") # type: DecodedURL class IHTTPResponse(IHTTPMessage): diff --git a/src/klein/_interfaces.py b/src/klein/_interfaces.py index 1a9d1638..d90695ba 100644 --- a/src/klein/_interfaces.py +++ b/src/klein/_interfaces.py @@ -21,7 +21,7 @@ IKleinRequest # Silence linter -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import Union from ._headers import FrozenHTTPHeaders, MutableHTTPHeaders @@ -39,9 +39,7 @@ ] IMutableHTTPHeaders = Union[ - _IMutableHTTPHeaders, - HTTPHeadersWrappingHeaders, - MutableHTTPHeaders, + _IMutableHTTPHeaders, HTTPHeadersWrappingHeaders, MutableHTTPHeaders, ] IHTTPMessage = Union[ @@ -53,22 +51,19 @@ ] IHTTPRequest = Union[ - _IHTTPRequest, - FrozenHTTPRequest, - HTTPRequestWrappingIRequest, + _IHTTPRequest, FrozenHTTPRequest, HTTPRequestWrappingIRequest, ] IHTTPResponse = Union[ - _IHTTPResponse, - FrozenHTTPResponse, + _IHTTPResponse, FrozenHTTPResponse, ] else: - IHTTPHeaders = _IHTTPHeaders + IHTTPHeaders = _IHTTPHeaders IMutableHTTPHeaders = _IMutableHTTPHeaders - IHTTPMessage = _IHTTPMessage - IHTTPRequest = _IHTTPRequest - IHTTPResponse = _IHTTPResponse + IHTTPMessage = _IHTTPMessage + IHTTPRequest = _IHTTPRequest + IHTTPResponse = _IHTTPResponse __all__ = () diff --git a/src/klein/_isession.py b/src/klein/_isession.py index 9ffa78ef..eb0e7989 100644 --- a/src/klein/_isession.py +++ b/src/klein/_isession.py @@ -1,18 +1,17 @@ - from typing import Any, TYPE_CHECKING 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 zope.interface import Attribute, Interface from ._typing import ifmethod -if TYPE_CHECKING: # pragma: no cover +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 @@ -20,7 +19,6 @@ from zope.interface.interfaces import IInterface - class NoSuchSession(Exception): """ No such session could be found. @@ -39,7 +37,6 @@ class TransactionEnded(Exception): """ - class ISessionStore(Interface): """ Backing storage for sessions. @@ -55,7 +52,6 @@ def newSession(isConfidential, authenticatedBy): @rtype: L{Deferred} firing with L{ISession}. """ - @ifmethod def loadSession(identifier, isConfidential, authenticatedBy): # type: (Text, bool, SessionMechanism) -> Deferred @@ -77,7 +73,6 @@ def loadSession(identifier, isConfidential, authenticatedBy): L{NoSuchSession}. """ - @ifmethod def sentInsecurely(identifiers): # type: (Sequence[Text]) -> None @@ -87,7 +82,6 @@ def sentInsecurely(identifiers): """ - class ISimpleAccountBinding(Interface): """ Data-store agnostic account / session binding manipulation API for "simple" @@ -133,7 +127,6 @@ def createAccount(username, email, password): """ - class ISimpleAccount(Interface): """ Data-store agnostic account interface. @@ -158,7 +151,6 @@ def bindSession(self, session): session to act on behalf of this account. """ - def changePassword(self, newPassword): # type: (Text) -> None """ @@ -166,7 +158,6 @@ def changePassword(self, newPassword): """ - class ISessionProcurer(Interface): """ An L{ISessionProcurer} wraps an L{ISessionStore} and can procure sessions @@ -235,7 +226,6 @@ class SessionMechanism(Names): Header = NamedConstant() - class ISession(Interface): """ An L{ISession} provider contains an identifier for the session, information @@ -274,7 +264,6 @@ class ISession(Interface): """ ) - @ifmethod def authorize(interfaces): # type: (Iterable[IInterface]) -> Deferred @@ -367,16 +356,16 @@ def registerInjector(injectionComponents, parameterName, lifecycle): """ - class IRequestLifecycle(Interface): """ Interface for adding hooks to the phases of a request's lifecycle. """ -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import Union from ._requirer import RequestLifecycle + IRequestLifecycleT = Union[RequestLifecycle, IRequestLifecycle] @@ -392,4 +381,5 @@ class EarlyExit(Exception): @type alternateReturnValue: Any type that's acceptable to return from a Klein route. """ + alternateReturnValue = attr.ib(type=Any) diff --git a/src/klein/_message.py b/src/klein/_message.py index 2c4f3bfb..44a9df46 100644 --- a/src/klein/_message.py +++ b/src/klein/_message.py @@ -24,7 +24,6 @@ InternalBody = Union[bytes, IFount] - @attrs(frozen=False) class MessageState(object): """ @@ -33,12 +32,13 @@ class MessageState(object): cachedBody = attrib( type=Optional[bytes], - validator=optional(instance_of(bytes)), default=None, init=False + validator=optional(instance_of(bytes)), + default=None, + init=False, ) fountExhausted = attrib( - type=bool, - validator=instance_of(bool), default=False, init=False + type=bool, validator=instance_of(bool), default=False, init=False ) @@ -48,10 +48,7 @@ def validateBody(instance, attribute, body): Validator for L{InternalBody}. """ - if ( - not isinstance(body, bytes) and - not IFount.providedBy(body) - ): + if not isinstance(body, bytes) and not IFount.providedBy(body): raise TypeError("body must be bytes or IFount") diff --git a/src/klein/_plating.py b/src/klein/_plating.py index 90eeb83a..1b8a8d79 100644 --- a/src/klein/_plating.py +++ b/src/klein/_plating.py @@ -20,21 +20,23 @@ from ._app import _call from ._decorators import bindable, modified, originalName -if TYPE_CHECKING: # pragma: no cover +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]]] # https://github.com/python/mypy/issues/224 ATOM_TYPES = ( - cast(Tuple[Any, ...], integer_types) + - cast(Tuple[Any, ...], string_types) + - cast(Tuple[Any, ...], (float, None.__class__)) + cast(Tuple[Any, ...], integer_types) + + cast(Tuple[Any, ...], string_types) + + cast(Tuple[Any, ...], (float, None.__class__)) ) + def _should_return_json(request): # type: (IRequest) -> bool """ @@ -75,10 +77,12 @@ def resolveDeferredObjects(root): parent = [None] * len(obj) # type: Any setter(parent) stack.extend( - reversed([ - (child, partial(setitem, parent, i)) - for i, child in enumerate(obj) - ]) + reversed( + [ + (child, partial(setitem, parent, i)) + for i, child in enumerate(obj) + ] + ) ) elif isinstance(obj, tuple): parent = [None] * len(obj) @@ -89,10 +93,12 @@ def setTupleItem(i, value, parent=parent, setter=setter): setter(tuple(parent)) stack.extend( - reversed([ - (child, partial(setTupleItem, i)) - for i, child in enumerate(obj) - ]) + reversed( + [ + (child, partial(setTupleItem, i)) + for i, child in enumerate(obj) + ] + ) ) elif isinstance(obj, dict): parent = {} @@ -111,14 +117,12 @@ def setValue(value, pair=pair, parent=parent): stack.append((obj._asJSON(), setter)) else: raise TypeError( - obj, - "{input} not JSON serializable".format(input=obj), + obj, "{input} not JSON serializable".format(input=obj), ) returnValue(result[0]) - def _extra_types(input): """ Renderability for a few additional types. @@ -128,15 +132,15 @@ def _extra_types(input): return input - class PlatedElement(Element): """ The element type returned by L{Plating}. This contains several utility renderers. """ - def __init__(self, slot_data, preloaded, boundInstance, presentationSlots, - renderers): + def __init__( + self, slot_data, preloaded, boundInstance, presentationSlots, renderers + ): """ @param slot_data: A dictionary mapping names to values. @@ -147,12 +151,13 @@ def __init__(self, slot_data, preloaded, boundInstance, presentationSlots, self._presentationSlots = presentationSlots self._renderers = renderers super(PlatedElement, self).__init__( - loader=TagLoader(preloaded.fillSlots( - **{k: _extra_types(v) for k, v in slot_data.items()} - )) + loader=TagLoader( + preloaded.fillSlots( + **{k: _extra_types(v) for k, v in slot_data.items()} + ) + ) ) - def _asJSON(self): """ Render this L{PlatedElement} as JSON-serializable data. @@ -162,7 +167,6 @@ def _asJSON(self): json_data.pop(ignored, None) return json_data - def lookupRenderMethod(self, name): """ @return: a renderer. @@ -173,8 +177,10 @@ def lookupRenderMethod(self, name): @modified("plated render wrapper", wrapped) def renderWrapper(request, tag, *args, **kw): # type: (IRequest, Tag, *Any, **Any) -> Any - return _call(self._boundInstance, wrapped, - request, tag, *args, **kw) + return _call( + self._boundInstance, wrapped, request, tag, *args, **kw + ) + return renderWrapper if ":" not in name: raise MissingRenderMethod(self, name) @@ -183,6 +189,7 @@ def renderWrapper(request, tag, *args, **kw): def renderList(request, tag): for item in self.slot_data[slot]: yield tag.fillSlots(item=_extra_types(item)) + types = { "list": renderList, } @@ -192,7 +199,6 @@ def renderList(request, tag): raise MissingRenderMethod(self, name) - class Plating(object): """ A L{Plating} is a container which can be used to generate HTML from data. @@ -202,8 +208,7 @@ class Plating(object): CONTENT = "klein:plating:content" - def __init__(self, defaults=None, tags=None, - presentation_slots=()): + def __init__(self, defaults=None, tags=None, presentation_slots=()): """ """ self._defaults = {} if defaults is None else defaults @@ -211,7 +216,6 @@ def __init__(self, defaults=None, tags=None, self._presentationSlots = {self.CONTENT} | set(presentation_slots) self._renderers = {} - def renderMethod(self, renderer): """ Add a render method to this L{Plating} object that can be used in the @@ -223,10 +227,10 @@ def renderMethod(self, renderer): self._renderers[text_type(originalName(renderer))] = renderer return renderer - def routed(self, routing, tags): """ """ + def mydecorator(method): loader = TagLoader(tags) @@ -241,22 +245,26 @@ def mymethod(instance, request, *args, **kw): json_data.update(data) for ignored in self._presentationSlots: json_data.pop(ignored, None) - text_type = u'json' + text_type = u"json" ready = yield resolveDeferredObjects(json_data) result = dumps(ready) else: data[self.CONTENT] = loader.load() - text_type = u'html' + text_type = u"html" result = self._elementify(instance, data) request.setHeader( - b'content-type', (u'text/{format}; charset=utf-8' - .format(format=text_type) - .encode("charmap")) + b"content-type", + ( + u"text/{format}; charset=utf-8".format( + format=text_type + ).encode("charmap") + ), ) returnValue(result) + return method - return mydecorator + return mydecorator def _elementify(self, instance, to_fill_with): """ @@ -266,11 +274,13 @@ def _elementify(self, instance, to_fill_with): slot_data.update(to_fill_with) [loaded] = self._loader.load() loaded = loaded.clone() - return PlatedElement(slot_data=slot_data, - preloaded=loaded, - renderers=self._renderers, - boundInstance=instance, - presentationSlots=self._presentationSlots) + return PlatedElement( + slot_data=slot_data, + preloaded=loaded, + renderers=self._renderers, + boundInstance=instance, + presentationSlots=self._presentationSlots, + ) @attr.s class _Widget(object): @@ -283,7 +293,8 @@ class _Widget(object): instance's L{Plating._elementify} to construct a L{PlatedElement}. """ - _plating = attr.ib(type='Plating') + + _plating = attr.ib(type="Plating") _function = attr.ib(type=Callable[..., Any]) _instance = attr.ib(type=object) @@ -304,11 +315,9 @@ def widget(self, *args, **kwargs): data = self._function(*args, **kwargs) return self._plating._elementify(self._instance, data) - def __getattr__(self, attr): return getattr(self._function, attr) - def widgeted(self, function): """ A decorator that turns a function into a renderer for an diff --git a/src/klein/_request.py b/src/klein/_request.py index 82d38d6f..c2718806 100644 --- a/src/klein/_request.py +++ b/src/klein/_request.py @@ -25,7 +25,6 @@ __all__ = () - @implementer(IHTTPRequest) # type: ignore[misc] @attrs(frozen=True) class FrozenHTTPRequest(object): @@ -33,8 +32,8 @@ class FrozenHTTPRequest(object): Immutable HTTP request. """ - method = attrib(validator=instance_of(Text)) # type: Text - uri = attrib(validator=instance_of(DecodedURL)) # type: DecodedURL + method = attrib(validator=instance_of(Text)) # type: Text + uri = attrib(validator=instance_of(DecodedURL)) # type: DecodedURL headers = attrib( validator=provides(IHTTPHeaders) # type: ignore[misc] ) # type: IHTTPHeaders @@ -45,12 +44,10 @@ class FrozenHTTPRequest(object): default=Factory(MessageState), init=False ) # type: MessageState - def bodyAsFount(self): # type: () -> IFount return bodyAsFount(self._body, self._state) - def bodyAsBytes(self): # type: () -> Deferred[bytes] return bodyAsBytes(self._body, self._state) diff --git a/src/klein/_request_compat.py b/src/klein/_request_compat.py index c3108a9e..f4502f53 100644 --- a/src/klein/_request_compat.py +++ b/src/klein/_request_compat.py @@ -34,7 +34,6 @@ noneIO = BytesIO() - @implementer(IHTTPRequest) # type: ignore[misc] @attrs(frozen=True) class HTTPRequestWrappingIRequest(object): @@ -50,13 +49,11 @@ class HTTPRequestWrappingIRequest(object): default=Factory(MessageState), init=False ) # type: MessageState - @property def method(self): # type: () -> Text return self._request.method.decode("ascii") - @property def uri(self): # type: () -> DecodedURL @@ -85,13 +82,11 @@ def uri(self): return DecodedURL.fromText(u"{}://{}/{}".format(scheme, netloc, path)) - @property def headers(self): # type: () -> IHTTPHeaders return HTTPHeadersWrappingHeaders(headers=self._request.requestHeaders) - def bodyAsFount(self): # type: () -> IFount source = self._request.content @@ -104,7 +99,6 @@ def bodyAsFount(self): return fount - def bodyAsBytes(self): # type: () -> Deferred[bytes] if self._state.cachedBody is not None: diff --git a/src/klein/_requirer.py b/src/klein/_requirer.py index e132d5e8..991331e1 100644 --- a/src/klein/_requirer.py +++ b/src/klein/_requirer.py @@ -1,4 +1,3 @@ - from typing import Any, Callable, List, TYPE_CHECKING import attr @@ -20,13 +19,13 @@ from .interfaces import IDependencyInjector, IRequiredParameter - @implementer(IRequestLifecycle) # type: ignore[misc] @attr.s class RequestLifecycle(object): """ Mechanism to run hooks at the start of a request managed by a L{Requirer}. """ + _prepareHooks = attr.ib(type=List, default=attr.Factory(list)) def addPrepareHook(self, beforeHook, requires=(), provides=()): @@ -42,7 +41,6 @@ def addPrepareHook(self, beforeHook, requires=(), provides=()): # TODO: topological requirements sort self._prepareHooks.append(beforeHook) - @inlineCallbacks def runPrepareHooks(self, instance, request): # type: (Any, IRequest) -> Deferred @@ -58,27 +56,26 @@ def runPrepareHooks(self, instance, request): yield _call(instance, hook, request) -_routeDecorator = Any # a decorator like @route -_routeT = Any # a thing decorated by a decorator like @route +_routeDecorator = Any # a decorator like @route +_routeT = Any # a thing decorated by a decorator like @route _prerequisiteCallback = Callable[[IRequestLifecycle], None] - @attr.s class Requirer(object): """ Dependency injection for required parameters. """ + _prerequisites = attr.ib( - type=List[_prerequisiteCallback], - default=attr.Factory(list) + type=List[_prerequisiteCallback], default=attr.Factory(list) ) def prerequisite( - self, - providesComponents, # type: Sequence[IInterface] - requiresComponents=() # type: Sequence[IInterface] + self, + providesComponents, # type: Sequence[IInterface] + requiresComponents=(), # type: Sequence[IInterface] ): # type: (...) -> Callable[[Callable], Callable] """ @@ -97,18 +94,21 @@ def fooForRequest(request): dependencies; you must presently register prerequisites in the order you want them to be called. """ + def decorator(prerequisiteMethod): # type: (Callable) -> Callable def oneHook(lifecycle): # type: (IRequestLifecycle) -> None lifecycle.addPrepareHook( - prerequisiteMethod, requires=requiresComponents, - provides=providesComponents + prerequisiteMethod, + requires=requiresComponents, + provides=providesComponents, ) + self._prerequisites.append(oneHook) return prerequisiteMethod - return decorator + return decorator def require(self, routeDecorator, **requiredParameters): # type: (_routeT, **IRequiredParameter) -> _routeDecorator @@ -124,7 +124,7 @@ def decorator(functionWithRequirements): IRequestLifecycle, lifecycle # type: ignore[misc] ) - injectors = {} # type: Dict[str, IDependencyInjector] + injectors = {} # type: Dict[str, IDependencyInjector] for parameterName, required in requiredParameters.items(): injectors[parameterName] = required.registerInjector( diff --git a/src/klein/_resource.py b/src/klein/_resource.py index 209545b3..1870fb5c 100644 --- a/src/klein/_resource.py +++ b/src/klein/_resource.py @@ -17,7 +17,6 @@ from ._interfaces import IKleinRequest - def ensure_utf8_bytes(v): """ Coerces a value which is either a C{unicode} or C{str} to a C{str}. @@ -28,7 +27,6 @@ def ensure_utf8_bytes(v): return v - class _StandInResource(object): """ A standin for a Resource. @@ -38,11 +36,11 @@ class _StandInResource(object): """ - class _URLDecodeError(Exception): """ Raised if one or more string parts of the URL could not be decoded. """ + __slots__ = ["errors"] def __init__(self, errors): @@ -57,7 +55,6 @@ def __repr__(self): return "".format(self.errors) - def _extractURLparts(request): """ Extracts and decodes URI parts from C{request}. @@ -74,29 +71,33 @@ def _extractURLparts(request): @rtype: L{tuple} of L{unicode}, L{unicode}, L{int}, L{unicode}, L{unicode} """ server_name = request.getRequestHostname() - if hasattr(request.getHost(), 'port'): + if hasattr(request.getHost(), "port"): server_port = request.getHost().port else: server_port = 0 if (bool(request.isSecure()), server_port) not in [ - (True, 443), (False, 80), (False, 0), (True, 0)]: + (True, 443), + (False, 80), + (False, 0), + (True, 0), + ]: server_name = server_name + b":" + intToBytes(server_port) - script_name = b'' + script_name = b"" if request.prepath: - script_name = b'/'.join(request.prepath) + script_name = b"/".join(request.prepath) - if not script_name.startswith(b'/'): - script_name = b'/' + script_name + if not script_name.startswith(b"/"): + script_name = b"/" + script_name - path_info = b'' + path_info = b"" if request.postpath: - path_info = b'/'.join(request.postpath) + path_info = b"/".join(request.postpath) - if not path_info.startswith(b'/'): - path_info = b'/' + path_info + if not path_info.startswith(b"/"): + path_info = b"/" + path_info - url_scheme = u'https' if request.isSecure() else u'http' + url_scheme = u"https" if request.isSecure() else u"http" utf8Failures = [] try: @@ -118,38 +119,38 @@ def _extractURLparts(request): return url_scheme, server_name, server_port, path_info, script_name - class KleinResource(Resource): """ A ``Resource`` that can do URL routing. """ - isLeaf = True + isLeaf = True def __init__(self, app): Resource.__init__(self) self._app = app - def __eq__(self, other): if isinstance(other, KleinResource): return vars(self) == vars(other) return NotImplemented - def __ne__(self, other): result = self.__eq__(other) if result is NotImplemented: return result return not result - def render(self, request): # Stuff we need to know for the mapper. try: - url_scheme, server_name, server_port, path_info, script_name = ( - _extractURLparts(request) - ) + ( + url_scheme, + server_name, + server_port, + path_info, + script_name, + ) = _extractURLparts(request) except _URLDecodeError as e: for what, fail in e.errors: log.err(fail, "Invalid encoding in {what}.".format(what=what)) @@ -192,10 +193,9 @@ def _execute(): # Standard Twisted Web stuff. Defer the method action, giving us # something renderable or printable. Return NOT_DONE_YET and set up # the incremental renderer. - d = defer.maybeDeferred(self._app.execute_endpoint, - endpoint, - request, - **kwargs) + d = defer.maybeDeferred( + self._app.execute_endpoint, endpoint, request, **kwargs + ) request.notifyFinish().addErrback(lambda _: d.cancel()) @@ -247,7 +247,7 @@ def processing_failed(failure, error_handlers): ensure_utf8_bytes(header), ensure_utf8_bytes(value) ) - return ensure_utf8_bytes(b''.join(resp.iter_encoded())) + return ensure_utf8_bytes(b"".join(resp.iter_encoded())) else: request.processingFailed(failure) return @@ -257,10 +257,12 @@ def processing_failed(failure, error_handlers): # Each error handler is a tuple of # (list_of_exception_types, handler_fn) if failure.check(*error_handler[0]): - d = defer.maybeDeferred(self._app.execute_error_handler, - error_handler[1], - request, - failure) + d = defer.maybeDeferred( + self._app.execute_error_handler, + error_handler[1], + request, + failure, + ) d.addCallback(process) @@ -273,7 +275,7 @@ def processing_failed(failure, error_handlers): def write_response(r): if r is not _StandInResource: if isinstance(r, unicode): - r = r.encode('utf-8') + r = r.encode("utf-8") if (r is not None) and (r != NOT_DONE_YET): request.write(r) diff --git a/src/klein/_response.py b/src/klein/_response.py index 600d9bff..dae5f03d 100644 --- a/src/klein/_response.py +++ b/src/klein/_response.py @@ -23,7 +23,6 @@ __all__ = () - @implementer(IHTTPResponse) # type: ignore[misc] @attrs(frozen=True) class FrozenHTTPResponse(object): @@ -43,12 +42,10 @@ class FrozenHTTPResponse(object): default=Factory(MessageState), init=False ) # type: MessageState - def bodyAsFount(self): # type: () -> IFount return bodyAsFount(self._body, self._state) - def bodyAsBytes(self): # type: () -> Deferred[bytes] return bodyAsBytes(self._body, self._state) diff --git a/src/klein/_session.py b/src/klein/_session.py index 994ad2c8..ad47ec16 100644 --- a/src/klein/_session.py +++ b/src/klein/_session.py @@ -1,8 +1,6 @@ # -*- test-case-name: klein.test.test_session -*- -from typing import ( - Any, Callable, Optional as _Optional, TYPE_CHECKING, Union -) +from typing import Any, Callable, Optional as _Optional, TYPE_CHECKING, Union import attr @@ -15,22 +13,29 @@ from zope.interface.interfaces import IInterface from .interfaces import ( - EarlyExit, IDependencyInjector, IRequestLifecycle, IRequiredParameter, - ISession, ISessionProcurer, ISessionStore, NoSuchSession, SessionMechanism, - TooLateForCookies + EarlyExit, + IDependencyInjector, + IRequestLifecycle, + IRequiredParameter, + ISession, + ISessionProcurer, + ISessionStore, + NoSuchSession, + SessionMechanism, + TooLateForCookies, ) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from mypy_extensions import Arg, KwArg from twisted.web.iweb import IRequest from twisted.python.components import Componentized from typing import Dict, Sequence, Text, TypeVar - T = TypeVar('T') + + T = TypeVar("T") else: Arg = KwArg = lambda t, *x: t - @implementer(ISessionProcurer) # type: ignore[misc] @attr.s class SessionProcurer(object): @@ -83,8 +88,7 @@ class SessionProcurer(object): _cookiePath = attr.ib(type=bytes, default=b"/") _secureTokenHeader = attr.ib(type=bytes, default=b"X-Auth-Token") - _insecureTokenHeader = attr.ib(type=bytes, - default=b"X-INSECURE-Auth-Token") + _insecureTokenHeader = attr.ib(type=bytes, default=b"X-INSECURE-Auth-Token") _setCookieOnGET = attr.ib(type=bool, default=True) @inlineCallbacks @@ -107,14 +111,23 @@ def procureSession(self, request, forceInsecure=False): else: # 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] - ) # type: Sequence[Text] + 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 + ] # 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! yield self._store.sentInsecurely(allPossibleSentTokens) @@ -142,9 +155,8 @@ def procureSession(self, request, forceInsecure=False): if mechanism == SessionMechanism.Header: raise session = None - if ( - mechanism == SessionMechanism.Cookie and - (session is None or session.identifier != sentCookie) + if mechanism == SessionMechanism.Cookie and ( + session is None or session.identifier != sentCookie ): if session is None: if request.startedWriting: @@ -155,14 +167,16 @@ def procureSession(self, request, forceInsecure=False): " late in the request pipeline; the headers" " were already sent." ) - if request.method != b'GET': + if request.method != b"GET": # Sessions should only ever be auto-created by GET # requests; there's no way that any meaningful data # manipulation could succeed (no CSRF token check could # ever succeed, for example). raise NoSuchSession( - u"Can't initialize a session on a {method} request." - .format(method=request.method.decode("ascii")) + u"Can't initialize a session on a " + u"{method} request.".format( + method=request.method.decode("ascii") + ) ) if not self._setCookieOnGET: # We don't have a session ID at all, and we're not allowed @@ -177,9 +191,13 @@ def procureSession(self, request, forceInsecure=False): if not isinstance(cookieName, str): cookieName = cookieName.decode("ascii") request.addCookie( - cookieName, identifierInCookie, max_age=str(self._maxAge), - domain=self._cookieDomain, path=self._cookiePath, - secure=sentSecurely, httpOnly=True, + cookieName, + identifierInCookie, + max_age=str(self._maxAge), + domain=self._cookieDomain, + path=self._cookiePath, + secure=sentSecurely, + httpOnly=True, ) if sentSecurely or not request.isSecure(): # Do not cache the insecure session on the secure request, thanks. @@ -188,16 +206,17 @@ def procureSession(self, request, forceInsecure=False): _procureProcurerType = Union[ - Callable[[Any], ISessionProcurer], - Callable[[], ISessionProcurer] + Callable[[Any], ISessionProcurer], Callable[[], ISessionProcurer] ] _kleinRenderable = Any _routeCallable = Any _kleinCallable = Callable[..., _kleinRenderable] _kleinDecorator = Callable[[_kleinCallable], _kleinCallable] -_requirerResult = Callable[[Arg(_routeCallable, 'route'), KwArg(Any)], - Callable[[_kleinCallable], _kleinCallable]] +_requirerResult = Callable[ + [Arg(_routeCallable, "route"), KwArg(Any)], + Callable[[_kleinCallable], _kleinCallable], +] class AuthorizationDenied(Resource, object): @@ -209,7 +228,7 @@ def __init__(self, interface, instance): def render(self, request): # type: (IRequest) -> bytes request.setResponseCode(UNAUTHORIZED) - return "{} DENIED".format(qual(self._interface)).encode('utf-8') + return "{} DENIED".format(qual(self._interface)).encode("utf-8") @implementer(IDependencyInjector, IRequiredParameter) # type: ignore[misc] @@ -266,10 +285,12 @@ def myRoute(adminPowers): passed to L{Requirer.require}? Note that this will never be used if C{required} is set to C{False}. """ + _interface = attr.ib(type=IInterface) _required = attr.ib(type=bool, default=True) - _whenDenied = attr.ib(type=Callable[[IInterface, Any], Any], - default=AuthorizationDenied) + _whenDenied = attr.ib( + type=Callable[[IInterface, Any], Any], default=AuthorizationDenied + ) def registerInjector(self, injectionComponents, parameterName, lifecycle): # type: (Componentized, str, IRequestLifecycle) -> IDependencyInjector @@ -278,7 +299,6 @@ def registerInjector(self, injectionComponents, parameterName, lifecycle): """ return self - @inlineCallbacks def injectValue(self, instance, request, routeParams): # type: (Any, IRequest, Dict[str, Any]) -> Any @@ -289,15 +309,14 @@ def injectValue(self, instance, request, routeParams): # collecting all the interfaces that are necessary and then using # addBeforeHook; the interface would not need to change. session = ISession(request) # type: ignore[misc] - provider = ( - (yield session.authorize([self._interface])).get(self._interface) + 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 returnValue(provider) - def finalize(self): # type: () -> None """ diff --git a/src/klein/_tubes.py b/src/klein/_tubes.py index 07784e9c..06e61f81 100644 --- a/src/klein/_tubes.py +++ b/src/klein/_tubes.py @@ -41,7 +41,6 @@ def bytesToFount(data): return IOFount(source=BytesIO(data)) - # https://github.com/twisted/tubes/issues/61 @implementer(IFount) @attrs(frozen=False) @@ -59,12 +58,10 @@ class IOFount(object): ) # type: IDrain _paused = attrib(validator=instance_of(bool), default=False, init=False) - def __attrs_post_init__(self): # type: () -> None self._pauser = Pauser(self._pause, self._resume) - def _flowToDrain(self): # type: () -> None if self.drain is not None and not self._paused: @@ -73,29 +70,24 @@ def _flowToDrain(self): self.drain.receive(data) self.drain.flowStopped(Failure(StopIteration())) - def flowTo(self, drain): # type: (IDrain) -> IFount result = beginFlowingTo(self, drain) self._flowToDrain() return result - def pauseFlow(self): # type: () -> None return self._pauser.pause() - def stopFlow(self): # type: () -> None return self._pauser.resume() - def _pause(self): # type: () -> None self._paused = True - def _resume(self): # type: () -> None self._paused = False diff --git a/src/klein/_typing.py b/src/klein/_typing.py index 2670e6b5..a4d4c16b 100644 --- a/src/klein/_typing.py +++ b/src/klein/_typing.py @@ -4,8 +4,9 @@ __all__ = () -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover ifmethod = staticmethod else: + def ifmethod(method): return method diff --git a/src/klein/_version.py b/src/klein/_version.py index a3d502a5..128af21a 100644 --- a/src/klein/_version.py +++ b/src/klein/_version.py @@ -7,5 +7,5 @@ from incremental import Version -__version__ = Version('klein', 19, 6, 0) +__version__ = Version("klein", 19, 6, 0) __all__ = ["__version__"] diff --git a/src/klein/interfaces.py b/src/klein/interfaces.py index e07cd314..955d4b28 100644 --- a/src/klein/interfaces.py +++ b/src/klein/interfaces.py @@ -4,9 +4,7 @@ ValidationError, ValueAbsent, ) -from ._interfaces import ( - IKleinRequest, -) +from ._interfaces import IKleinRequest from ._isession import ( EarlyExit, IDependencyInjector as _IDependencyInjector, @@ -23,7 +21,7 @@ TransactionEnded, ) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from .storage._memory import MemorySessionStore, MemorySession from ._session import SessionProcurer, Authorization from ._form import Field, RenderableFormParam, FieldInjector @@ -37,12 +35,22 @@ ISession = Union[_ISession, MemorySession] ISimpleAccount = _ISimpleAccount ISimpleAccountBinding = _ISimpleAccountBinding - IDependencyInjector = Union[_IDependencyInjector, Authorization, - RenderableFormParam, FieldInjector, RequestURL, - RequestComponent] - IRequiredParameter = Union[_IRequiredParameter, Authorization, Field, - RenderableFormParam, RequestURL, - RequestComponent] + IDependencyInjector = Union[ + _IDependencyInjector, + Authorization, + RenderableFormParam, + FieldInjector, + RequestURL, + RequestComponent, + ] + IRequiredParameter = Union[ + _IRequiredParameter, + Authorization, + Field, + RenderableFormParam, + RequestURL, + RequestComponent, + ] IRequestLifecycle = _IRequestLifecycleT else: ISession = _ISession diff --git a/src/klein/resource.py b/src/klein/resource.py index d94db486..bf5294c5 100644 --- a/src/klein/resource.py +++ b/src/klein/resource.py @@ -16,8 +16,8 @@ if TYPE_CHECKING: from typing import AnyStr, Callable, Text - KleinResource = _KleinResource + KleinResource = _KleinResource class _SpecialModuleObject(object): @@ -34,13 +34,11 @@ class _SpecialModuleObject(object): KleinResource = _KleinResource - @property def ensure_utf8_bytes(self): # type: () -> Callable[[AnyStr], Text] return ensure_utf8_bytes - def __call__(self): # type: () -> _KleinResource """ @@ -52,7 +50,6 @@ def __call__(self): # confusion. return _globalResourceMethod() - def __repr__(self): # type: () -> str """ @@ -63,4 +60,4 @@ def __repr__(self): preserve = modules[__name__] modules[__name__] = _SpecialModuleObject() # type: ignore -modules[__name__].__preserve__ = preserve # type: ignore +modules[__name__].__preserve__ = preserve # type: ignore diff --git a/src/klein/storage/_memory.py b/src/klein/storage/_memory.py index 122af5a2..fccc281e 100644 --- a/src/klein/storage/_memory.py +++ b/src/klein/storage/_memory.py @@ -13,13 +13,15 @@ from zope.interface.interfaces import IInterface from klein.interfaces import ( - ISession, ISessionStore, NoSuchSession, SessionMechanism + ISession, + ISessionStore, + NoSuchSession, + SessionMechanism, ) _authCB = Callable[[IInterface, ISession, Componentized], Any] - @implementer(ISession) # type: ignore[misc] @attr.s class MemorySession(object): @@ -31,8 +33,7 @@ class MemorySession(object): isConfidential = attr.ib(type=bool) authenticatedBy = attr.ib(type=SessionMechanism) _authorizationCallback = attr.ib(type=_authCB) # type: ignore[misc] - _components = attr.ib(default=Factory(Componentized), - type=Componentized) + _components = attr.ib(default=Factory(Componentized), type=Componentized) def authorize(self, interfaces): # type: (List[IInterface]) -> Deferred @@ -42,18 +43,19 @@ def authorize(self, interfaces): """ result = {} for interface in interfaces: - provider = self._authorizationCallback(interface, self, - self._components) + provider = self._authorizationCallback( + interface, self, self._components + ) if provider is not None: result[interface] = provider return succeed(result) - class _MemoryAuthorizerFunction(object): """ Type shadow for function with the given attribute. """ + __memoryAuthInterface__ = None # type: IInterface def __call__(self, interface, session, data): @@ -62,8 +64,8 @@ def __call__(self, interface, session, data): Return a provider of the given interface. """ -_authFn = Callable[[IInterface, ISession, Componentized], Any] +_authFn = Callable[[IInterface, ISession, Componentized], Any] def declareMemoryAuthorizer(forInterface): @@ -72,11 +74,13 @@ def declareMemoryAuthorizer(forInterface): Declare that the decorated function is an authorizer usable with a memory session store. """ + def decorate(decoratee): # type: (_authFn) -> _MemoryAuthorizerFunction decoratee = cast(_MemoryAuthorizerFunction, decoratee) decoratee.__memoryAuthInterface__ = forInterface return decoratee + return decorate @@ -85,18 +89,19 @@ def _noAuthorization(interface, session, data): return None - @implementer(ISessionStore) # type: ignore[misc] @attr.s class MemorySessionStore(object): authorizationCallback = attr.ib( type=_authFn, # type: ignore[misc] - default=_noAuthorization + default=_noAuthorization, + ) + _secureStorage = attr.ib( + type=Dict[str, Any], default=cast(Dict[str, Any], Factory(dict)) + ) + _insecureStorage = attr.ib( + type=Dict[str, Any], default=cast(Dict[str, Any], Factory(dict)) ) - _secureStorage = attr.ib(type=Dict[str, Any], - default=cast(Dict[str, Any], Factory(dict))) - _insecureStorage = attr.ib(type=Dict[str, Any], - default=cast(Dict[str, Any], Factory(dict))) @classmethod def fromAuthorizers(cls, authorizers): @@ -115,8 +120,8 @@ def authorizationCallback(interface, session, data): return interfaceToCallable.get(interface, _noAuthorization)( interface, session, data ) - return cls(authorizationCallback) + return cls(authorizationCallback) def _storage(self, isConfidential): # type: (bool) -> Dict[str, Any] @@ -128,28 +133,32 @@ def _storage(self, isConfidential): else: return self._insecureStorage - def newSession(self, isConfidential, authenticatedBy): # type: (bool, SessionMechanism) -> Deferred storage = self._storage(isConfidential) - identifier = hexlify(urandom(32)).decode('ascii') - session = MemorySession(identifier, isConfidential, authenticatedBy, - self.authorizationCallback) + identifier = hexlify(urandom(32)).decode("ascii") + session = MemorySession( + identifier, + isConfidential, + authenticatedBy, + self.authorizationCallback, + ) storage[identifier] = session return succeed(session) - def loadSession(self, identifier, isConfidential, authenticatedBy): # type: (str, bool, SessionMechanism) -> Deferred storage = self._storage(isConfidential) if identifier in storage: return succeed(storage[identifier]) else: - return fail(NoSuchSession( - u"Session not found in memory store {id!r}" - .format(id=identifier) - )) - + return fail( + NoSuchSession( + u"Session not found in memory store {id!r}".format( + id=identifier + ) + ) + ) def sentInsecurely(self, tokens): # type: (Iterable[str]) -> None diff --git a/src/klein/storage/memory.py b/src/klein/storage/memory.py index ac481dbf..d915d5a1 100644 --- a/src/klein/storage/memory.py +++ b/src/klein/storage/memory.py @@ -1,7 +1,6 @@ - from ._memory import MemorySessionStore, declareMemoryAuthorizer __all__ = [ - 'declareMemoryAuthorizer', - 'MemorySessionStore', + "declareMemoryAuthorizer", + "MemorySessionStore", ] diff --git a/src/klein/test/_strategies.py b/src/klein/test/_strategies.py index 2de23d8c..7fb9d3c5 100644 --- a/src/klein/test/_strategies.py +++ b/src/klein/test/_strategies.py @@ -14,7 +14,12 @@ from hypothesis import assume from hypothesis.strategies import ( - characters, composite, integers, lists, sampled_from, text + characters, + composite, + integers, + lists, + sampled_from, + text, ) from idna import IDNAError, check_label, encode as idna_encode @@ -27,7 +32,7 @@ __all__ = () -T = TypeVar('T') +T = TypeVar("T") DrawCallable = Callable[[Callable[..., T]], T] @@ -76,6 +81,7 @@ def idna_characters(): # pragma: no cover return _idnaCharacters + _idnaCharacters = None # type: Optional[str] @@ -91,9 +97,9 @@ 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 draw( + text(min_size=min_size, max_size=max_size, alphabet=ascii_letters) + ) @composite # pragma: no cover @@ -108,10 +114,15 @@ def latin1_text(draw, min_size=0, max_size=None): @param max_size: The maximum number of characters in the text. Use C{None} for an unbounded size. """ - return u"".join(draw(lists( - characters(max_codepoint=255), - min_size=min_size, max_size=max_size, - ))) + return u"".join( + draw( + lists( + characters(max_codepoint=255), + min_size=min_size, + max_size=max_size, + ) + ) + ) @composite @@ -126,9 +137,9 @@ 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 draw( + text(min_size=min_size, max_size=max_size, alphabet=idna_characters()) + ) @composite @@ -172,10 +183,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 = 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 @@ -203,8 +217,9 @@ def hostnames( 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) + 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) @@ -228,6 +243,7 @@ def path_characters(): global _path_characters if _path_characters is None: + def chars(): # type: () -> Iterable[Text] for i in range(maxunicode): @@ -249,6 +265,7 @@ def chars(): return _path_characters + _path_characters = None # type: Optional[str] @@ -277,7 +294,9 @@ def encoded_urls(draw): # pragma: no cover args = dict( scheme=draw(sampled_from((u"http", u"https"))), - host=host, port=port, path=path, + host=host, + port=port, + path=path, ) return EncodedURL(**args) diff --git a/src/klein/test/_trial.py b/src/klein/test/_trial.py index 68ea6184..612e25ec 100644 --- a/src/klein/test/_trial.py +++ b/src/klein/test/_trial.py @@ -18,13 +18,13 @@ __all__ = () - 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 """ @@ -44,7 +44,6 @@ def assertRegex(self, text, regex, msg=None): # 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..b5c2cb29 100644 --- a/src/klein/test/py3_test_resource.py +++ b/src/klein/test/py3_test_resource.py @@ -7,14 +7,12 @@ 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) diff --git a/src/klein/test/test_app.py b/src/klein/test/test_app.py index b18ba287..ee407f86 100644 --- a/src/klein/test/test_app.py +++ b/src/klein/test/test_app.py @@ -18,7 +18,6 @@ from .._interfaces import IKleinRequest - class DummyRequest(object): def __init__(self, n): self.n = n @@ -27,7 +26,7 @@ def __eq__(self, other): return other.n == self.n def __repr__(self): - return ''.format(n=self.n) + return "".format(n=self.n) registerAdapter(KleinRequest, DummyRequest, IKleinRequest) @@ -37,6 +36,7 @@ class KleinEqualityTestCase(unittest.TestCase, EqualityTestsMixin): """ Tests for L{Klein}'s implementation of C{==} and C{!=}. """ + class _One(object): app = Klein() @@ -59,19 +59,17 @@ def anInstance(self): # equal to everything. return self._One().app - def anotherInstance(self): return self._another - class DuplicateHasher(object): """ Every L{DuplicateHasher} has the same hash value and compares equal to every other L{DuplicateHasher}. """ - __slots__ = ('_identifier',) + __slots__ = ("_identifier",) def __init__(self, identifier): self._identifier = identifier @@ -89,7 +87,6 @@ def __eq__(self, other): return True - class KleinTestCase(unittest.TestCase): def test_route(self): """ @@ -107,7 +104,6 @@ def foo(request): self.assertEqual(app.execute_endpoint("foo", DummyRequest(1)), "foo") - def test_mapByIdentity(self): """ Routes are routed to the proper object regardless of its C{__hash__} @@ -121,11 +117,12 @@ def test_mapByIdentity(self): d[a] = "test" self.assertEqual(d.get(b), "test") - self.assertEqual(a.myRouter.execute_endpoint("root", DummyRequest(1)), - "a") - self.assertEqual(b.myRouter.execute_endpoint("root", DummyRequest(1)), - "b") - + self.assertEqual( + a.myRouter.execute_endpoint("root", DummyRequest(1)), "a" + ) + self.assertEqual( + b.myRouter.execute_endpoint("root", DummyRequest(1)), "b" + ) def test_preserveIdentityWhenPossible(self): """ @@ -137,7 +134,8 @@ def test_preserveIdentityWhenPossible(self): """ # This is the desirable property. class DuplicateHasherWithWritableAttribute(DuplicateHasher): - __slots__ = ('__klein_bound_myRouter__',) + __slots__ = ("__klein_bound_myRouter__",) + a = DuplicateHasherWithWritableAttribute("a") self.assertIs(a.myRouter, a.myRouter) @@ -151,7 +149,6 @@ class DuplicateHasherWithWritableAttribute(DuplicateHasher): # just not identical. self.assertIsNot(b.myRouter, b.myRouter) - def test_kleinNotFoundOnClass(self): """ When the Klein object can't find itself on the class it still preserves @@ -177,7 +174,6 @@ class TwoRouters(object): self.assertIs(tr.app1, tr.app1) self.assertIs(tr.app2, tr.app2) - def test_bindInstanceIgnoresBlankProperties(self): """ L{Klein.__get__} doesn't propagate L{AttributeError} when @@ -196,7 +192,6 @@ class Oddment(object): self.assertIsInstance(Oddment().app, Klein) - def test_submountedRoute(self): """ L{Klein.subroute} adds functions as routable endpoints. @@ -204,18 +199,17 @@ def test_submountedRoute(self): app = Klein() with app.subroute("/sub") as app: + @app.route("/prefixed_uri") def foo_endpoint(request): return b"foo" c = app.url_map.bind("sub/prefixed_uri") + self.assertEqual(c.match("/sub/prefixed_uri"), ("foo_endpoint", {})) + self.assertEqual(len(app.endpoints), 1) self.assertEqual( - c.match("/sub/prefixed_uri"), ("foo_endpoint", {})) - self.assertEqual( - len(app.endpoints), 1) - self.assertEqual( - app.execute_endpoint("foo_endpoint", DummyRequest(1)), b"foo") - + app.execute_endpoint("foo_endpoint", DummyRequest(1)), b"foo" + ) def test_stackedRoute(self): """ @@ -238,10 +232,7 @@ def foobar(request): ) self.assertEqual(c.match("/bar"), ("bar", {})) - self.assertEqual( - app.execute_endpoint("bar", DummyRequest(2)), "foobar" - ) - + self.assertEqual(app.execute_endpoint("bar", DummyRequest(2)), "foobar") def test_branchRoute(self): """ @@ -257,22 +248,24 @@ def branchfunc(request): c = app.url_map.bind("foo") self.assertEqual(c.match("/foo/"), ("branchfunc", {})) self.assertEqual( - c.match("/foo/bar"), - ("branchfunc_branch", {'__rest__': 'bar'})) + c.match("/foo/bar"), ("branchfunc_branch", {"__rest__": "bar"}) + ) - self.assertEquals(app.endpoints["branchfunc"].__name__, - "route '/foo/' executor for branchfunc") + self.assertEquals( + app.endpoints["branchfunc"].__name__, + "route '/foo/' executor for branchfunc", + ) self.assertEquals( app.endpoints["branchfunc_branch"].__name__, - "branch route '/foo/' executor for branchfunc" + "branch route '/foo/' executor for branchfunc", ) self.assertEquals( - app.execute_endpoint("branchfunc_branch", - DummyRequest("looking for foo")), - "foo" + app.execute_endpoint( + "branchfunc_branch", DummyRequest("looking for foo") + ), + "foo", ) - def test_bindable(self): """ L{bindable} is a decorator which allows a function decorated by @route @@ -300,12 +293,12 @@ class BoundTo(object): self.assertEquals(calls, [(None, req), (b, req)]) self.assertEqual(originalName(method), "method") - def test_modified(self): """ L{modified} is a decorator which alters the thing that it decorates, and describes itself as such. """ + def annotate(decoratee): decoratee.supersized = True return decoratee @@ -322,7 +315,6 @@ def megaAdd(a, b): self.assertIn("supersizer for add", str(megaAdd)) self.assertEqual(megaAdd(3, 4), 43000) - def test_classicalRoute(self): """ L{Klein.route} may be used a method decorator when a L{Klein} instance @@ -347,13 +339,13 @@ def bar(self, request): ) self.assertEqual(bar_calls, [(foo, DummyRequest(1))]) - def test_classicalRouteWithTwoInstances(self): """ Multiple instances of a class with a L{Klein} attribute and L{Klein.route}'d methods can be created and their L{Klein}s used independently. """ + class Foo(object): app = Klein() @@ -373,19 +365,19 @@ def bar(self, request): dr1 = DummyRequest(1) dr2 = DummyRequest(2) - foo_1_app.execute_endpoint('bar', dr1) - foo_2_app.execute_endpoint('bar', dr2) + foo_1_app.execute_endpoint("bar", dr1) + foo_2_app.execute_endpoint("bar", dr2) self.assertEqual(foo_1.bar_calls, [(foo_1, dr1)]) self.assertEqual(foo_2.bar_calls, [(foo_2, dr2)]) - def test_classicalRouteWithBranch(self): """ Multiple instances of a class with a L{Klein} attribute and L{Klein.route}'d methods can be created and their L{Klein}s used independently. """ + class Foo(object): app = Klein() @@ -405,13 +397,12 @@ def bar(self, request): dr1 = DummyRequest(1) dr2 = DummyRequest(2) - foo_1_app.execute_endpoint('bar_branch', dr1) - foo_2_app.execute_endpoint('bar_branch', dr2) + foo_1_app.execute_endpoint("bar_branch", dr1) + foo_2_app.execute_endpoint("bar_branch", dr2) self.assertEqual(foo_1.bar_calls, [(foo_1, dr1)]) self.assertEqual(foo_2.bar_calls, [(foo_2, dr2)]) - def test_branchDoesntRequireTrailingSlash(self): """ L{Klein.route} should create a branch path which consumes all children, @@ -425,14 +416,14 @@ def foo(request): return "foo" c = app.url_map.bind("foo") - self.assertEqual(c.match("/foo/bar"), - ("foo_branch", {"__rest__": "bar"})) - + self.assertEqual( + c.match("/foo/bar"), ("foo_branch", {"__rest__": "bar"}) + ) - @patch('klein._app.KleinResource') - @patch('klein._app.Site') - @patch('klein._app.log') - @patch('klein._app.reactor') + @patch("klein._app.KleinResource") + @patch("klein._app.Site") + @patch("klein._app.log") + @patch("klein._app.reactor") def test_run(self, reactor, mock_log, mock_site, mock_kr): """ L{Klein.run} configures a L{KleinResource} and a L{Site} @@ -444,7 +435,8 @@ def test_run(self, reactor, mock_log, mock_site, mock_kr): app.run("localhost", 8080) reactor.listenTCP.assert_called_with( - 8080, mock_site.return_value, backlog=50, interface="localhost") + 8080, mock_site.return_value, backlog=50, interface="localhost" + ) reactor.run.assert_called_with() @@ -452,11 +444,10 @@ def test_run(self, reactor, mock_log, mock_site, mock_kr): mock_kr.assert_called_with(app) mock_log.startLogging.assert_called_with(sys.stdout) - - @patch('klein._app.KleinResource') - @patch('klein._app.Site') - @patch('klein._app.log') - @patch('klein._app.reactor') + @patch("klein._app.KleinResource") + @patch("klein._app.Site") + @patch("klein._app.log") + @patch("klein._app.reactor") def test_runWithLogFile(self, reactor, mock_log, mock_site, mock_kr): """ L{Klein.run} logs to the specified C{logFile}. @@ -467,7 +458,8 @@ def test_runWithLogFile(self, reactor, mock_log, mock_site, mock_kr): app.run("localhost", 8080, logFile=logFile) reactor.listenTCP.assert_called_with( - 8080, mock_site.return_value, backlog=50, interface="localhost") + 8080, mock_site.return_value, backlog=50, interface="localhost" + ) reactor.run.assert_called_with() @@ -475,11 +467,10 @@ def test_runWithLogFile(self, reactor, mock_log, mock_site, mock_kr): mock_kr.assert_called_with(app) mock_log.startLogging.assert_called_with(logFile) - - @patch('klein._app.KleinResource') - @patch('klein._app.log') - @patch('klein._app.endpoints.serverFromString') - @patch('klein._app.reactor') + @patch("klein._app.KleinResource") + @patch("klein._app.log") + @patch("klein._app.endpoints.serverFromString") + @patch("klein._app.reactor") def test_runTCP6(self, reactor, mock_sfs, mock_log, mock_kr): """ L{Klein.run} called with tcp6 endpoint description. @@ -493,11 +484,10 @@ def test_runTCP6(self, reactor, mock_sfs, mock_log, mock_kr): mock_log.startLogging.assert_called_with(sys.stdout) mock_kr.assert_called_with(app) - - @patch('klein._app.KleinResource') - @patch('klein._app.log') - @patch('klein._app.endpoints.serverFromString') - @patch('klein._app.reactor') + @patch("klein._app.KleinResource") + @patch("klein._app.log") + @patch("klein._app.endpoints.serverFromString") + @patch("klein._app.reactor") def test_runSSL(self, reactor, mock_sfs, mock_log, mock_kr): """ L{Klein.run} called with SSL endpoint specification. @@ -514,8 +504,7 @@ def test_runSSL(self, reactor, mock_sfs, mock_log, mock_kr): mock_log.startLogging.assert_called_with(sys.stdout) mock_kr.assert_called_with(app) - - @patch('klein._app.KleinResource') + @patch("klein._app.KleinResource") def test_resource(self, mock_kr): """ L{Klien.resource} returns a L{KleinResource}. @@ -526,59 +515,69 @@ def test_resource(self, mock_kr): mock_kr.assert_called_with(app) self.assertEqual(mock_kr.return_value, resource) - def test_urlFor(self): """L{Klein.urlFor} builds an URL for an endpoint with parameters""" app = Klein() - @app.route('/user/') + @app.route("/user/") def userpage(req, name): return name - @app.route('/post/', endpoint='bar') + @app.route("/post/", endpoint="bar") def foo(req, postid): return str(postid) - request = requestMock(b'/user/john') + request = requestMock(b"/user/john") self.assertEqual( - app.execute_endpoint('userpage', request, 'john'), - 'john' + app.execute_endpoint("userpage", request, "john"), "john" ) self.assertEqual( - app.execute_endpoint('bar', requestMock(b'/post/123'), 123), - '123' + app.execute_endpoint("bar", requestMock(b"/post/123"), 123), "123" ) - request = requestMock(b'/addr') - self.assertEqual(app.urlFor(request, 'userpage', {'name': 'john'}), - '/user/john') + request = requestMock(b"/addr") + self.assertEqual( + app.urlFor(request, "userpage", {"name": "john"}), "/user/john" + ) - request = requestMock(b'/addr') - self.assertEqual(app.urlFor(request, 'userpage', {'name': 'john'}, - force_external=True), - 'http://localhost:8080/user/john') + request = requestMock(b"/addr") + self.assertEqual( + app.urlFor( + request, "userpage", {"name": "john"}, force_external=True + ), + "http://localhost:8080/user/john", + ) - request = requestMock(b'/addr', host=b'example.com', port=4321) - self.assertEqual(app.urlFor(request, 'userpage', {'name': 'john'}, - force_external=True), - 'http://example.com:4321/user/john') + request = requestMock(b"/addr", host=b"example.com", port=4321) + self.assertEqual( + app.urlFor( + request, "userpage", {"name": "john"}, force_external=True + ), + "http://example.com:4321/user/john", + ) - request = requestMock(b'/addr') - url = app.urlFor(request, 'userpage', {'name': 'john', 'age': 29}, - append_unknown=True) - self.assertEqual(url, '/user/john?age=29') + request = requestMock(b"/addr") + url = app.urlFor( + request, + "userpage", + {"name": "john", "age": 29}, + append_unknown=True, + ) + self.assertEqual(url, "/user/john?age=29") - request = requestMock(b'/addr') - self.assertEqual(app.urlFor(request, 'bar', {'postid': 123}), - '/post/123') + request = requestMock(b"/addr") + self.assertEqual( + app.urlFor(request, "bar", {"postid": 123}), "/post/123" + ) - request = requestMock(b'/addr') - request.requestHeaders.removeHeader(b'host') - self.assertEqual(app.urlFor(request, 'bar', {'postid': 123}), - '/post/123') + request = requestMock(b"/addr") + request.requestHeaders.removeHeader(b"host") + self.assertEqual( + app.urlFor(request, "bar", {"postid": 123}), "/post/123" + ) - request = requestMock(b'/addr') - request.requestHeaders.removeHeader(b'host') + request = requestMock(b"/addr") + request.requestHeaders.removeHeader(b"host") with self.assertRaises(ValueError): - app.urlFor(request, 'bar', {'postid': 123}, force_external=True) + app.urlFor(request, "bar", {"postid": 123}, force_external=True) diff --git a/src/klein/test/test_exports.py b/src/klein/test/test_exports.py index 4e1dc8b2..5f72cea0 100644 --- a/src/klein/test/test_exports.py +++ b/src/klein/test/test_exports.py @@ -5,7 +5,6 @@ from twisted.trial import unittest - class PublicSymbolsTestCase(unittest.TestCase): """ Tests for public API modules. @@ -27,7 +26,6 @@ def test_klein(self): self.assertIdentical(k.Plating, p.Plating) - def test_klein_resource(self): # type: () -> None """ @@ -38,7 +36,6 @@ def test_klein_resource(self): self.assertIdentical(k.resource()._app, a.resource()._app) - def test_app(self): # type: () -> None """ @@ -53,7 +50,6 @@ def test_app(self): self.assertIdentical(a.route, _a.route) self.assertIdentical(a.run, _a.run) - def test_interfaces(self): # type: () -> None """ @@ -64,7 +60,6 @@ def test_interfaces(self): self.assertIdentical(i.IKleinRequest, _i.IKleinRequest) - def test_resource(self): # type: () -> None """ diff --git a/src/klein/test/test_form.py b/src/klein/test/test_form.py index 23a35e14..bf5484aa 100644 --- a/src/klein/test/test_form.py +++ b/src/klein/test/test_form.py @@ -1,4 +1,3 @@ - from typing import List, TYPE_CHECKING, Text, cast from xml.etree import ElementTree @@ -14,8 +13,11 @@ from klein import Field, Form, Klein, Requirer, SessionProcurer from klein.interfaces import ( - ISession, ISessionStore, NoSuchSession, SessionMechanism, - ValidationError + ISession, + ISessionStore, + NoSuchSession, + SessionMechanism, + ValidationError, ) from klein.storage.memory import MemorySessionStore @@ -25,7 +27,6 @@ from klein import RenderableForm - class DanglingField(Field): """ A dangling field that, for some reason, doesn't remember its own name when @@ -50,9 +51,11 @@ class TestObject(object): def procureASession(self, request): # type: (IRequest) -> Any try: - yield (SessionProcurer(self.sessionStore, - secureTokenHeader=b'X-Test-Session') - .procureSession(request)) + yield ( + SessionProcurer( + self.sessionStore, secureTokenHeader=b"X-Test-Session" + ).procureSession(request) + ) except NoSuchSession: # Intentionally slightly buggy - if a session can't be procured, # simply leave it out and rely on checkCSRF to ensure the session @@ -68,17 +71,19 @@ def danglingParameter(self, dangling): "..." @requirer.require( - router.route("/handle", methods=['POST']), - name=Field.text(), value=Field.number(), + router.route("/handle", methods=["POST"]), + name=Field.text(), + value=Field.number(), ) def handler(self, name, value): # type: (Text, float) -> bytes self.calls.append((name, value)) - return b'yay' + return b"yay" @requirer.require( - router.route("/handle-submit", methods=['POST']), - name=Field.text(), button=Field.submit(u"OK") + router.route("/handle-submit", methods=["POST"]), + name=Field.text(), + button=Field.submit(u"OK"), ) def handlerWithSubmit(self, name, button): # type: (str, str) -> None @@ -87,52 +92,51 @@ def handlerWithSubmit(self, name, button): """ @requirer.require( - router.route("/password-field", methods=["POST"]), - pw=Field.password() + router.route("/password-field", methods=["POST"]), pw=Field.password() ) def gotPassword(self, pw): # type: (Text) -> bytes self.calls.append(("password", pw)) - return b'password received' + return b"password received" @requirer.require( - router.route("/notrequired", methods=['POST']), - name=Field.text(), value=Field.number(required=False, default=7.0) + router.route("/notrequired", methods=["POST"]), + name=Field.text(), + value=Field.number(required=False, default=7.0), ) def notRequired(self, name, value): # type: (IRequest, Text, float) -> bytes self.calls.append((name, value)) - return b'okay' + return b"okay" @requirer.require( - router.route("/constrained", methods=['POST']), - goldilocks=Field.number(minimum=3, maximum=9) + router.route("/constrained", methods=["POST"]), + goldilocks=Field.number(minimum=3, maximum=9), ) def constrained(self, goldilocks): # type: (int) -> bytes - self.calls.append(('constrained', goldilocks)) - return b'got it' + self.calls.append(("constrained", goldilocks)) + return b"got it" @requirer.require( - router.route("/render", methods=['GET']), - form=Form.rendererFor(handler, action=u'/handle') + router.route("/render", methods=["GET"]), + form=Form.rendererFor(handler, action=u"/handle"), ) def renderer(self, form): # type: (IRequest, Form) -> Form return form @requirer.require( - router.route("/render-submit", methods=['GET']), - form=Form.rendererFor(handlerWithSubmit, action=u'/handle-submit') + router.route("/render-submit", methods=["GET"]), + form=Form.rendererFor(handlerWithSubmit, action=u"/handle-submit"), ) def submitRenderer(self, form): # type: (IRequest, RenderableForm) -> RenderableForm return form - @requirer.require( router.route("/render-custom", methods=["GET"]), - form=Form.rendererFor(handler, action=u"/handle") + form=Form.rendererFor(handler, action=u"/handle"), ) def customFormRender(self, form): # type: (RenderableForm) -> Any @@ -142,31 +146,27 @@ def customFormRender(self, form): """ return Element(loader=TagLoader(tags.html(tags.body(form.glue())))) - @requirer.require( router.route("/render-cascade", methods=["GET"]), - form=Form.rendererFor(handler, action=u"/handle") + form=Form.rendererFor(handler, action=u"/handle"), ) def cascadeRenderer(self, form): # type: (RenderableForm) -> RenderableForm class CustomElement(Element): - @renderer def customize(self, request, tag): # type: (IRequest, Any) -> Any return tag("customized") - form.validationErrors[form._form.fields[0]] = ValidationError(message=( - tags.div(class_="checkme", render="customize") - )) - - return CustomElement( - loader=TagLoader(form) + form.validationErrors[form._form.fields[0]] = ValidationError( + message=(tags.div(class_="checkme", render="customize")) ) + return CustomElement(loader=TagLoader(form)) + @requirer.require( - router.route("/handle-validation", methods=['POST']), + router.route("/handle-validation", methods=["POST"]), value=Field.number(maximum=10), ) def customValidation(self, value): @@ -175,31 +175,25 @@ def customValidation(self, value): never called. """ - @requirer.require( - Form.onValidationFailureFor(customValidation) - ) + @requirer.require(Form.onValidationFailureFor(customValidation)) def customFailureHandling(self, values): # type: (RenderableForm) -> bytes """ Handle validation failure. """ - self.calls.append(('validation', values)) - return b'~special~' + self.calls.append(("validation", values)) + return b"~special~" - - @requirer.require( - router.route("/handle-empty", methods=['POST']), - ) + @requirer.require(router.route("/handle-empty", methods=["POST"]),) def emptyHandler(self): # type: () -> bytes """ Empty form handler; just for testing rendering. """ - @requirer.require( - router.route("/render-empty", methods=['GET']), - form=Form.rendererFor(emptyHandler, action=u'/handle-empty') + router.route("/render-empty", methods=["GET"]), + form=Form.rendererFor(emptyHandler, action=u"/handle-empty"), ) def emptyRenderer(self, form): # type: (RenderableForm) -> RenderableForm @@ -216,19 +210,19 @@ def simpleFormRouter(): calls = [] @requirer.require( - router.route("/getme", methods=['GET']), - name=Field.text(), value=Field.number(), - custom=Field(formInputType='number', converter=int, required=False), + router.route("/getme", methods=["GET"]), + name=Field.text(), + value=Field.number(), + custom=Field(formInputType="number", converter=int, required=False), ) def justGet(name, value, custom): # type: (str, int, int) -> bytes calls.append((name, value or custom)) - return b'got' + return b"got" return router, calls - class TestForms(SynchronousTestCase): """ Tests for L{klein.Form} and associated tools. @@ -248,15 +242,16 @@ def test_handling(self): to = TestObject(mem) stub = StubTreq(to.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/handle', - data=dict(name='hello', value='1234', ignoreme='extraneous'), - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.post( + "https://localhost/handle", + data=dict(name="hello", value="1234", ignoreme="extraneous"), + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 200) - self.assertEqual(self.successResultOf(content(response)), b'yay') - self.assertEqual(to.calls, [(u'hello', 1234)]) - + self.assertEqual(self.successResultOf(content(response)), b"yay") + self.assertEqual(to.calls, [(u"hello", 1234)]) def test_handlingPassword(self): # type: () -> None @@ -272,16 +267,18 @@ def test_handlingPassword(self): to = TestObject(mem) stub = StubTreq(to.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/password-field', - data=dict(pw='asdfjkl;'), - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.post( + "https://localhost/password-field", + data=dict(pw="asdfjkl;"), + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 200) - self.assertEqual(self.successResultOf(content(response)), - b'password received') - self.assertEqual(to.calls, [(u'password', u'asdfjkl;')]) - + self.assertEqual( + self.successResultOf(content(response)), b"password received" + ) + self.assertEqual(to.calls, [(u"password", u"asdfjkl;")]) def test_numberConstraints(self): # type: () -> None @@ -297,28 +294,33 @@ def test_numberConstraints(self): to = TestObject(mem) stub = StubTreq(to.router.resource()) - tooLow = self.successResultOf(stub.post( - 'https://localhost/constrained', - data=dict(goldilocks='1'), - headers={b'X-Test-Session': session.identifier} - )) - tooHigh = self.successResultOf(stub.post( - 'https://localhost/constrained', - data=dict(goldilocks='20'), - headers={b'X-Test-Session': session.identifier} - )) - justRight = self.successResultOf(stub.post( - 'https://localhost/constrained', - data=dict(goldilocks='7'), - headers={b'X-Test-Session': session.identifier} - )) + tooLow = self.successResultOf( + stub.post( + "https://localhost/constrained", + data=dict(goldilocks="1"), + headers={b"X-Test-Session": session.identifier}, + ) + ) + tooHigh = self.successResultOf( + stub.post( + "https://localhost/constrained", + data=dict(goldilocks="20"), + headers={b"X-Test-Session": session.identifier}, + ) + ) + justRight = self.successResultOf( + stub.post( + "https://localhost/constrained", + data=dict(goldilocks="7"), + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(tooHigh.code, 400) self.assertEqual(tooLow.code, 400) self.assertEqual(justRight.code, 200) - self.assertEqual(self.successResultOf(content(justRight)), b'got it') - self.assertEqual(to.calls, [(u'constrained', 7)]) - + self.assertEqual(self.successResultOf(content(justRight)), b"got it") + self.assertEqual(to.calls, [(u"constrained", 7)]) def test_missingRequiredParameter(self): # type: () -> None @@ -334,19 +336,20 @@ def test_missingRequiredParameter(self): to = TestObject(mem) stub = StubTreq(to.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/handle', - data=dict(), - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.post( + "https://localhost/handle", + data=dict(), + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 400) self.assertIn( b"a value was required but none was supplied", - self.successResultOf(content(response)) + self.successResultOf(content(response)), ) self.assertEqual(to.calls, []) - def test_noName(self): # type: () -> None """ @@ -359,17 +362,19 @@ def test_noName(self): ) to = TestObject(mem) stub = StubTreq(to.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/dangling-param', - data=dict(), - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.post( + "https://localhost/dangling-param", + data=dict(), + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 500) errors = self.flushLoggedErrors(ValueError) self.assertEqual(len(errors), 1) - self.assertIn(str(errors[0].value), - "Cannot extract unnamed form field.") - + self.assertIn( + str(errors[0].value), "Cannot extract unnamed form field." + ) def test_handlingGET(self): # type: () -> None @@ -381,13 +386,14 @@ def test_handlingGET(self): stub = StubTreq(router.resource()) - response = self.successResultOf(stub.get( - b"https://localhost/getme?name=hello,%20big+world&value=4321" - )) + response = self.successResultOf( + stub.get( + b"https://localhost/getme?name=hello,%20big+world&value=4321" + ) + ) self.assertEqual(response.code, 200) - self.assertEqual(self.successResultOf(content(response)), b'got') - self.assertEqual(calls, [(u'hello, big world', 4321)]) - + self.assertEqual(self.successResultOf(content(response)), b"got") + self.assertEqual(calls, [(u"hello, big world", 4321)]) def test_validatingParameters(self): # type: () -> None @@ -400,21 +406,23 @@ def test_validatingParameters(self): stub = StubTreq(router.resource()) - response = self.successResultOf(stub.get( - b"https://localhost/getme?" - b"name=hello,%20big+world&value=not+a+number" - )) + response = self.successResultOf( + stub.get( + b"https://localhost/getme?" + b"name=hello,%20big+world&value=not+a+number" + ) + ) responseForm = self.successResultOf(content(response)) self.assertEqual(response.code, 400) self.assertEqual(calls, []) responseForm = self.successResultOf(content(response)) responseDom = ElementTree.fromstring(responseForm) errors = responseDom.findall( - ".//*[@class='klein-form-validation-error']") + ".//*[@class='klein-form-validation-error']" + ) self.assertEqual(len(errors), 1) self.assertEquals(errors[0].text, "not a valid number") - def test_customParameterValidation(self): # type: () -> None """ @@ -427,32 +435,29 @@ def test_customParameterValidation(self): stub = StubTreq(router.resource()) - response = self.successResultOf(stub.get( - b"https://localhost/getme?" - b"name=hello,%20big+world&value=0&custom=not+a+number" - )) + response = self.successResultOf( + stub.get( + b"https://localhost/getme?" + b"name=hello,%20big+world&value=0&custom=not+a+number" + ) + ) responseForm = self.successResultOf(content(response)) self.assertEqual(response.code, 400) self.assertEqual(calls, []) responseForm = self.successResultOf(content(response)) responseDom = ElementTree.fromstring(responseForm) errors = responseDom.findall( - ".//*[@class='klein-form-validation-error']") + ".//*[@class='klein-form-validation-error']" + ) self.assertEqual(len(errors), 1) errorText = cast(str, errors[0].text) self.assertIsNot(errorText, None) self.assertTrue( - errorText.startswith( - "invalid literal for int() with base 10: " - ) + errorText.startswith("invalid literal for int() with base 10: ") ) # Peculiar 2-step assert because pypy2 (invalidly) sticks a 'u' in # there. - self.assertTrue( - errorText.endswith( - "'not a number'" - ) - ) + self.assertTrue(errorText.endswith("'not a number'")) def test_handlingJSON(self): # type: () -> None @@ -468,15 +473,16 @@ def test_handlingJSON(self): to = TestObject(mem) stub = StubTreq(to.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/handle', - json=dict(name='hello', value='1234', ignoreme='extraneous'), - headers={u'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.post( + "https://localhost/handle", + json=dict(name="hello", value="1234", ignoreme="extraneous"), + headers={u"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 200) - self.assertEqual(self.successResultOf(content(response)), b'yay') - self.assertEqual(to.calls, [(u'hello', 1234)]) - + self.assertEqual(self.successResultOf(content(response)), b"yay") + self.assertEqual(to.calls, [(u"hello", 1234)]) def test_missingOptionalParameterJSON(self): # type: () -> None @@ -492,22 +498,25 @@ def test_missingOptionalParameterJSON(self): to = TestObject(mem) stub = StubTreq(to.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/notrequired', - json=dict(name='one'), - headers={b'X-Test-Session': session.identifier} - )) - response2 = self.successResultOf(stub.post( - 'https://localhost/notrequired', - json=dict(name='two', value=2), - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.post( + "https://localhost/notrequired", + json=dict(name="one"), + headers={b"X-Test-Session": session.identifier}, + ) + ) + response2 = self.successResultOf( + stub.post( + "https://localhost/notrequired", + json=dict(name="two", value=2), + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 200) self.assertEqual(response2.code, 200) - self.assertEqual(self.successResultOf(content(response)), b'okay') - self.assertEqual(self.successResultOf(content(response2)), b'okay') - self.assertEqual(to.calls, [(u'one', 7.0), (u'two', 2.0)]) - + self.assertEqual(self.successResultOf(content(response)), b"okay") + self.assertEqual(self.successResultOf(content(response2)), b"okay") + self.assertEqual(to.calls, [(u"one", 7.0), (u"two", 2.0)]) def test_rendering(self): # type: () -> None @@ -521,21 +530,24 @@ def test_rendering(self): ) stub = StubTreq(TestObject(mem).router.resource()) - response = self.successResultOf(stub.get( - 'https://localhost/render', - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.get( + "https://localhost/render", + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 200) - self.assertIn(response.headers.getRawHeaders(b"content-type")[0], - b"text/html") + self.assertIn( + response.headers.getRawHeaders(b"content-type")[0], b"text/html" + ) responseDom = ElementTree.fromstring( self.successResultOf(content(response)) ) submitButton = responseDom.findall(".//*[@type='submit']") self.assertEqual(len(submitButton), 1) - self.assertEqual(submitButton[0].attrib['name'], - '__klein_auto_submit__') - + self.assertEqual( + submitButton[0].attrib["name"], "__klein_auto_submit__" + ) def test_renderingExplicitSubmit(self): # type: () -> None @@ -550,20 +562,22 @@ def test_renderingExplicitSubmit(self): ) stub = StubTreq(TestObject(mem).router.resource()) - response = self.successResultOf(stub.get( - 'https://localhost/render-submit', - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.get( + "https://localhost/render-submit", + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 200) - self.assertIn(response.headers.getRawHeaders(b"content-type")[0], - b"text/html") + self.assertIn( + response.headers.getRawHeaders(b"content-type")[0], b"text/html" + ) responseDom = ElementTree.fromstring( self.successResultOf(content(response)) ) submitButton = responseDom.findall(".//*[@type='submit']") self.assertEqual(len(submitButton), 1) - self.assertEqual(submitButton[0].attrib['name'], 'button') - + self.assertEqual(submitButton[0].attrib["name"], "button") def test_renderingFormGlue(self): # type: () -> None @@ -578,13 +592,16 @@ def test_renderingFormGlue(self): ) stub = StubTreq(TestObject(mem).router.resource()) - response = self.successResultOf(stub.get( - 'https://localhost/render-custom', - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.get( + "https://localhost/render-custom", + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 200) - self.assertIn(response.headers.getRawHeaders(b"content-type")[0], - b"text/html") + self.assertIn( + response.headers.getRawHeaders(b"content-type")[0], b"text/html" + ) responseDom = ElementTree.fromstring( self.successResultOf(content(response)) ) @@ -593,9 +610,7 @@ def test_renderingFormGlue(self): protectionField = responseDom.findall( ".//*[@name='__csrf_protection__']" ) - self.assertEqual(protectionField[0].attrib['value'], - session.identifier) - + self.assertEqual(protectionField[0].attrib["value"], session.identifier) def test_renderingEmptyForm(self): # type: () -> None @@ -610,26 +625,28 @@ def test_renderingEmptyForm(self): ) stub = StubTreq(TestObject(mem).router.resource()) - response = self.successResultOf(stub.get( - 'https://localhost/render-empty', - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.get( + "https://localhost/render-empty", + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 200) - self.assertIn(response.headers.getRawHeaders(b"content-type")[0], - b"text/html") + self.assertIn( + response.headers.getRawHeaders(b"content-type")[0], b"text/html" + ) responseDom = ElementTree.fromstring( self.successResultOf(content(response)) ) submitButton = responseDom.findall(".//*[@type='submit']") self.assertEqual(len(submitButton), 1) - self.assertEqual(submitButton[0].attrib['name'], - '__klein_auto_submit__') + self.assertEqual( + submitButton[0].attrib["name"], "__klein_auto_submit__" + ) protectionField = responseDom.findall( ".//*[@name='__csrf_protection__']" ) - self.assertEqual(protectionField[0].attrib['value'], - session.identifier) - + self.assertEqual(protectionField[0].attrib["value"], session.identifier) def test_renderLookupError(self): # type: () -> None @@ -644,17 +661,18 @@ def test_renderLookupError(self): ) stub = StubTreq(TestObject(mem).router.resource()) - response = self.successResultOf(stub.get( - 'https://localhost/render-cascade', - headers={b'X-Test-Session': session.identifier} - )) + response = self.successResultOf( + stub.get( + "https://localhost/render-cascade", + headers={b"X-Test-Session": session.identifier}, + ) + ) self.assertEqual(response.code, 200) # print(self.successResultOf(response.content()).decode('utf-8')) failures = self.flushLoggedErrors() self.assertEqual(len(failures), 1) self.assertIn("MissingRenderMethod", str(failures[0])) - def test_customValidationHandling(self): # type: () -> None """ @@ -669,23 +687,27 @@ def test_customValidationHandling(self): testobj = TestObject(mem) stub = StubTreq(testobj.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/handle-validation', - headers={b'X-Test-Session': session.identifier}, - json={"value": 300} - )) + response = self.successResultOf( + stub.post( + "https://localhost/handle-validation", + headers={b"X-Test-Session": session.identifier}, + json={"value": 300}, + ) + ) self.assertEqual(response.code, 200) - self.assertIn(response.headers.getRawHeaders(b"content-type")[0], - b"text/html") + self.assertIn( + response.headers.getRawHeaders(b"content-type")[0], b"text/html" + ) responseText = self.successResultOf(content(response)) self.assertEqual(responseText, b"~special~") self.assertEqual( - [(k.pythonArgumentName, v) for k, v - in testobj.calls[-1][1].prevalidationValues.items()], - [('value', 300)] + [ + (k.pythonArgumentName, v) + for k, v in testobj.calls[-1][1].prevalidationValues.items() + ], + [("value", 300)], ) - def test_renderingWithNoSessionYet(self): # type: () -> None """ @@ -694,16 +716,15 @@ def test_renderingWithNoSessionYet(self): """ mem = MemorySessionStore() stub = StubTreq(TestObject(mem).router.resource()) - response = self.successResultOf(stub.get('https://localhost/render')) + response = self.successResultOf(stub.get("https://localhost/render")) self.assertEqual(response.code, 200) - setCookie = response.cookies()['Klein-Secure-Session'] + 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") self.assertIn(expected, actual) - def test_noSessionPOST(self): # type: () -> None """ @@ -713,14 +734,15 @@ def test_noSessionPOST(self): mem = MemorySessionStore() to = TestObject(mem) stub = StubTreq(to.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/handle', - data=dict(name='hello', value='1234') - )) + response = self.successResultOf( + stub.post( + "https://localhost/handle", + data=dict(name="hello", value="1234"), + ) + ) self.assertEqual(to.calls, []) self.assertEqual(response.code, 403) - self.assertIn(b'CSRF', self.successResultOf(content(response))) - + self.assertIn(b"CSRF", self.successResultOf(content(response))) def test_cookieNoToken(self): # type: () -> None @@ -734,15 +756,18 @@ def test_cookieNoToken(self): ) to = TestObject(mem) stub = StubTreq(to.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/handle', - data=dict(name='hello', value='1234', ignoreme='extraneous'), - cookies={"Klein-Secure-Session": nativeString(session.identifier)} - )) + response = self.successResultOf( + stub.post( + "https://localhost/handle", + data=dict(name="hello", value="1234", ignoreme="extraneous"), + cookies={ + "Klein-Secure-Session": nativeString(session.identifier) + }, + ) + ) self.assertEqual(to.calls, []) self.assertEqual(response.code, 403) - self.assertIn(b'CSRF', self.successResultOf(content(response))) - + self.assertIn(b"CSRF", self.successResultOf(content(response))) def test_cookieWithToken(self): # type: () -> None @@ -756,12 +781,20 @@ def test_cookieWithToken(self): ) to = TestObject(mem) stub = StubTreq(to.router.resource()) - response = self.successResultOf(stub.post( - 'https://localhost/handle', - data=dict(name='hello', value='1234', ignoreme='extraneous', - __csrf_protection__=session.identifier), - cookies={"Klein-Secure-Session": nativeString(session.identifier)} - )) - self.assertEqual(to.calls, [('hello', 1234)]) + response = self.successResultOf( + stub.post( + "https://localhost/handle", + data=dict( + name="hello", + value="1234", + ignoreme="extraneous", + __csrf_protection__=session.identifier, + ), + cookies={ + "Klein-Secure-Session": nativeString(session.identifier) + }, + ) + ) + self.assertEqual(to.calls, [("hello", 1234)]) self.assertEqual(response.code, 200) - self.assertIn(b'yay', self.successResultOf(content(response))) + self.assertIn(b"yay", self.successResultOf(content(response))) diff --git a/src/klein/test/test_headers.py b/src/klein/test/test_headers.py index 78d5674d..33bf1d0d 100644 --- a/src/klein/test/test_headers.py +++ b/src/klein/test/test_headers.py @@ -15,19 +15,26 @@ from ._trial import TestCase from .._headers import ( FrozenHTTPHeaders, - HEADER_NAME_ENCODING, HEADER_VALUE_ENCODING, - IHTTPHeaders, IMutableHTTPHeaders, + HEADER_NAME_ENCODING, + HEADER_VALUE_ENCODING, + IHTTPHeaders, + IMutableHTTPHeaders, MutableHTTPHeaders, - RawHeaders, getFromRawHeaders, - headerNameAsBytes, headerNameAsText, headerValueAsBytes, headerValueAsText, - normalizeHeaderName, normalizeRawHeaders, normalizeRawHeadersFrozen, + RawHeaders, + getFromRawHeaders, + headerNameAsBytes, + headerNameAsText, + headerValueAsBytes, + headerValueAsText, + normalizeHeaderName, + normalizeRawHeaders, + normalizeRawHeadersFrozen, ) __all__ = () - def encodeName(name): # type: (Text) -> Optional[bytes] return name.encode(HEADER_NAME_ENCODING) @@ -54,11 +61,11 @@ def headerValueSanitize(value): Sanitize a header value by replacing linear whitespace with spaces. """ if isinstance(value, bytes): - lws = [b'\r\n', b'\r', b'\n'] - space = b' ' + lws = [b"\r\n", b"\r", b"\n"] + space = b" " else: - lws = ['\r\n', '\r', '\n'] - space = ' ' + lws = ["\r\n", "\r", "\n"] + space = " " for l in lws: value = value.replace(l, space) return value @@ -77,7 +84,6 @@ def test_headerNameAsBytesWithBytes(self, name): """ self.assertIdentical(headerNameAsBytes(name), name) - @given(latin1_text(min_size=1)) def test_headerNameAsBytesWithText(self, name): # type: (Text) -> None @@ -87,7 +93,6 @@ def test_headerNameAsBytesWithText(self, name): rawName = encodeName(name) self.assertEqual(headerNameAsBytes(name), rawName) - @given(binary()) def test_headerNameAsTextWithBytes(self, name): # type: (bytes) -> None @@ -96,7 +101,6 @@ def test_headerNameAsTextWithBytes(self, name): """ self.assertEqual(headerNameAsText(name), decodeName(name)) - @given(text(min_size=1)) def test_headerNameAsTextWithText(self, name): # type: (Text) -> None @@ -105,7 +109,6 @@ def test_headerNameAsTextWithText(self, name): """ self.assertIdentical(headerNameAsText(name), name) - @given(binary()) def test_headerValueAsBytesWithBytes(self, value): # type: (bytes) -> None @@ -114,7 +117,6 @@ def test_headerValueAsBytesWithBytes(self, value): """ self.assertIdentical(headerValueAsBytes(value), value) - @given(latin1_text(min_size=1)) def test_headerValueAsBytesWithText(self, value): # type: (Text) -> None @@ -124,7 +126,6 @@ def test_headerValueAsBytesWithText(self, value): rawValue = encodeValue(value) self.assertEqual(headerValueAsBytes(value), rawValue) - @given(binary()) def test_headerValueAsTextWithBytes(self, value): # type: (bytes) -> None @@ -133,7 +134,6 @@ def test_headerValueAsTextWithBytes(self, value): """ self.assertEqual(headerValueAsText(value), decodeValue(value)) - @given(text(min_size=1)) def test_headerValueAsTextWithText(self, value): # type: (Text) -> None @@ -143,7 +143,6 @@ def test_headerValueAsTextWithText(self, value): self.assertIdentical(headerValueAsText(value), value) - class HeaderNameNormalizationTests(TestCase): """ Tests for header name normalization. @@ -157,7 +156,6 @@ def test_normalizeLowerCase(self): self.assertEqual(normalizeHeaderName("FooBar"), "foobar") - class RawHeadersConversionTests(TestCase): """ Tests for L{normalizeRawHeaders}. @@ -179,7 +177,6 @@ def test_pairWrongLength(self): ) self.assertEqual(str(e), "header pair must be a 2-item iterable") - @given(latin1_text()) def test_pairNameText(self, name): # type: (Text) -> None @@ -195,7 +192,6 @@ def test_pairNameText(self, name): ((normalizeHeaderName(headerNameAsBytes(name)), b"value"),), ) - @given(latin1_text()) def test_pairValueText(self, value): # type: (Text) -> None @@ -209,7 +205,6 @@ def test_pairValueText(self, value): self.assertEqual(normalized, ((b"name", headerValueAsBytes(value)),)) - class GetValuesTestsMixIn(object): """ Tests for utilities that access data from the C{RawHeaders} internal @@ -230,16 +225,13 @@ def getValues(self, rawHeaders, name): "{} must implement getValues()".format(self.__class__) ) - def test_getBytesName(self): # type: () -> None """ C{getValues} returns an iterable of L{bytes} values for the given L{bytes} header name. """ - rawHeaders = ( - (b"a", b"1"), (b"b", b"2"), (b"c", b"3"), (b"B", b"TWO") - ) + rawHeaders = ((b"a", b"1"), (b"b", b"2"), (b"c", b"3"), (b"B", b"TWO")) normalized = defaultdict(list) # type: Dict[bytes, List[bytes]] for name, value in rawHeaders: @@ -247,11 +239,11 @@ def test_getBytesName(self): for name, values in normalized.items(): cast(TestCase, self).assertEqual( - list(self.getValues(rawHeaders, name)), values, - "header name: {!r}".format(name) + list(self.getValues(rawHeaders, name)), + values, + "header name: {!r}".format(name), ) - def headerNormalize(self, value): # type: (Text) -> Text """ @@ -260,7 +252,6 @@ def headerNormalize(self, value): """ return value - @given(iterables(tuples(ascii_text(min_size=1), latin1_text()))) def test_getTextName(self, textPairs): # type: (Iterable[Tuple[Text, Text]]) -> None @@ -271,8 +262,9 @@ def test_getTextName(self, textPairs): This test only inserts Latin1 text into the header values, which is valid data. """ - textHeaders = tuple((name, headerValueSanitize(value)) - for name, value in textPairs) + textHeaders = tuple( + (name, headerValueSanitize(value)) for name, value in textPairs + ) textValues = defaultdict(list) # type: Dict[Text, List[Text]] for name, value in textHeaders: @@ -287,10 +279,9 @@ def test_getTextName(self, textPairs): cast(TestCase, self).assertEqual( list(self.getValues(rawHeaders, name)), [self.headerNormalize(value) for value in _values], - "header name: {!r}".format(name) + "header name: {!r}".format(name), ) - @given(iterables(tuples(ascii_text(min_size=1), binary()))) def test_getTextNameBinaryValues(self, pairs): # type: (Iterable[Tuple[Text, bytes]]) -> None @@ -318,12 +309,13 @@ def test_getTextNameBinaryValues(self, pairs): for textName, values in binaryValues.items(): cast(TestCase, self).assertEqual( tuple(self.getValues(rawHeaders, textName)), - tuple(self.headerNormalize(headerValueAsText(value)) - for value in values), - "header name: {!r}".format(textName) + tuple( + self.headerNormalize(headerValueAsText(value)) + for value in values + ), + "header name: {!r}".format(textName), ) - def test_getInvalidNameType(self): # type: () -> None """ @@ -338,7 +330,6 @@ def test_getInvalidNameType(self): ) - class RawHeadersReadTests(GetValuesTestsMixIn, TestCase): """ Tests for utilities that access data from the "headers tartare" internal @@ -350,7 +341,6 @@ def getValues(self, rawHeaders, name): return getFromRawHeaders(normalizeRawHeadersFrozen(rawHeaders), name) - class FrozenHTTPHeadersTests(GetValuesTestsMixIn, TestCase): """ Tests for L{FrozenHTTPHeaders}. @@ -361,7 +351,6 @@ def getValues(self, rawHeaders, name): headers = FrozenHTTPHeaders(rawHeaders=rawHeaders) return headers.getValues(name=name) - def test_interface(self): # type: () -> None """ @@ -370,7 +359,6 @@ def test_interface(self): headers = FrozenHTTPHeaders(rawHeaders=()) self.assertProvides(IHTTPHeaders, headers) # type: ignore[misc] - def test_defaultHeaders(self): # type: () -> None """ @@ -380,7 +368,6 @@ def test_defaultHeaders(self): self.assertEqual(headers.rawHeaders, ()) - class MutableHTTPHeadersTestsMixIn(GetValuesTestsMixIn): """ Tests for L{IMutableHTTPHeaders} implementations. @@ -390,20 +377,17 @@ def assertRawHeadersEqual(self, rawHeaders1, rawHeaders2): # type: (RawHeaders, RawHeaders) -> None cast(TestCase, self).assertEqual(rawHeaders1, rawHeaders2) - def headers(self, rawHeaders): # type: (RawHeaders) -> IMutableHTTPHeaders raise NotImplementedError( "{} must implement headers()".format(self.__class__) ) - def getValues(self, rawHeaders, name): # type: (RawHeaders, AnyStr) -> Iterable[AnyStr] headers = self.headers(rawHeaders=rawHeaders) return headers.getValues(name=name) - def test_interface(self): # type: () -> None """ @@ -414,7 +398,6 @@ def test_interface(self): IMutableHTTPHeaders, headers # type: ignore[misc] ) - def test_rawHeaders(self): # type: () -> None """ @@ -425,7 +408,6 @@ def test_rawHeaders(self): headers = self.headers(rawHeaders=rawHeaders) self.assertRawHeadersEqual(headers.rawHeaders, tuple(rawHeaders)) - def test_removeBytesName(self): # type: () -> None """ @@ -440,7 +422,6 @@ def test_removeBytesName(self): headers.rawHeaders, ((b"a", b"1"), (b"c", b"3")) ) - def test_removeTextName(self): # type: () -> None """ @@ -455,7 +436,6 @@ def test_removeTextName(self): headers.rawHeaders, ((b"a", b"1"), (b"c", b"3")) ) - def test_removeInvalidNameType(self): # type: () -> None """ @@ -471,7 +451,6 @@ def test_removeInvalidNameType(self): str(e), "name must be text or bytes" ) - def test_addValueBytesName(self): # type: () -> None """ @@ -486,7 +465,6 @@ def test_addValueBytesName(self): headers.rawHeaders, ((b"a", b"1"), (b"b", b"2a"), (b"b", b"2b")) ) - def test_addValueTextName(self): # type: () -> None """ @@ -501,7 +479,6 @@ def test_addValueTextName(self): headers.rawHeaders, ((b"a", b"1"), (b"b", b"2a"), (b"b", b"2b")) ) - def test_addValueBytesNameTextValue(self): # type: () -> None """ @@ -517,7 +494,6 @@ def test_addValueBytesNameTextValue(self): str(e), "value u?'1' must be bytes to match name b?'a'" ) - def test_addValueTextNameBytesValue(self): # type: () -> None """ @@ -533,7 +509,6 @@ def test_addValueTextNameBytesValue(self): str(e), "value b?'1' must be text to match name u?'a'" ) - def test_addValueInvalidNameType(self): # type: () -> None """ @@ -550,7 +525,6 @@ def test_addValueInvalidNameType(self): ) - class MutableHTTPHeadersTests(MutableHTTPHeadersTestsMixIn, TestCase): """ Tests for L{MutableHTTPHeaders}. @@ -560,7 +534,6 @@ def headers(self, rawHeaders): # type: (RawHeaders) -> IMutableHTTPHeaders return MutableHTTPHeaders(rawHeaders=rawHeaders) - def test_defaultHeaders(self): # type: () -> None """ diff --git a/src/klein/test/test_headers_compat.py b/src/klein/test/test_headers_compat.py index 672a8744..c218f348 100644 --- a/src/klein/test/test_headers_compat.py +++ b/src/klein/test/test_headers_compat.py @@ -12,7 +12,9 @@ from ._trial import TestCase from .test_headers import MutableHTTPHeadersTestsMixIn from .._headers import ( - IMutableHTTPHeaders, RawHeaders, normalizeRawHeadersFrozen + IMutableHTTPHeaders, + RawHeaders, + normalizeRawHeadersFrozen, ) from .._headers_compat import HTTPHeadersWrappingHeaders @@ -22,6 +24,7 @@ except ImportError: _sanitizeLinearWhitespace = None + def _twistedHeaderNormalize(value): # type: (Text) -> Text """ @@ -37,7 +40,6 @@ def _twistedHeaderNormalize(value): __all__ = () - class HTTPHeadersWrappingHeadersTests(MutableHTTPHeadersTestsMixIn, TestCase): """ Tests for L{HTTPHeadersWrappingHeaders}. @@ -49,12 +51,10 @@ def assertRawHeadersEqual(self, rawHeaders1, rawHeaders2): sorted(rawHeaders1), sorted(rawHeaders2) ) - def headerNormalize(self, value): # type: (Text) -> Text return _twistedHeaderNormalize(value) - def headers(self, rawHeaders): # type: (RawHeaders) -> IMutableHTTPHeaders headers = Headers() @@ -63,7 +63,6 @@ def headers(self, rawHeaders): return HTTPHeadersWrappingHeaders(headers=headers) - def test_rawHeaders(self): # type: () -> None """ diff --git a/src/klein/test/test_memory.py b/src/klein/test/test_memory.py index 60581c8f..3a57aafd 100644 --- a/src/klein/test/test_memory.py +++ b/src/klein/test/test_memory.py @@ -1,4 +1,3 @@ - from typing import Any from twisted.trial.unittest import SynchronousTestCase @@ -10,21 +9,18 @@ from klein.storage.memory import MemorySessionStore, declareMemoryAuthorizer - class IFoo(Interface): """ Testing interface 1. """ - class IBar(Interface): """ Testing interface 2. """ - class MemoryTests(SynchronousTestCase): """ Tests for memory-based session storage. @@ -38,12 +34,12 @@ def test_interfaceCompliance(self): store = MemorySessionStore() verifyObject(ISessionStore, store) # type: ignore[misc] verifyObject( - ISession, self.successResultOf( # type: ignore[misc] + ISession, # type: ignore[misc] + self.successResultOf( store.newSession(True, SessionMechanism.Header) - ) + ), ) - def test_noAuthorizers(self): # type: () -> None """ @@ -54,9 +50,9 @@ def test_noAuthorizers(self): session = self.successResultOf( store.newSession(True, SessionMechanism.Header) ) - self.assertEqual(self.successResultOf(session.authorize([IFoo, IBar])), - {}) - + self.assertEqual( + self.successResultOf(session.authorize([IFoo, IBar])), {} + ) def test_simpleAuthorization(self): # type: () -> None @@ -65,6 +61,7 @@ def test_simpleAuthorization(self): decorated with L{declareMemoryAuthorizer} and constructs a session store that can authorize for those interfaces. """ + @declareMemoryAuthorizer(IFoo) def fooMe(interface, session, componentized): # type: (Any, Any, Any) -> int @@ -79,5 +76,7 @@ def barMe(interface, session, componentized): session = self.successResultOf( store.newSession(False, SessionMechanism.Cookie) ) - self.assertEqual(self.successResultOf(session.authorize([IBar, IFoo])), - {IFoo: 1, IBar: 2}) + self.assertEqual( + self.successResultOf(session.authorize([IBar, IFoo])), + {IFoo: 1, IBar: 2}, + ) diff --git a/src/klein/test/test_message.py b/src/klein/test/test_message.py index 2ec03333..bfe1cc02 100644 --- a/src/klein/test/test_message.py +++ b/src/klein/test/test_message.py @@ -18,7 +18,6 @@ __all__ = () - class FrozenHTTPMessageTestsMixIn(object): """ Mix-In class for implementations of IHTTPMessage. @@ -31,10 +30,7 @@ def messageFromBytes(cls, data=b""): Return a new instance of an L{IHTTPMessage} implementation using the given bytes as the message body. """ - raise NotImplementedError( - "{} must implement getValues()".format(cls) - ) - + raise NotImplementedError("{} must implement getValues()".format(cls)) @classmethod def messageFromFountFromBytes(cls, data=b""): @@ -45,7 +41,6 @@ def messageFromFountFromBytes(cls, data=b""): """ return cls.messageFromBytes(bytesToFount(data)) - def test_interface_message(self): # type: () -> None """ @@ -56,7 +51,6 @@ def test_interface_message(self): IHTTPMessage, message # type: ignore[misc] ) - @given(binary()) def test_bodyAsFountFromBytes(self, data): # type: (bytes) -> None @@ -70,7 +64,6 @@ def test_bodyAsFountFromBytes(self, data): cast(TestCase, self).assertEqual(body, data) - @given(binary()) def test_bodyAsFountFromBytesTwice(self, data): # type: (bytes) -> None @@ -84,7 +77,6 @@ def test_bodyAsFountFromBytesTwice(self, data): FountAlreadyAccessedError, message.bodyAsFount ) - @given(binary()) def test_bodyAsFountFromFount(self, data): # type: (bytes) -> None @@ -97,7 +89,6 @@ def test_bodyAsFountFromFount(self, data): cast(TestCase, self).assertEqual(body, data) - @given(binary()) def test_bodyAsFountFromFountTwice(self, data): # type: (bytes) -> None @@ -111,7 +102,6 @@ def test_bodyAsFountFromFountTwice(self, data): FountAlreadyAccessedError, message.bodyAsFount ) - @given(binary()) def test_bodyAsBytesFromBytes(self, data): # type: (bytes) -> None @@ -123,7 +113,6 @@ def test_bodyAsBytesFromBytes(self, data): cast(TestCase, self).assertEqual(body, data) - @given(binary()) def test_bodyAsBytesFromBytesCached(self, data): # type: (bytes) -> None @@ -137,7 +126,6 @@ def test_bodyAsBytesFromBytesCached(self, data): cast(TestCase, self).assertIdentical(body1, body2) - @given(binary()) def test_bodyAsBytesFromFount(self, data): # type: (bytes) -> None @@ -148,7 +136,6 @@ def test_bodyAsBytesFromFount(self, data): body = cast(TestCase, self).successResultOf(message.bodyAsBytes()) cast(TestCase, self).assertEqual(body, data) - @given(binary()) def test_bodyAsBytesFromFountCached(self, data): # type: (bytes) -> None diff --git a/src/klein/test/test_plating.py b/src/klein/test/test_plating.py index 6ed9f66f..dec8845c 100644 --- a/src/klein/test/test_plating.py +++ b/src/klein/test/test_plating.py @@ -3,7 +3,10 @@ """ from __future__ import ( - absolute_import, division, print_function, unicode_literals + absolute_import, + division, + print_function, + unicode_literals, ) import json @@ -15,7 +18,8 @@ from twisted.internet.defer import Deferred, succeed from twisted.trial.unittest import ( - SynchronousTestCase, TestCase as AsynchronousTestCase + SynchronousTestCase, + TestCase as AsynchronousTestCase, ) from twisted.web.error import FlattenerError, MissingRenderMethod from twisted.web.template import slot, tags @@ -35,22 +39,17 @@ tags.body( tags.h1(slot("title")), tags.div(slot(Plating.CONTENT), Class="content"), - tags.div(id="rendermethod", render="registeredRenderMethod") + tags.div(id="rendermethod", render="registeredRenderMethod"), ), ), ) element = Plating( - defaults={ - "a": "NO VALUE FOR A", - "b": "NO VALUE FOR B", - }, - tags=tags.div( - tags.span("a: ", slot("a")), - tags.span("b: ", slot("b")), - ), + defaults={"a": "NO VALUE FOR A", "b": "NO VALUE FOR B",}, + tags=tags.div(tags.span("a: ", slot("a")), tags.span("b: ", slot("b")),), ) + @element.widgeted def enwidget(a, b): """ @@ -108,6 +107,7 @@ class DeferredValue(object): @param deferred: The L{Deferred} representing the value. """ + value = attr.ib() # type: object deferred = attr.ib(attr.Factory(Deferred)) # type: Deferred @@ -119,11 +119,13 @@ def resolve(self): self.deferred.callback(self.value) -jsonAtoms = (st.none() | - st.booleans() | - st.integers() | - st.floats(allow_nan=False) | - st.text(printable)) +jsonAtoms = ( + st.none() + | st.booleans() + | st.integers() + | st.floats(allow_nan=False) + | st.text(printable) +) def jsonComposites(children): @@ -136,9 +138,11 @@ def jsonComposites(children): @return: The composite objects strategy. """ - return (st.lists(children) | - st.dictionaries(st.text(printable), children) | - st.tuples(children)) + return ( + st.lists(children) + | st.dictionaries(st.text(printable), children) + | st.tuples(children) + ) jsonObjects = st.recursive(jsonAtoms, jsonComposites, max_leaves=200) @@ -186,6 +190,7 @@ def test_transform_atom(self): L{transform_json_object} transforms a representative atomic JSON data types. """ + def inc(value): return value + 1 @@ -195,6 +200,7 @@ def test_transform_tuple(self): """ L{transformJSONObject} transforms L{tuple}s. """ + def inc(value): return value + 1 @@ -204,6 +210,7 @@ def test_transform_list(self): """ L{transformJSONObject} transforms L{list}s. """ + def inc(value): return value + 1 @@ -213,6 +220,7 @@ def test_transform_dict(self): """ L{transformJSONObject} transforms L{dict}s. """ + def inc(value): return value + 1 @@ -223,9 +231,9 @@ def test_transform_unserializable(self): L{transformJSONObject} will not transform objects that are not JSON serializable. """ - self.assertRaises(AssertionError, - transformJSONObject, - set(), list.append) + self.assertRaises( + AssertionError, transformJSONObject, set(), list.append + ) class ResolveDeferredObjectsTests(SynchronousTestCase): @@ -235,8 +243,7 @@ class ResolveDeferredObjectsTests(SynchronousTestCase): @settings(max_examples=500) @given( - jsonObject=jsonObjects, - data=st.data(), + jsonObject=jsonObjects, data=st.data(), ) def test_resolveObjects(self, jsonObject, data): """ @@ -255,8 +262,7 @@ def maybeWrapInDeferred(value): return value deferredJSONObject = transformJSONObject( - jsonObject, - maybeWrapInDeferred, + jsonObject, maybeWrapInDeferred, ) resolved = resolveDeferredObjects(deferredJSONObject) @@ -266,10 +272,8 @@ def maybeWrapInDeferred(value): self.assertEqual(self.successResultOf(resolved), jsonObject) - @given( - jsonObject=jsonObjects, - data=st.data(), + jsonObject=jsonObjects, data=st.data(), ) def test_elementSerialized(self, jsonObject, data): """ @@ -280,24 +284,24 @@ def test_elementSerialized(self, jsonObject, data): def injectPlatingElements(value): if data.draw(choose) and isinstance(value, dict): - return PlatedElement(slot_data=value, - preloaded=tags.html(), - boundInstance=None, - presentationSlots={}, - renderers={}) + return PlatedElement( + slot_data=value, + preloaded=tags.html(), + boundInstance=None, + presentationSlots={}, + renderers={}, + ) else: return value withPlatingElements = transformJSONObject( - jsonObject, - injectPlatingElements, + jsonObject, injectPlatingElements, ) resolved = resolveDeferredObjects(withPlatingElements) self.assertEqual(self.successResultOf(resolved), jsonObject) - def test_unserializableObject(self): """ An object that cannot be serialized causes the L{Deferred} to @@ -315,9 +319,7 @@ class ConsistentRepr(object): resolveDeferredObjects(ConsistentRepr()) ).value self.assertIsInstance(exception, TypeError) - self.assertIn("ConsistentRepr() not JSON serializable", - str(exception)) - + self.assertIn("ConsistentRepr() not JSON serializable", str(exception)) class PlatingTests(AsynchronousTestCase): @@ -348,27 +350,29 @@ def test_template_html(self): Rendering a L{Plating.routed} decorated route results in templated HTML. """ + @page.routed(self.app.route("/"), tags.span(slot("ok"))) def plateMe(request): return {"ok": "test-data-present"} request, written = self.get(b"/") - self.assertIn(b'test-data-present', written) - self.assertIn(b'default title unchanged', written) + self.assertIn(b"test-data-present", written) + self.assertIn(b"default title unchanged", written) def test_selfhood(self): """ Rendering a L{Plating.routed} decorated route on a method still results in the decorated method receiving the appropriate C{self}. """ + class AppObj(object): app = Klein() def __init__(self, x): self.x = x - @page.routed(app.route("/"), tags.span(slot('yeah'))) + @page.routed(app.route("/"), tags.span(slot("yeah"))) def plateInstance(self, request): return {"yeah": "test-instance-data-" + self.x} @@ -376,30 +380,29 @@ def plateInstance(self, request): self.kr = obj.app.resource() request, written = self.get(b"/") - self.assertIn(b'test-instance-data-confirmed', written) - self.assertIn(b'default title unchanged', written) - self.assertIn(b'
(self)some text!
', - written) - + self.assertIn(b"test-instance-data-confirmed", written) + self.assertIn(b"default title unchanged", written) + self.assertIn(b'
(self)some text!
', written) def test_template_json(self): """ Rendering a L{Plating.routed} decorated route with a query parameter asking for JSON will yield JSON instead. """ + @page.routed(self.app.route("/"), tags.span(slot("ok"))) def plateMe(request): return {"ok": "an-plating-test"} request, written = self.get(b"/?json=true") self.assertEqual( - request.responseHeaders.getRawHeaders(b'content-type')[0], - b'text/json; charset=utf-8' + request.responseHeaders.getRawHeaders(b"content-type")[0], + b"text/json; charset=utf-8", + ) + self.assertEquals( + {"ok": "an-plating-test", "title": "default title unchanged"}, + json.loads(written.decode("utf-8")), ) - self.assertEquals({"ok": "an-plating-test", - "title": "default title unchanged"}, - json.loads(written.decode('utf-8'))) - def test_template_json_contains_deferred(self): """ @@ -407,19 +410,20 @@ def test_template_json_contains_deferred(self): parameter asking for JSON waits until the L{Deferred}s returned by the route have fired. """ + @page.routed(self.app.route("/"), tags.span(slot("ok"))) def plateMe(request): return {"ok": succeed("an-plating-test")} request, written = self.get(b"/?json=true") self.assertEqual( - request.responseHeaders.getRawHeaders(b'content-type')[0], - b'text/json; charset=utf-8' + request.responseHeaders.getRawHeaders(b"content-type")[0], + b"text/json; charset=utf-8", + ) + self.assertEquals( + {"ok": "an-plating-test", "title": "default title unchanged"}, + json.loads(written.decode("utf-8")), ) - self.assertEquals({"ok": "an-plating-test", - "title": "default title unchanged"}, - json.loads(written.decode('utf-8'))) - def test_template_numbers(self): """ @@ -428,6 +432,7 @@ def test_template_numbers(self): serializable by twisted.web.template, will be converted by plating into their decimal representation. """ + @page.routed( self.app.route("/"), tags.div( @@ -437,9 +442,11 @@ def test_template_numbers(self): ), ) def plateMe(result): - return {"anInteger": 7, - "anFloat": 3.2, - "anLong": 0x10000000000000001} + return { + "anInteger": 7, + "anFloat": 3.2, + "anLong": 0x10000000000000001, + } request, written = self.get(b"/") @@ -452,17 +459,18 @@ def test_render_list(self): The C{:list} renderer suffix will render the slot named by the renderer as a list, filling each slot. """ + @page.routed( self.app.route("/"), - tags.ul(tags.li(slot("item"), render="subplating:list")) + tags.ul(tags.li(slot("item"), render="subplating:list")), ) def rsrc(request): return {"subplating": [1, 2, 3]} request, written = self.get(b"/") - self.assertIn(b'
  • 1
  • 2
  • 3
', written) - self.assertIn(b'default title unchanged', written) + self.assertIn(b"
  • 1
  • 2
  • 3
", written) + self.assertIn(b"default title unchanged", written) def test_widget_function(self): """ @@ -472,12 +480,12 @@ def test_widget_function(self): self.assertEqual(enwidget(5, 6), {"a": 5, "b": 6}) self.assertEqual(InstanceWidget().enwidget(7, 8), {"a": 7, "b": 8}) - def test_renderMethod(self): """ L{Plating.renderMethod} registers a renderer that may be used by the L{Plating}'s template. """ + @page.routed(self.app.route("/"), []) def rsrc(request): return {} @@ -485,7 +493,6 @@ def rsrc(request): request, written = self.get(b"/") self.assertIn(b'
some text!
', written) - def test_widget_html(self): """ When L{Plating.widgeted} is applied as a decorator, it gives the @@ -493,12 +500,18 @@ def test_widget_html(self): function with a modified return type that turns it into a renderable HTML sub-element that may fill a slot. """ - @page.routed(self.app.route("/"), - tags.div(tags.div(slot("widget")), - tags.div(slot("instance-widget")))) + + @page.routed( + self.app.route("/"), + tags.div( + tags.div(slot("widget")), tags.div(slot("instance-widget")) + ), + ) def rsrc(request): - return {"widget": enwidget.widget(a=3, b=4), - "instance-widget": InstanceWidget().enwidget.widget(5, 6)} + return { + "widget": enwidget.widget(a=3, b=4), + "instance-widget": InstanceWidget().enwidget.widget(5, 6), + } request, written = self.get(b"/") @@ -513,18 +526,28 @@ def test_widget_json(self): serialized to JSON, it appears the same as the returned value despite the HTML-friendly wrapping described above. """ - @page.routed(self.app.route("/"), - tags.div(tags.div(slot("widget")), - tags.div(slot("instance-widget")))) + + @page.routed( + self.app.route("/"), + tags.div( + tags.div(slot("widget")), tags.div(slot("instance-widget")) + ), + ) def rsrc(request): - return {"widget": enwidget.widget(a=3, b=4), - "instance-widget": InstanceWidget().enwidget.widget(5, 6)} + return { + "widget": enwidget.widget(a=3, b=4), + "instance-widget": InstanceWidget().enwidget.widget(5, 6), + } request, written = self.get(b"/?json=1") - self.assertEqual(json.loads(written.decode('utf-8')), - {"widget": {"a": 3, "b": 4}, - "instance-widget": {"a": 5, "b": 6}, - "title": "default title unchanged"}) + self.assertEqual( + json.loads(written.decode("utf-8")), + { + "widget": {"a": 3, "b": 4}, + "instance-widget": {"a": 5, "b": 6}, + "title": "default title unchanged", + }, + ) def test_widget_json_deferred(self): """ @@ -533,19 +556,28 @@ def test_widget_json_deferred(self): the HTML-friendly wrapping described above. """ - @page.routed(self.app.route("/"), - tags.div(tags.div(slot("widget")), - tags.div(slot("instance-widget")))) + @page.routed( + self.app.route("/"), + tags.div( + tags.div(slot("widget")), tags.div(slot("instance-widget")) + ), + ) def rsrc(request): instance = InstanceWidget() - return {"widget": deferredEnwidget.widget(a=3, b=4), - "instance-widget": instance.deferredEnwidget.widget(5, 6)} + return { + "widget": deferredEnwidget.widget(a=3, b=4), + "instance-widget": instance.deferredEnwidget.widget(5, 6), + } request, written = self.get(b"/?json=1") - self.assertEqual(json.loads(written.decode('utf-8')), - {"widget": {"a": 3, "b": 4}, - "instance-widget": {"a": 5, "b": 6}, - "title": "default title unchanged"}) + self.assertEqual( + json.loads(written.decode("utf-8")), + { + "widget": {"a": 3, "b": 4}, + "instance-widget": {"a": 5, "b": 6}, + "title": "default title unchanged", + }, + ) def test_prime_directive_return(self): """ @@ -566,6 +598,7 @@ def test_prime_directive_arguments(self): ... or shall require the function to modify its signature under these Articles Of Federation. """ + @page.routed(self.app.route("/"), tags.span(slot("ok"))) def plateMe(request, one, two, three): return (one, two, three) @@ -587,8 +620,7 @@ def test_presentation_only_json(self): output. """ plating = Plating( - tags=tags.span(slot("title")), - presentation_slots={"title"} + tags=tags.span(slot("title")), presentation_slots={"title"} ) @plating.routed(self.app.route("/"), tags.span(slot("data"))) @@ -597,19 +629,20 @@ def justJson(request): request, written = self.get(b"/?json=1") - self.assertEqual(json.loads(written.decode("utf-8")), - {"data": "interesting"}) + self.assertEqual( + json.loads(written.decode("utf-8")), {"data": "interesting"} + ) def test_missing_renderer(self): """ Missing renderers will result in an exception during rendering. """ + def test(missing): plating = Plating(tags=tags.span(slot(Plating.CONTENT))) @plating.routed( - self.app.route("/"), - tags.span(tags.span(render=missing)) + self.app.route("/"), tags.span(tags.span(render=missing)) ) def no(request): return {} diff --git a/src/klein/test/test_request.py b/src/klein/test/test_request.py index a9d97acd..ca6cd20c 100644 --- a/src/klein/test/test_request.py +++ b/src/klein/test/test_request.py @@ -17,7 +17,6 @@ __all__ = () - class FrozenHTTPRequestTests(FrozenHTTPMessageTestsMixIn, TestCase): """ Tests for L{FrozenHTTPRequest}. @@ -33,13 +32,11 @@ def requestFromBytes(data=b""): body=data, ) - @classmethod def messageFromBytes(cls, data=b""): # type: (bytes) -> IHTTPMessage return cls.requestFromBytes(data) - def test_interface(self): # type: () -> None """ @@ -48,7 +45,6 @@ def test_interface(self): request = self.requestFromBytes() self.assertProvides(IHTTPRequest, request) # type: ignore[misc] - def test_initInvalidBodyType(self): # type: () -> None """ diff --git a/src/klein/test/test_request_compat.py b/src/klein/test/test_request_compat.py index fdf99a5f..81edc64f 100644 --- a/src/klein/test/test_request_compat.py +++ b/src/klein/test/test_request_compat.py @@ -28,7 +28,6 @@ __all__ = () - class HTTPRequestWrappingIRequestTests(TestCase): """ Tests for L{HTTPRequestWrappingIRequest}. @@ -36,21 +35,25 @@ class HTTPRequestWrappingIRequestTests(TestCase): def legacyRequest( self, - path=b"/", # type: bytes - method=b"GET", # type: bytes + path=b"/", # type: bytes + method=b"GET", # type: bytes host=b"localhost", # type: bytes - port=8080, # type: int - isSecure=False, # type: bool - body=None, # type: Optional[bytes] - headers=None, # type: Optional[Headers] + port=8080, # type: int + isSecure=False, # type: bool + body=None, # type: Optional[bytes] + headers=None, # type: Optional[Headers] ): # type: (...) -> IRequest return requestMock( - path=path, method=method, host=host, port=port, - isSecure=isSecure, body=body, headers=headers, + path=path, + method=method, + host=host, + port=port, + isSecure=isSecure, + body=body, + headers=headers, ) - def test_interface(self): # type: () -> None """ @@ -59,7 +62,6 @@ def test_interface(self): request = HTTPRequestWrappingIRequest(request=self.legacyRequest()) self.assertProvides(IHTTPRequest, request) # type: ignore[misc] - @given(text(alphabet=ascii_uppercase, min_size=1)) def test_method(self, methodText): # type: (Text) -> None @@ -71,7 +73,6 @@ def test_method(self, methodText): request = HTTPRequestWrappingIRequest(request=legacyRequest) self.assertEqual(request.method, methodText) - @given(decoded_urls()) def test_uri(self, url): # type: (DecodedURL) -> None @@ -83,11 +84,14 @@ def test_uri(self, url): path = ( uri.replace(scheme=u"", host=u"", port=None) - .asText().encode("ascii") + .asText() + .encode("ascii") ) legacyRequest = self.legacyRequest( isSecure=(uri.scheme == u"https"), - host=uri.host.encode("ascii"), port=uri.port, path=path, + host=uri.host.encode("ascii"), + port=uri.port, + path=path, ) request = HTTPRequestWrappingIRequest(request=legacyRequest) @@ -109,13 +113,13 @@ def strURL(url): ).format(url=url) self.assertEqual( - requestURINormalized, uriNormalized, + requestURINormalized, + uriNormalized, "{} != {}".format( strURL(requestURINormalized), strURL(uriNormalized) - ) + ), ) - def test_headers(self): # type: () -> None """ @@ -129,7 +133,6 @@ def test_headers(self): IHTTPHeaders, request.headers # type: ignore[misc] ) - def test_bodyAsFountTwice(self): # type: () -> None """ @@ -141,7 +144,6 @@ def test_bodyAsFountTwice(self): request.bodyAsFount() self.assertRaises(FountAlreadyAccessedError, request.bodyAsFount) - @given(binary()) def test_bodyAsBytes(self, data): # type: (bytes) -> None @@ -155,7 +157,6 @@ def test_bodyAsBytes(self, data): self.assertEqual(body, data) - def test_bodyAsBytesCached(self): # type: () -> None """ diff --git a/src/klein/test/test_requirer.py b/src/klein/test/test_requirer.py index b1fb99aa..e693dc53 100644 --- a/src/klein/test/test_requirer.py +++ b/src/klein/test/test_requirer.py @@ -13,7 +13,6 @@ from klein import Klein, RequestComponent, RequestURL, Requirer, Response - class BadlyBehavedHeaders(Headers): """ Make L{Headers} lie, and refuse to return a Host header from @@ -26,17 +25,17 @@ def getAllRawHeaders(self): Don't return a host header. """ for key, values in super(BadlyBehavedHeaders, self).getAllRawHeaders(): - if key != b'Host': + if key != b"Host": yield (key, values) - router = Klein() requirer = Requirer() -@requirer.require(router.route("/hello/world", methods=['GET']), - url=RequestURL()) +@requirer.require( + router.route("/hello/world", methods=["GET"]), url=RequestURL() +) def requiresURL(url): # type: (DecodedURL) -> Text """ @@ -45,14 +44,12 @@ def requiresURL(url): return url.child(u"hello/ world").asText() - class ISample(Interface): """ Interface for testing. """ - @requirer.prerequisite([ISample]) def provideSample(request): # type: (IRequest) -> None @@ -62,8 +59,10 @@ def provideSample(request): request.setComponent(ISample, "sample component") -@requirer.require(router.route("/retrieve/component", methods=['GET']), - component=RequestComponent(ISample)) +@requirer.require( + router.route("/retrieve/component", methods=["GET"]), + component=RequestComponent(ISample), +) def needsComponent(component): # type: (Text) -> Text """ @@ -79,13 +78,12 @@ def someHeaders(): Set some response attributes. """ return Response( - 209, {'x-single-header': b'one', - 'x-multi-header': [b'two', b'three']}, - "this is the response body" + 209, + {"x-single-header": b"one", "x-multi-header": [b"two", b"three"]}, + "this is the response body", ) - class RequireURLTests(SynchronousTestCase): """ Tests for RequestURL() required parameter. @@ -98,14 +96,16 @@ def test_requiresURL(self): passed in to the decorated route. """ response = self.successResultOf( - self.successResultOf(StubTreq(router.resource()).get( - "https://example.com/hello/world" - )).text() + self.successResultOf( + StubTreq(router.resource()).get( + "https://example.com/hello/world" + ) + ).text() ) - self.assertEqual(response, - "https://example.com/hello/world/hello%2F%20world") - + self.assertEqual( + response, "https://example.com/hello/world/hello%2F%20world" + ) def test_requiresURLNonStandardPort(self): # type: () -> None @@ -114,36 +114,37 @@ def test_requiresURLNonStandardPort(self): passed in to the decorated route. """ response = self.successResultOf( - self.successResultOf(StubTreq(router.resource()).get( - "http://example.com:8080/hello/world" - )).text() + self.successResultOf( + StubTreq(router.resource()).get( + "http://example.com:8080/hello/world" + ) + ).text() ) self.assertEqual( - response, - "http://example.com:8080/hello/world/hello%2F%20world" + response, "http://example.com:8080/hello/world/hello%2F%20world" ) - def test_requiresURLBadlyBehavedClient(self): # type: () -> None """ requiresURL will press on in the face of badly-behaved client code. """ response = self.successResultOf( - self.successResultOf(StubTreq(router.resource()).get( - "https://example.com/hello/world", - headers=BadlyBehavedHeaders() - )).text() + self.successResultOf( + StubTreq(router.resource()).get( + "https://example.com/hello/world", + headers=BadlyBehavedHeaders(), + ) + ).text() ) self.assertEqual( response, # Static values from StubTreq - this should probably be tunable. - "https://127.0.0.1:31337/hello/world/hello%2F%20world" + "https://127.0.0.1:31337/hello/world/hello%2F%20world", ) - class RequireComponentTests(SynchronousTestCase): """ Tests for RequestComponent. @@ -155,14 +156,13 @@ def test_requestComponent(self): Test for requiring a component installed on the request. """ response = self.successResultOf( - self.successResultOf(StubTreq(router.resource()).get( - "https://example.com/retrieve/component", - )).text() - ) - self.assertEqual( - response, "sample component" + self.successResultOf( + StubTreq(router.resource()).get( + "https://example.com/retrieve/component", + ) + ).text() ) - + self.assertEqual(response, "sample component") class ResponseTests(SynchronousTestCase): @@ -177,11 +177,14 @@ def test_basicResponse(self): can't access it to set headers and response codes, instead, they can return a Response object that has those attributes. """ - response = self.successResultOf(StubTreq(router.resource()).get( - "https://example.com/set/headers", - )) + response = self.successResultOf( + StubTreq(router.resource()).get("https://example.com/set/headers",) + ) self.assertEqual(response.code, 209) - self.assertEqual(response.headers.getRawHeaders(b"X-Single-Header"), - [b'one']) - self.assertEqual(response.headers.getRawHeaders(b"X-Multi-Header"), - [b'two', b'three']) + self.assertEqual( + response.headers.getRawHeaders(b"X-Single-Header"), [b"one"] + ) + self.assertEqual( + response.headers.getRawHeaders(b"X-Multi-Header"), + [b"two", b"three"], + ) diff --git a/src/klein/test/test_resource.py b/src/klein/test/test_resource.py index 0ecc2c69..d90dba50 100644 --- a/src/klein/test/test_resource.py +++ b/src/klein/test/test_resource.py @@ -28,17 +28,27 @@ from .. import Klein from .._interfaces import IKleinRequest from .._resource import ( - KleinResource, _URLDecodeError, _extractURLparts, ensure_utf8_bytes + KleinResource, + _URLDecodeError, + _extractURLparts, + ensure_utf8_bytes, ) -def requestMock(path, method=b"GET", host=b"localhost", port=8080, - isSecure=False, body=None, headers=None): +def requestMock( + path, + method=b"GET", + host=b"localhost", + port=8080, + isSecure=False, + body=None, + headers=None, +): if not headers: headers = {} if not body: - body = b'' + body = b"" path, qpath = (path.split(b"?", 1) + [b""])[:2] @@ -53,9 +63,9 @@ def requestMock(path, method=b"GET", host=b"localhost", port=8080, request.setHost(host, port, isSecure) request.uri = path request.prepath = [] - request.postpath = path.split(b'/')[1:] + request.postpath = path.split(b"/")[1:] request.method = method - request.clientproto = b'HTTP/1.1' + request.clientproto = b"HTTP/1.1" request.setHeader = Mock(wraps=request.setHeader) request.setResponseCode = Mock(wraps=request.setResponseCode) @@ -77,7 +87,7 @@ def finish(): request.finishCount += 1 if not request.startedWriting: - request.write(b'') + request.write(b"") if not request.finished: request.finished = True @@ -90,8 +100,10 @@ def write(data): if not request.finished: request._written.write(data) else: - raise RuntimeError('Request.write called on a request after ' - 'Request.finish was called.') + raise RuntimeError( + "Request.write called on a request after " + "Request.finish was called." + ) def getWrittenData(): return request._written.getvalue() @@ -137,6 +149,7 @@ def __init__(self, name): def name(self, request, tag): return tag(self._name) + class DeferredElement(SimpleElement): @renderer def name(self, request, tag): @@ -144,6 +157,7 @@ def name(self, request, tag): self.deferred.addCallback(lambda ignored: tag(self._name)) return self.deferred + class LeafResource(Resource): isLeaf = True @@ -166,7 +180,7 @@ def render(self, request): return b"I have children!" def getChild(self, path, request): - if path == b'': + if path == b"": return self return ChildResource(path) @@ -199,11 +213,11 @@ def resumeProducing(self): self.request.finish() - class KleinResourceEqualityTests(SynchronousTestCase, EqualityTestsMixin): """ Tests for L{KleinResource}'s implementation of C{==} and C{!=}. """ + class _One(object): oneKlein = Klein() @@ -213,7 +227,6 @@ def foo(self): _one = _One() - class _Another(object): anotherKlein = Klein() @@ -223,42 +236,37 @@ def bar(self): _another = _Another() - def anInstance(self): return self._one.oneKlein - def anotherInstance(self): return self._another.anotherKlein - class KleinResourceTests(SynchronousTestCase): def setUp(self): self.app = Klein() self.kr = KleinResource(self.app) - def assertFired(self, deferred, result=None): """ Assert that the given deferred has fired with the given result. """ self.assertEqual(self.successResultOf(deferred), result) - def assertNotFired(self, deferred): """ Assert that the given deferred has not fired with a result. """ _pawn = object() - result = getattr(deferred, 'result', _pawn) + result = getattr(deferred, "result", _pawn) if result != _pawn: self.fail( - "Expected deferred not to have fired, but it has: {!r}" - .format(deferred) + "Expected deferred not to have fired, but it has: {!r}".format( + deferred + ) ) - def test_simplePost(self): app = self.app @@ -269,103 +277,98 @@ def test_simplePost(self): @app.route("/", methods=["POST"]) def handle_post(request): - return b'posted' + return b"posted" @app.route("/") def handle(request): - return b'gotted' + return b"gotted" - request = requestMock(b'/', b'POST') - request2 = requestMock(b'/') + request = requestMock(b"/", b"POST") + request2 = requestMock(b"/") d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(request.getWrittenData(), b'posted') + self.assertEqual(request.getWrittenData(), b"posted") d2 = _render(self.kr, request2) self.assertFired(d2) - self.assertEqual(request2.getWrittenData(), b'gotted') - + self.assertEqual(request2.getWrittenData(), b"gotted") def test_simpleRouting(self): app = self.app @app.route("/") def slash(request): - return b'ok' + return b"ok" - request = requestMock(b'/') + request = requestMock(b"/") d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(request.getWrittenData(), b'ok') - + self.assertEqual(request.getWrittenData(), b"ok") def test_branchRendering(self): app = self.app @app.route("/", branch=True) def slash(request): - return b'ok' + return b"ok" - request = requestMock(b'/foo') + request = requestMock(b"/foo") d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(request.getWrittenData(), b'ok') - + self.assertEqual(request.getWrittenData(), b"ok") def test_branchWithExplicitChildrenRouting(self): app = self.app @app.route("/") def slash(request): - return b'ok' + return b"ok" @app.route("/zeus") def wooo(request): - return b'zeus' + return b"zeus" - request = requestMock(b'/zeus') - request2 = requestMock(b'/') + request = requestMock(b"/zeus") + request2 = requestMock(b"/") d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(request.getWrittenData(), b'zeus') + self.assertEqual(request.getWrittenData(), b"zeus") d2 = _render(self.kr, request2) self.assertFired(d2) - self.assertEqual(request2.getWrittenData(), b'ok') - + self.assertEqual(request2.getWrittenData(), b"ok") def test_branchWithExplicitChildBranch(self): app = self.app @app.route("/", branch=True) def slash(request): - return b'ok' + return b"ok" @app.route("/zeus/", branch=True) def wooo(request): - return b'zeus' + return b"zeus" - request = requestMock(b'/zeus/foo') - request2 = requestMock(b'/') + request = requestMock(b"/zeus/foo") + request2 = requestMock(b"/") d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(request.getWrittenData(), b'zeus') + self.assertEqual(request.getWrittenData(), b"zeus") d2 = _render(self.kr, request2) self.assertFired(d2) - self.assertEqual(request2.getWrittenData(), b'ok') - + self.assertEqual(request2.getWrittenData(), b"ok") def test_deferredRendering(self): app = self.app @@ -382,11 +385,10 @@ def deferred(request): self.assertNotFired(d) - deferredResponse.callback(b'ok') + deferredResponse.callback(b"ok") self.assertFired(d) - self.assertEqual(request.getWrittenData(), b'ok') - + self.assertEqual(request.getWrittenData(), b"ok") def test_elementRendering(self): app = self.app @@ -400,9 +402,9 @@ def element(request, name): d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(request.getWrittenData(), - b"\n

foo

") - + self.assertEqual( + request.getWrittenData(), b"\n

foo

" + ) def test_deferredElementRendering(self): app = self.app @@ -423,9 +425,9 @@ def element(request, name): self.assertNoResult(d) oneElement.deferred.callback(None) self.assertFired(d) - self.assertEqual(request.getWrittenData(), - b"\n

bar

") - + self.assertEqual( + request.getWrittenData(), b"\n

bar

" + ) def test_leafResourceRendering(self): app = self.app @@ -441,7 +443,6 @@ def leaf(request): self.assertFired(d) self.assertEqual(request.getWrittenData(), b"I am a leaf in the wind.") - def test_childResourceRendering(self): app = self.app request = requestMock(b"/resource/children/betty") @@ -453,10 +454,7 @@ def children(request): d = _render(self.kr, request) self.assertFired(d) - self.assertEqual( - request.getWrittenData(), b"I'm a child named betty!" - ) - + self.assertEqual(request.getWrittenData(), b"I'm a child named betty!") def test_childrenResourceRendering(self): app = self.app @@ -472,7 +470,6 @@ def children(request): self.assertFired(d) self.assertEqual(request.getWrittenData(), b"I have children!") - def test_producerResourceRendering(self): """ Test that Klein will correctly handle producing L{Resource}s. @@ -492,8 +489,9 @@ def producer(request): d = _render(self.kr, request, notifyFinish=False) self.assertNotEqual( - request.getWrittenData(), b"abcd", - "The full response should not have been written at this point." + request.getWrittenData(), + b"abcd", + "The full response should not have been written at this point.", ) while request.producer: @@ -505,7 +503,6 @@ def producer(request): self.assertEqual(request.finishCount, 1) self.assertEqual(request.producer, None) - def test_notFound(self): request = requestMock(b"/fourohofour") @@ -515,7 +512,6 @@ def test_notFound(self): request.setResponseCode.assert_called_with(404) self.assertIn(b"404 Not Found", request.getWrittenData()) - def test_renderUnicode(self): app = self.app @@ -523,14 +519,13 @@ def test_renderUnicode(self): @app.route("/snowman") def snowman(request): - return u'\u2603' + return u"\u2603" d = _render(self.kr, request) self.assertFired(d) self.assertEqual(request.getWrittenData(), b"\xE2\x98\x83") - def test_renderNone(self): app = self.app @@ -543,17 +538,16 @@ def none(request): d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(request.getWrittenData(), b'') + self.assertEqual(request.getWrittenData(), b"") self.assertEqual(request.finishCount, 1) self.assertEqual(request.writeCount, 1) - def test_staticRoot(self): app = self.app request = requestMock(b"/__init__.py") expected = open( - os.path.join(os.path.dirname(__file__), "__init__.py"), 'rb' + os.path.join(os.path.dirname(__file__), "__init__.py"), "rb" ).read() @app.route("/", branch=True) @@ -566,13 +560,12 @@ def root(request): self.assertEqual(request.getWrittenData(), expected) self.assertEqual(request.finishCount, 1) - def test_explicitStaticBranch(self): app = self.app request = requestMock(b"/static/__init__.py") expected = open( - os.path.join(os.path.dirname(__file__), "__init__.py"), 'rb' + os.path.join(os.path.dirname(__file__), "__init__.py"), "rb" ).read() @app.route("/static/", branch=True) @@ -598,7 +591,7 @@ def root(request): d = _render(self.kr, request) self.assertFired(d) - self.assertIn(b'Directory listing', request.getWrittenData()) + self.assertIn(b"Directory listing", request.getWrittenData()) self.assertEqual(request.writeCount, 1) self.assertEqual(request.finishCount, 1) @@ -614,17 +607,19 @@ def foo(request): self.assertFired(d) self.assertEqual(request.setHeader.call_count, 3) - request.setHeader.assert_has_calls([ - call(b'Content-Type', b'text/html; charset=utf-8'), - call(b'Content-Length', b'259'), - call(b'Location', b'http://localhost:8080/foo/') - ]) + request.setHeader.assert_has_calls( + [ + call(b"Content-Type", b"text/html; charset=utf-8"), + call(b"Content-Length", b"259"), + call(b"Location", b"http://localhost:8080/foo/"), + ] + ) def test_methodNotAllowed(self): app = self.app - request = requestMock(b"/foo", method=b'DELETE') + request = requestMock(b"/foo", method=b"DELETE") - @app.route("/foo", methods=['GET']) + @app.route("/foo", methods=["GET"]) def foo(request): return "foo" @@ -635,13 +630,13 @@ def foo(request): def test_methodNotAllowedWithRootCollection(self): app = self.app - request = requestMock(b"/foo/bar", method=b'DELETE') + request = requestMock(b"/foo/bar", method=b"DELETE") - @app.route("/foo/bar", methods=['GET']) + @app.route("/foo/bar", methods=["GET"]) def foobar(request): return b"foo/bar" - @app.route("/foo/", methods=['DELETE']) + @app.route("/foo/", methods=["DELETE"]) def foo(request): return b"foo" @@ -678,30 +673,30 @@ def root(request): self.assertFired(d) self.assertEqual(str(request_url[0]), "http://localhost:8080/foo/bar") - self.assertEqual(request.getWrittenData(), b'foo') + self.assertEqual(request.getWrittenData(), b"foo") self.assertEqual(request.code, 200) def test_URLPath(self): app = self.app - request = requestMock(b'/egg/chicken') + request = requestMock(b"/egg/chicken") request_url = [None] @app.route("/egg/chicken") def wooo(request): request_url[0] = request.URLPath() - return b'foo' + return b"foo" d = _render(self.kr, request) self.assertFired(d) self.assertEqual( - str(request_url[0]), 'http://localhost:8080/egg/chicken' + str(request_url[0]), "http://localhost:8080/egg/chicken" ) def test_URLPath_root(self): app = self.app - request = requestMock(b'/') + request = requestMock(b"/") request_url = [None] @@ -712,11 +707,11 @@ def root(request): d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(str(request_url[0]), 'http://localhost:8080/') + self.assertEqual(str(request_url[0]), "http://localhost:8080/") def test_URLPath_traversedResource(self): app = self.app - request = requestMock(b'/resource/foo') + request = requestMock(b"/resource/foo") request_url = [None] @@ -735,7 +730,7 @@ def root(request): self.assertFired(d) self.assertEqual( - str(request_url[0]), 'http://localhost:8080/resource/foo' + str(request_url[0]), "http://localhost:8080/resource/foo" ) def test_handlerRaises(self): @@ -837,7 +832,7 @@ def test_notFoundException(self): @app.handle_errors(NotFound) def handle_not_found(request, failure): request.setResponseCode(404) - return b'Custom Not Found' + return b"Custom Not Found" @app.handle_errors def handle_generic_error(request, failure): @@ -851,7 +846,7 @@ def handle_generic_error(request, failure): self.assertEqual(request.processingFailed.called, False) self.assertEqual(generic_error_handled, False) self.assertEqual(request.code, 404) - self.assertEqual(request.getWrittenData(), b'Custom Not Found') + self.assertEqual(request.getWrittenData(), b"Custom Not Found") self.assertEqual(request.writeCount, 1) def test_errorHandlerNeedsRendering(self): @@ -885,7 +880,7 @@ class NotFoundResource(Resource): def render(self, request): request.setResponseCode(404) - return b'Nothing found' + return b"Nothing found" @app.handle_errors(NotFound) def handle_not_found(request, failure): @@ -895,7 +890,7 @@ def handle_not_found(request, failure): self.assertFired(d) self.assertEqual(request.code, 404) - self.assertEqual(request.getWrittenData(), b'Nothing found') + self.assertEqual(request.getWrittenData(), b"Nothing found") def test_requestWriteAfterFinish(self): app = self.app @@ -904,19 +899,22 @@ def test_requestWriteAfterFinish(self): @app.route("/") def root(request): request.finish() - return b'foo' + return b"foo" d = _render(self.kr, request) self.assertFired(d) self.assertEqual(request.writeCount, 2) - self.assertEqual(request.getWrittenData(), b'') + self.assertEqual(request.getWrittenData(), b"") [failure] = self.flushLoggedErrors(RuntimeError) self.assertEqual( str(failure.value), - ("Request.write called on a request after Request.finish was " - "called.")) + ( + "Request.write called on a request after Request.finish was " + "called." + ), + ) def test_requestFinishAfterConnectionLost(self): app = self.app @@ -926,7 +924,7 @@ def test_requestFinishAfterConnectionLost(self): @app.route("/") def root(request): - request.notifyFinish().addBoth(lambda _: finished.callback(b'foo')) + request.notifyFinish().addBoth(lambda _: finished.callback(b"foo")) return finished d = _render(self.kr, request) @@ -936,8 +934,11 @@ def _eb(result): self.assertEqual( str(failure.value), - ("Request.finish called on a request after its connection was " - "lost; use Request.notifyFinish to keep track of this.")) + ( + "Request.finish called on a request after its connection " + "was lost; use Request.notifyFinish to keep track of this." + ), + ) d.addErrback(lambda _: finished) d.addErrback(_eb) @@ -948,7 +949,6 @@ def _eb(result): self.assertFired(d) - def test_routeHandlesRequestFinished(self): app = self.app request = requestMock(b"/") @@ -969,25 +969,25 @@ def root(request): self.assertFired(d) cancelled[0].trap(CancelledError) - self.assertEqual(request.getWrittenData(), b'') + self.assertEqual(request.getWrittenData(), b"") self.assertEqual(request.writeCount, 1) self.assertEqual(request.processingFailed.call_count, 0) def test_url_for(self): app = self.app - request = requestMock(b'/foo/1') + request = requestMock(b"/foo/1") relative_url = [None] @app.route("/foo/") def foo(request, bar): krequest = IKleinRequest(request) - relative_url[0] = krequest.url_for('foo', {'bar': bar + 1}) + relative_url[0] = krequest.url_for("foo", {"bar": bar + 1}) d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(relative_url[0], '/foo/2') + self.assertEqual(relative_url[0], "/foo/2") def test_cancelledDeferred(self): app = self.app @@ -1008,7 +1008,7 @@ def root(request): def test_external_url_for(self): app = self.app - request = requestMock(b'/foo/1') + request = requestMock(b"/foo/1") relative_url = [None] @@ -1016,13 +1016,13 @@ def test_external_url_for(self): def foo(request, bar): krequest = IKleinRequest(request) relative_url[0] = krequest.url_for( - 'foo', {'bar': bar + 1}, force_external=True + "foo", {"bar": bar + 1}, force_external=True ) d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(relative_url[0], 'http://localhost:8080/foo/2') + self.assertEqual(relative_url[0], "http://localhost:8080/foo/2") def test_cancelledIsEatenOnConnectionLost(self): app = self.app @@ -1121,41 +1121,40 @@ def test_urlDecodeErrorReprPy3(self): else: test_urlDecodeErrorReprPy3.skip = "Only works on Py3" # type: ignore - def test_subroutedBranch(self): subapp = Klein() - @subapp.route('/foo') + @subapp.route("/foo") def foo(request): - return b'foo' + return b"foo" app = self.app - with app.subroute('/sub') as app: - @app.route('/app', branch=True) + with app.subroute("/sub") as app: + + @app.route("/app", branch=True) def subapp_endpoint(request): return subapp.resource() - request = requestMock(b'/sub/app/foo') + request = requestMock(b"/sub/app/foo") d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(request.getWrittenData(), b'foo') - + self.assertEqual(request.getWrittenData(), b"foo") def test_correctContentLengthForRequestRedirect(self): app = self.app - @app.route('/alias', alias=True) - @app.route('/real') + @app.route("/alias", alias=True) + @app.route("/real") def real(req): - return b'42' + return b"42" - request = requestMock(b'/real') + request = requestMock(b"/real") d = _render(self.kr, request) self.assertFired(d) - self.assertEqual(request.getWrittenData(), b'42') + self.assertEqual(request.getWrittenData(), b"42") - request = requestMock(b'/alias') + request = requestMock(b"/alias") d = _render(self.kr, request) self.assertFired(d) # Werkzeug switched the redirect status code used from 301 to 308. @@ -1164,7 +1163,7 @@ def real(req): actual_length = len(request.getWrittenData()) reported_length = int( - request.responseHeaders.getRawHeaders(b'content-length')[0] + request.responseHeaders.getRawHeaders(b"content-length")[0] ) self.assertEqual(reported_length, actual_length) @@ -1173,13 +1172,18 @@ class ExtractURLpartsTests(SynchronousTestCase): """ Tests for L{klein.resource._extractURLparts}. """ + def test_types(self): """ Returns the correct types. """ - url_scheme, server_name, server_port, path_info, script_name = ( - _extractURLparts(requestMock(b"/f\xc3\xb6\xc3\xb6")) - ) + ( + url_scheme, + server_name, + server_port, + path_info, + script_name, + ) = _extractURLparts(requestMock(b"/f\xc3\xb6\xc3\xb6")) self.assertIsInstance(url_scheme, unicode) self.assertIsInstance(server_name, unicode) @@ -1187,7 +1191,6 @@ def test_types(self): self.assertIsInstance(path_info, unicode) self.assertIsInstance(script_name, unicode) - def assertDecodingFailure(self, exception, part): """ Checks whether C{exception} consists of a single L{UnicodeDecodeError} @@ -1198,7 +1201,6 @@ def assertDecodingFailure(self, exception, part): self.assertEqual(part, actualPart) self.assertIsInstance(actualFail.value, UnicodeDecodeError) - def test_failServerName(self): """ Raises URLDecodeError if SERVER_NAME can't be decoded. @@ -1208,7 +1210,6 @@ def test_failServerName(self): e = self.assertRaises(_URLDecodeError, _extractURLparts, request) self.assertDecodingFailure(e, "SERVER_NAME") - def test_failPathInfo(self): """ Raises URLDecodeError if PATH_INFO can't be decoded. @@ -1217,7 +1218,6 @@ def test_failPathInfo(self): e = self.assertRaises(_URLDecodeError, _extractURLparts, request) self.assertDecodingFailure(e, "PATH_INFO") - def test_failScriptName(self): """ Raises URLDecodeError if SCRIPT_NAME can't be decoded. @@ -1227,7 +1227,6 @@ def test_failScriptName(self): e = self.assertRaises(_URLDecodeError, _extractURLparts, request) self.assertDecodingFailure(e, "SCRIPT_NAME") - def test_failAll(self): """ If multiple parts fail, they all get appended to the errors list of @@ -1239,7 +1238,7 @@ def test_failAll(self): e = self.assertRaises(_URLDecodeError, _extractURLparts, request) self.assertEqual( set(["SERVER_NAME", "PATH_INFO", "SCRIPT_NAME"]), - set(part for part, _ in e.errors) + set(part for part, _ in e.errors), ) def test_afUnixSocket(self): @@ -1248,10 +1247,14 @@ def test_afUnixSocket(self): """ request = requestMock(b"/f\xc3\xb6\xc3\xb6") server_mock = Mock(Server) - server_mock.getRequestHostname = u'/var/run/twisted.socket' + server_mock.getRequestHostname = u"/var/run/twisted.socket" request.host = server_mock ( - url_scheme, server_name, server_port, path_info, script_name + url_scheme, + server_name, + server_port, + path_info, + script_name, ) = _extractURLparts(request) self.assertIsInstance(url_scheme, unicode) @@ -1275,19 +1278,18 @@ def test_global_app(self): self.assertIs(resource.__self__, globalApp) self.assertIs(handle_errors.__self__, globalApp) - @route('/') + @route("/") def index(request): 1 // 0 @handle_errors(ZeroDivisionError) def on_zero(request, failure): - return b'alive' + return b"alive" - request = requestMock(b'/') + request = requestMock(b"/") d = _render(resource(), request) self.assertIsNone(self.successResultOf(d)) - self.assertEqual(request.getWrittenData(), b'alive') - + self.assertEqual(request.getWrittenData(), b"alive") def test_weird_resource_situation(self): """ @@ -1309,14 +1311,18 @@ def test_weird_resource_situation(self): """ from klein import resource from klein.resource import KleinResource, ensure_utf8_bytes - self.assertEqual(repr(resource), - "") + + self.assertEqual( + repr(resource), "" + ) self.assertIdentical(resource.KleinResource, KleinResource) self.assertIdentical(resource.ensure_utf8_bytes, ensure_utf8_bytes) if _PY3: import sys + if sys.version_info >= (3, 5): from .py3_test_resource import PY3KleinResourceTests + PY3KleinResourceTests # shh pyflakes diff --git a/src/klein/test/test_response.py b/src/klein/test/test_response.py index 66336a97..f1fa6835 100644 --- a/src/klein/test/test_response.py +++ b/src/klein/test/test_response.py @@ -15,7 +15,6 @@ __all__ = () - class FrozenHTTPResponseTests(FrozenHTTPMessageTestsMixIn, TestCase): """ Tests for L{FrozenHTTPResponse}. @@ -25,18 +24,14 @@ class FrozenHTTPResponseTests(FrozenHTTPMessageTestsMixIn, TestCase): def responseFromBytes(data=b""): # type: (bytes) -> FrozenHTTPResponse return FrozenHTTPResponse( - status=200, - headers=FrozenHTTPHeaders(rawHeaders=()), - body=data, + status=200, headers=FrozenHTTPHeaders(rawHeaders=()), body=data, ) - @classmethod def messageFromBytes(cls, data=b""): # type: (bytes) -> IHTTPMessage return cls.responseFromBytes(data) - def test_interface(self): # type: () -> None """ @@ -45,7 +40,6 @@ def test_interface(self): response = self.responseFromBytes() self.assertProvides(IHTTPResponse, response) # type: ignore[misc] - def test_initInvalidBodyType(self): # type: () -> None """ diff --git a/src/klein/test/test_session.py b/src/klein/test/test_session.py index 9ecab3be..e68005d8 100644 --- a/src/klein/test/test_session.py +++ b/src/klein/test/test_session.py @@ -22,11 +22,11 @@ from twisted.python.components import Componentized from zope.interface.interfaces import IInterface from typing import Tuple, List + sessions = List[ISession] errors = List[NoSuchSession] - class ISimpleTest(Interface): """ Interface for testing. @@ -46,7 +46,6 @@ class IDenyMe(Interface): """ - @implementer(ISimpleTest) class SimpleTest(object): """ @@ -61,7 +60,6 @@ def doTest(self): return 3 - @declareMemoryAuthorizer(ISimpleTest) def memoryAuthorizer(interface, session, data): # type: (IInterface, ISession, Componentized) -> SimpleTest @@ -71,7 +69,6 @@ def memoryAuthorizer(interface, session, data): return SimpleTest() - def simpleSessionRouter(): # type: () -> Tuple[sessions, errors, str, str, StubTreq] """ @@ -83,8 +80,11 @@ def simpleSessionRouter(): router = Klein() token = "X-Test-Session-Token" cookie = "X-Test-Session-Cookie" - sproc = SessionProcurer(mss, secureTokenHeader=b"X-Test-Session-Token", - secureCookie=b"X-Test-Session-Cookie") + sproc = SessionProcurer( + mss, + secureTokenHeader=b"X-Test-Session-Token", + secureCookie=b"X-Test-Session-Cookie", + ) @router.route("/") @inlineCallbacks @@ -94,7 +94,7 @@ def route(request): sessions.append((yield sproc.procureSession(request))) except NoSuchSession as nss: exceptions.append(nss) - returnValue(b'ok') + returnValue(b"ok") requirer = Requirer() @@ -117,7 +117,6 @@ def testDenied(nope): return sessions, exceptions, token, cookie, treq - class ProcurementTests(SynchronousTestCase): """ Tests for L{klein.SessionProcurer}. @@ -140,23 +139,21 @@ def test_procurementSecurity(self): def route(request): # type: (IRequest) -> Deferred sproc = SessionProcurer(mss) + sessions.append((yield sproc.procureSession(request))) + sessions.append((yield sproc.procureSession(request))) sessions.append( - (yield sproc.procureSession(request))) - sessions.append( - (yield sproc.procureSession(request))) - sessions.append( - (yield sproc.procureSession(request, forceInsecure=True))) - returnValue(b'sessioned') + (yield sproc.procureSession(request, forceInsecure=True)) + ) + returnValue(b"sessioned") treq = StubTreq(router.resource()) - self.successResultOf(treq.get('http://unittest.example.com/')) + self.successResultOf(treq.get("http://unittest.example.com/")) self.assertIs(sessions[0], sessions[1]) self.assertIs(sessions[0], sessions[2]) - self.successResultOf(treq.get('https://unittest.example.com/')) + self.successResultOf(treq.get("https://unittest.example.com/")) self.assertIs(sessions[3], sessions[4]) self.assertIsNot(sessions[3], sessions[5]) - def test_procuredTooLate(self): # type: () -> None """ @@ -179,9 +176,8 @@ def route(request): request.finish() treq = StubTreq(router.resource()) - result = self.successResultOf(treq.get('http://unittest.example.com/')) - self.assertEqual(self.successResultOf(result.content()), b'oops...bye') - + result = self.successResultOf(treq.get("http://unittest.example.com/")) + self.assertEqual(self.successResultOf(result.content()), b"oops...bye") def test_cookiesTurnedOff(self): # type: () -> None @@ -199,12 +195,11 @@ def route(request): sproc = SessionProcurer(mss, setCookieOnGET=False) with self.assertRaises(NoSuchSession): yield sproc.procureSession(request) - returnValue(b'no session') + returnValue(b"no session") treq = StubTreq(router.resource()) - result = self.successResultOf(treq.get('http://unittest.example.com/')) - self.assertEqual(self.successResultOf(result.content()), b'no session') - + result = self.successResultOf(treq.get("http://unittest.example.com/")) + self.assertEqual(self.successResultOf(result.content()), b"no session") def test_unknownSessionHeader(self): # type: () -> None @@ -215,13 +210,12 @@ def test_unknownSessionHeader(self): sessions, exceptions, token, cookie, treq = simpleSessionRouter() response = self.successResultOf( - treq.get('https://unittest.example.com/', headers={token: u"bad"}) + treq.get("https://unittest.example.com/", headers={token: u"bad"}) ) self.assertEqual(response.code, 200) self.assertEqual(len(sessions), 0) self.assertEqual(len(exceptions), 1) - def test_unknownSessionCookieGET(self): # type: () -> None """ @@ -230,15 +224,16 @@ def test_unknownSessionCookieGET(self): """ badSessionID = "bad" sessions, exceptions, token, cookie, treq = simpleSessionRouter() - response = self.successResultOf(treq.get( - 'https://unittest.example.com/', cookies={cookie: badSessionID} - )) + response = self.successResultOf( + treq.get( + "https://unittest.example.com/", cookies={cookie: badSessionID} + ) + ) self.assertEqual(response.code, 200) self.assertEqual(len(exceptions), 0) self.assertEqual(len(sessions), 1) self.assertNotEqual(sessions[0].identifier, badSessionID) - def test_unknownSessionCookiePOST(self): # type: () -> None """ @@ -247,14 +242,15 @@ def test_unknownSessionCookiePOST(self): """ badSessionID = "bad" sessions, exceptions, token, cookie, treq = simpleSessionRouter() - response = self.successResultOf(treq.post( - 'https://unittest.example.com/', cookies={cookie: badSessionID} - )) + response = self.successResultOf( + treq.post( + "https://unittest.example.com/", cookies={cookie: badSessionID} + ) + ) self.assertEqual(response.code, 200) self.assertEqual(len(exceptions), 1) self.assertEqual(len(sessions), 0) - def test_authorization(self): # type: () -> None """ @@ -262,12 +258,11 @@ def test_authorization(self): knows how to supply that authorization, it is passed to the object. """ sessions, exceptions, token, cookie, treq = simpleSessionRouter() - response = self.successResultOf(treq.get( - 'https://unittest.example.com/test' - )) + response = self.successResultOf( + treq.get("https://unittest.example.com/test") + ) self.assertEqual(self.successResultOf(response.content()), b"ok: 7") - def test_authorizationDenied(self): # type: () -> None """ @@ -276,8 +271,10 @@ def test_authorizationDenied(self): is not invoked. """ sessions, exceptions, token, cookie, treq = simpleSessionRouter() - response = self.successResultOf(treq.get( - 'https://unittest.example.com/denied' - )) - self.assertEqual(self.successResultOf(response.content()), - b'klein.test.test_session.IDenyMe DENIED') + response = self.successResultOf( + treq.get("https://unittest.example.com/denied") + ) + self.assertEqual( + self.successResultOf(response.content()), + b"klein.test.test_session.IDenyMe DENIED", + ) diff --git a/src/klein/test/test_trial.py b/src/klein/test/test_trial.py index 994bc660..8d433d29 100644 --- a/src/klein/test/test_trial.py +++ b/src/klein/test/test_trial.py @@ -13,7 +13,6 @@ __all__ = () - class TestCaseTests(TestCase): """ Tests for L{TestCase}. @@ -23,6 +22,7 @@ class IFrobbable(Interface): """ Frobbable object. """ + @ifmethod def frob(): # type: () -> None @@ -30,24 +30,22 @@ def frob(): Frob the object. """ - @implementer(IFrobbable) class Frobbable(object): """ Implements L{IFrobbable}. """ + def frob(self): # type: () -> None pass - @implementer(IFrobbable) class NotFrobbable(object): """ Does not implement L{IFrobbable}, despite declaring. """ - def test_assertProvidesPass(self): # type: () -> None """ @@ -58,7 +56,6 @@ def test_assertProvidesPass(self): self.assertProvides(self.IFrobbable, frobbable) frobbable.frob() # Coverage - def test_assertProvidesFail(self): # type: () -> None """ @@ -67,5 +64,7 @@ def test_assertProvidesFail(self): """ self.assertRaises( self.failureException, - self.assertProvides, self.IFrobbable, self.NotFrobbable(), + self.assertProvides, + self.IFrobbable, + self.NotFrobbable(), ) diff --git a/src/klein/test/util.py b/src/klein/test/util.py index 6dfeee95..b31ead7c 100644 --- a/src/klein/test/util.py +++ b/src/klein/test/util.py @@ -3,11 +3,11 @@ """ - class EqualityTestsMixin(object): """ A mixin defining tests for the standard implementation of C{==} and C{!=}. """ + def anInstance(self): """ Return an instance of the class under test. Each call to this method @@ -16,7 +16,6 @@ def anInstance(self): """ raise NotImplementedError() - def anotherInstance(self): """ Return an instance of the class under test. Each call to this method @@ -26,7 +25,6 @@ def anotherInstance(self): """ raise NotImplementedError() - def test_identicalEq(self): """ An object compares equal to itself using the C{==} operator. @@ -34,7 +32,6 @@ def test_identicalEq(self): o = self.anInstance() self.assertTrue(o == o) - def test_identicalNe(self): """ An object doesn't compare not equal to itself using the C{!=} operator. @@ -42,7 +39,6 @@ def test_identicalNe(self): o = self.anInstance() self.assertFalse(o != o) - def test_sameEq(self): """ Two objects that are equal to each other compare equal to each other @@ -52,7 +48,6 @@ def test_sameEq(self): b = self.anInstance() self.assertTrue(a == b) - def test_sameNe(self): """ Two objects that are equal to each other do not compare not equal to @@ -62,7 +57,6 @@ def test_sameNe(self): b = self.anInstance() self.assertFalse(a != b) - def test_differentEq(self): """ Two objects that are not equal to each other do not compare equal to @@ -72,7 +66,6 @@ def test_differentEq(self): b = self.anotherInstance() self.assertFalse(a == b) - def test_differentNe(self): """ Two objects that are not equal to each other compare not equal to each @@ -82,7 +75,6 @@ def test_differentNe(self): b = self.anotherInstance() self.assertTrue(a != b) - def test_anotherTypeEq(self): """ The object does not compare equal to an object of an unrelated type @@ -92,7 +84,6 @@ def test_anotherTypeEq(self): b = object() self.assertFalse(a == b) - def test_anotherTypeNe(self): """ The object compares not equal to an object of an unrelated type (which @@ -102,12 +93,12 @@ def test_anotherTypeNe(self): b = object() self.assertTrue(a != b) - def test_delegatedEq(self): """ The result of comparison using C{==} is delegated to the right-hand operand if it is of an unrelated type. """ + class Delegate(object): def __eq__(self, other): # Do something crazy and obvious. @@ -117,12 +108,12 @@ def __eq__(self, other): b = Delegate() self.assertEqual(a == b, [b]) - def test_delegateNe(self): """ The result of comparison using C{!=} is delegated to the right-hand operand if it is of an unrelated type. """ + class Delegate(object): def __ne__(self, other): # Do something crazy and obvious. diff --git a/tox.ini b/tox.ini index 37bcc9a9..bfd372af 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,23 @@ [tox] envlist = - flake8 - mypy - + flake8, black, mypy coverage-py{27,35,36,37,38,py2,py3}-tw{171,184,current,trunk} - + coverage_report docs, docs-linkcheck + packaging + +skip_missing_interpreters = {tty:True:False} + + +[default] + +basepython = python3.8 -skip_missing_interpreters = {env:TOX_SKIP_MISSING_INTERPRETERS:True} +setenv = + PY_MODULE=klein + + PYTHONPYCACHEPREFIX={envtmpdir}/pycache ## @@ -17,6 +26,8 @@ skip_missing_interpreters = {env:TOX_SKIP_MISSING_INTERPRETERS:True} [testenv] +description = run tests + basepython = py27: python2.7 py35: python3.5 @@ -55,32 +66,56 @@ 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 - {trial,coverage}-py{27,py2}: mock==3.0.5 - {trial,coverage}-py{27,py2}: typing==3.7.4.1 + {test,coverage}: treq==18.6.0 + {test,coverage}: hypothesis==4.41.2 + {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: coverage==4.5.4 -passenv = - # See https://github.com/codecov/codecov-python/blob/5b9d539a6a09bc84501b381b563956295478651a/README.md#using-tox - codecov: TOXENV - codecov: CI - codecov: TRAVIS TRAVIS_* - setenv = - PIP_DISABLE_PIP_VERSION_CHECK=1 - VIRTUALENV_NO_DOWNLOAD=1 + {[default]setenv} -commands = - "{toxinidir}/.travis/environment" + coverage: COVERAGE_FILE={toxworkdir}/coverage.{envname} +commands = # Run trial without coverage - trial: trial --random=0 --logfile="{envlogdir}/trial.log" --temp-directory="{envlogdir}/trial.d" {posargs:klein} + test: trial --random=0 --logfile="{envlogdir}/trial.log" --temp-directory="{envlogdir}/trial.d" {posargs:{env:PY_MODULE}} # Run trial with coverage - coverage: coverage run -p "{envbindir}/trial" --random=0 --logfile="{envlogdir}/trial.log" --temp-directory="{envlogdir}/trial.d" {posargs:klein} + coverage: coverage run --source {env:PY_MODULE} "{envdir}/bin/trial" --random=0 --logfile="{envlogdir}/trial.log" --temp-directory="{envlogdir}/trial.d" {posargs:{env:PY_MODULE}} + + # Run coverage reports, ignore exit status + coverage: - coverage report --skip-covered + + +## +# Black code formatting +## + +[testenv:black] + +description = run Black (linter) + +basepython = {[default]basepython} + +skip_install = True + +deps = + black==19.10b0 + +commands = + black {env:BLACK_LINT_ARGS:} src + + +[testenv:black-reformat] + +description = {[testenv:black]description} and reformat +basepython = {[testenv:black]basepython} +skip_install = {[testenv:black]skip_install} +deps = {[testenv:black]deps} +commands = {[testenv:black]commands} ## @@ -89,51 +124,96 @@ commands = [testenv:flake8] +description = run Flake8 (linter) + +basepython = {[default]basepython} + skip_install = True deps = - flake8==3.7.9 flake8-bugbear==19.8.0 flake8-docstrings==1.5.0 flake8-import-order==0.18.1 flake8-mutable==1.2.0 flake8-pep3101==1.2.1 + flake8==3.7.9 mccabe==0.6.1 - pep8-naming==0.8.2 + pep8-naming==0.9.0 pycodestyle==2.5.0 pydocstyle==4.0.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.8 - commands = - "{toxinidir}/.travis/environment" - - flake8 {posargs:src/klein} + flake8 {posargs:src/{env:PY_MODULE}} [flake8] -select = B,C,E,F,I,N,S,W +# !!! BRING THE PAIN !!! +select = A,B,B9,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z + +show-source = True +doctests = True -disable-noqa = True -show-source = True -doctests = True +max-line-length = 80 # Codes: http://flake8.pycqa.org/en/latest/user/error-codes.html ignore = - # multiple spaces before operator - E221, + ######## WARNINGS BELOW SHOULD BE FIXED ######## + + # Missing docstring in public module + D100, + + # Missing docstring in public class + D101, + + # Missing docstring in public method + D102, - # too many blank lines - E302, + # Missing docstring in public function + D103, - # too many blank lines - E303, + # Missing docstring in public package + D104, - # expected 2 blank lines after class or function definition - E305, + # Missing docstring in magic method + D105, + + # Missing docstring in __init__ + D107, + + # Use """triple double quotes""" + D300, + + # First word of the first line should be properly capitalized + D403, + + # Additional newline in a group of imports + I202, + + ######## WARNINGS ABOVE SHOULD BE FIXED ######## + + # Invalid first argument used for instance method + B902, + + # One-line docstring should fit on one line with quotes + D200, + + # No blank lines allowed after function docstring + D202, + + # 1 blank line required between summary line and description + D205, + + # First line should end with a period + D400, + + # First line should be in imperative mood + D401, + + # missing whitespace after ',' + E231, # function name should be lowercase N802, @@ -150,14 +230,17 @@ ignore = # lowercase imported as non lowercase N812, - # variable 'rawHeaders' in class scope should not be mixedCase + # variable in class scope should not be mixedCase N815, - # variable 'noneIO' in global scope should not be mixedCase + # variable in global scope should not be mixedCase N816, - # line break after binary operator (W503 and W504 are opposites) - W504, + # line break before binary operator + W503, + + # End of list (allows last item to end with trailing ',') + EOL # flake8-import-order: local module name space application-import-names = klein @@ -174,17 +257,13 @@ max-complexity = 60 description = run Mypy (static type checker) -basepython = python3.8 - -skip_install = True +basepython = {[default]basepython} deps = mypy==0.740 mypy_extensions==0.4.3 commands = - "{toxinidir}/.travis/environment" - mypy \ --config-file="{toxinidir}/tox.ini" \ --cache-dir="{toxworkdir}/mypy_cache" \ @@ -283,75 +362,96 @@ ignore_missing_imports = True ## -# Run twistedchecker +# Coverage report ## -[testenv:twistedchecker] - -deps = - twistedchecker==0.7.2 - -basepython = python2.7 - -commands = - "{toxinidir}/.travis/environment" +[testenv:coverage_report] - twistedchecker {posargs:klein} +description = generate coverage report +depends = {test,coverage}-py{27,35,36,37,38,py2,py3}-tw{171,184,current,trunk} -## -# Run twistedchecker on changes relative to master -## +basepython = {[default]basepython} -[testenv:twistedchecker-diff] +skip_install = True deps = - {[testenv:twistedchecker]deps} - diff_cover==2.4.0 + coverage==4.5.4 -basepython = python2.7 +setenv = + {[default]setenv} -commands = - "{toxinidir}/.travis/environment" + COVERAGE_FILE={toxworkdir}/coverage - "{toxinidir}/.travis/twistedchecker-diff" {posargs:klein} +commands = + coverage combine + - coverage report + - coverage html ## -# Publish to Codecov +# Codecov ## [testenv:codecov] +description = upload coverage to Codecov + +depends = {[coverage_report]depends} + +basepython = python + skip_install = True deps = + {[testenv:coverage_report]deps} codecov==2.0.15 -commands = - "{toxinidir}/.travis/environment" +passenv = + # See https://github.com/codecov/codecov-python/blob/master/README.md#using-tox + # And CI-specific docs: + # https://help.github.com/en/articles/virtual-environments-for-github-actions#default-environment-variables + # https://docs.travis-ci.com/user/environment-variables#default-environment-variables + # https://www.appveyor.com/docs/environment-variables/ + TOXENV CODECOV_* CI + GITHUB_* + TRAVIS TRAVIS_* + APPVEYOR APPVEYOR_* + +setenv = + {[testenv:coverage_report]setenv} - coverage combine --append + COVERAGE_XML={envlogdir}/coverage_report.xml - codecov -e TOXENV --required +commands = + # Note documentation for CI variables in passenv above + coverage combine + coverage xml --ignore-errors -o "{env:COVERAGE_XML}" + codecov --file="{env:COVERAGE_XML}" --env \ + GITHUB_REF GITHUB_COMMIT GITHUB_USER GITHUB_WORKFLOW \ + TRAVIS_BRANCH TRAVIS_BUILD_WEB_URL \ + TRAVIS_COMMIT TRAVIS_COMMIT_MESSAGE \ + APPVEYOR_REPO_BRANCH APPVEYOR_REPO_COMMIT \ + APPVEYOR_REPO_COMMIT_AUTHOR_EMAIL \ + APPVEYOR_REPO_COMMIT_MESSAGE_EXTENDED ## -# Build the documentation +# Documentation ## [testenv:docs] +description = build documentation + +basepython = {[default]basepython} + deps = # Updating to Sphinx 2.x fails / needs work sphinx==1.8.5 sphinx_rtd_theme==0.4.3 -basepython = python2.7 - commands = - "{toxinidir}/.travis/environment" - sphinx-build -b html -d "{envtmpdir}/doctrees" docs docs/_build/html @@ -361,12 +461,53 @@ commands = [testenv:docs-linkcheck] -deps = {[testenv:docs]deps} +description = check for broken links in documentation -basepython = python2.7 +basepython = {[default]basepython} -commands = - "{toxinidir}/.travis/environment" +deps = {[testenv:docs]deps} +commands = sphinx-build -b html -d "{envtmpdir}/doctrees" docs docs/_build/html sphinx-build -b linkcheck docs docs/_build/html + + +## +# Packaging +## + +[testenv:packaging] + +description = check for potential packaging problems + +basepython = {[default]basepython} + +skip_install = True + +deps = + check-manifest==0.40 + readme_renderer==24.0 + twine==2.0.0 + +commands = + check-manifest + pip wheel --wheel-dir "{envtmpdir}/dist" --no-deps {toxinidir} + twine check "{envtmpdir}/dist/"* + + +## +# Print dependencies +## + +[testenv:dependencies] + +description = print dependencies + +basepython = {[default]basepython} + +recreate = true + +deps = + +commands = + pip freeze