Skip to content

Commit

Permalink
Merge 9fc1d55 into 06769dd
Browse files Browse the repository at this point in the history
  • Loading branch information
aodag committed Jan 28, 2018
2 parents 06769dd + 9fc1d55 commit fd8598c
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 83 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -23,4 +23,5 @@ env27
*,cover
htmlcov
venv
.cache
.cache
.mypy_cache
3 changes: 2 additions & 1 deletion .travis.yml
@@ -1,7 +1,6 @@
sudo: false
language: python
python:
- pypy
- 3.4
- 3.5
- 3.6
Expand All @@ -17,5 +16,7 @@ jobs:
env: TOXENV=flake8
- python: 3.6
env: TOXENV=pylint
- python: 3.6
env: TOXENV=mypy
after_success:
- if test "$TOXENV" = coverage ; then pip install coveralls; coveralls ; fi
1 change: 0 additions & 1 deletion CHANGES.txt
@@ -1,7 +1,6 @@
Changes
=========================


1.3
-------------------------

Expand Down
2 changes: 2 additions & 0 deletions newsfragments/13.feature
@@ -0,0 +1,2 @@
support mypy typings

9 changes: 7 additions & 2 deletions setup.py
Expand Up @@ -10,18 +10,21 @@
"python-coveralls",
"flake8",
"pylint",
"mypy",
]

here = os.path.dirname(__file__)
readme = None
changes = None


def _read(name):
try:
return open(os.path.join(here, name)).read()
except:
except Exception:
return ""


readme = _read("README.rst")
changes = _read("CHANGES.txt")

Expand All @@ -39,7 +42,9 @@ def _read(name):
long_description=readme + "\n" + changes,
test_suite="webdispatch",
license="MIT",
install_requires=[],
install_requires=[
"typing; python_version < '3.5'",
],
tests_require=tests_require,
extras_require={
"testing": tests_require,
Expand Down
8 changes: 7 additions & 1 deletion tox.ini
@@ -1,5 +1,5 @@
[tox]
envlist = py34,py35,py36,pypy,coverage,flake8,pylint
envlist = py34,py35,py36,coverage,flake8,pylint,mypy

[testenv]
commands =
Expand All @@ -23,3 +23,9 @@ basepython = python3.6
commands =
pip install -e .[testing] -c constraints.txt
pylint webdispatch

[testenv:mypy]
basepython = python3.6
commands =
pip install -e .[testing] -c constraints.txt
mypy --ignore-missing-imports webdispatch
27 changes: 19 additions & 8 deletions webdispatch/base.py
@@ -1,22 +1,26 @@
""" base dispatchers
"""
from typing import Dict, Any, Callable, Iterable


class DispatchBase(object):
""" Base class for dispatcher application"""

def __init__(self, applications=None, extra_environ=None):
def __init__(
self,
applications: Dict[str, Callable] = None,
extra_environ: Dict[str, Any] = None) -> None:

if applications is None:
self.applications = {}
self.applications = {} # type: Dict[str, Callable]
else:
self.applications = applications
if extra_environ is None:
self.extra_environ = {}
self.extra_environ = {} # type: Dict[str, Any]
else:
self.extra_environ = extra_environ

def register_app(self, name, app=None):
def register_app(self, name: str, app: Callable = None) -> Callable:
""" register dispatchable wsgi application"""
if app is None:
def dec(app):
Expand All @@ -26,20 +30,27 @@ def dec(app):
return app
return dec
self.applications[name] = app
return None

def get_extra_environ(self):
def get_extra_environ(self) -> Dict[str, Any]:
""" returns for environ values for wsgi environ"""
return self.extra_environ

def detect_view_name(self, environ): # pragma: nocover
def detect_view_name(
self, environ: Dict[str, Any]) -> str: # pragma: nocover
""" must returns view name for request """
raise NotImplementedError()

def on_view_not_found(self, environ, start_response): # pragma: nocover
def on_view_not_found(
self,
environ: Dict[str, Any],
start_response: Callable) -> Iterable[bytes]: # pragma: nocover
""" called when view is not found"""
raise NotImplementedError()

def __call__(self, environ, start_response):
def __call__(self,
environ: Dict[str, Any],
start_response: Callable) -> Iterable[bytes]:
extra_environ = self.get_extra_environ()
environ.update(extra_environ)
view_name = self.detect_view_name(environ)
Expand Down
24 changes: 15 additions & 9 deletions webdispatch/methoddispatcher.py
@@ -1,24 +1,27 @@
""" methoddispatcher
"""

from typing import Dict, Any, Iterable, Callable, List, Tuple
from wsgiref.util import application_uri
from .base import DispatchBase


class MethodDispatcher(DispatchBase):
""" dispatch applications with request method.
"""
def __init__(self, **kwargs):
def __init__(self, **kwargs) -> None:
super(MethodDispatcher, self).__init__()
for name, app in kwargs.items():
self.register_app(name, app)

def detect_view_name(self, environ):
def detect_view_name(self, environ: Dict[str, Any]) -> str:
""" convert request method to view name """
return environ['REQUEST_METHOD'].lower()

def on_view_not_found(self, _, start_response):
def on_view_not_found(
self, _,
start_response: Callable[[str, List[Tuple[str, str]]], None],
) -> Iterable[bytes]:
""" called when valid view is not found """

start_response(
Expand All @@ -27,7 +30,7 @@ def on_view_not_found(self, _, start_response):
return [b"Method Not Allowed"]


def action_handler_adapter(handler_cls, action_name):
def action_handler_adapter(handler_cls: type, action_name: str) -> Callable:
""" wraps class to wsgi application dispathing action"""

if not hasattr(handler_cls(), action_name):
Expand All @@ -44,24 +47,27 @@ def wsgiapp(environ, start_response):
class ActionDispatcher(DispatchBase):
""" wsgi application dispatching actions to registered classes"""

def __init__(self, action_var_name='action'):
def __init__(self, action_var_name: str = 'action') -> None:
super(ActionDispatcher, self).__init__()
self.action_var_name = action_var_name

def register_actionhandler(self, action_handler):
def register_actionhandler(self, action_handler: type) -> None:
""" register class as action handler """
for k in action_handler.__dict__:
if k.startswith('_'):
continue
app = action_handler_adapter(action_handler, k)
self.register_app(k, app)

def detect_view_name(self, environ):
def detect_view_name(self, environ: Dict[str, Any]) -> str:
""" get view name from routing args """
urlvars = environ.get('wsgiorg.routing_args', [(), {}])[1]
return urlvars.get(self.action_var_name)

def on_view_not_found(self, environ, start_response):
def on_view_not_found(
self, environ: Dict[str, Any],
start_response: Callable[[str, List[Tuple[str, str]]], None],
) -> Iterable[bytes]:
""" called when action is not found """
start_response(
"404 Not Found",
Expand Down
8 changes: 6 additions & 2 deletions webdispatch/mixins.py
@@ -1,15 +1,19 @@
""" useful mixin classes
"""
from typing import Dict, Any # noqa pylint: disable=unused-import
from .urldispatcher import URLGenerator


class URLMapperMixin(object):
""" mixin to add :meth:`generate_url` method.
"""
def generate_url(self, name, **kwargs):
environ = {} # type: Dict[str, Any]

def generate_url(self, name: str, **kwargs) -> str:
""" generate url with urlgenerator used by urldispatch"""
return self.urlmapper.generate(name, **kwargs)

@property
def urlmapper(self):
def urlmapper(self) -> URLGenerator:
""" get urlmapper object from wsgi environ """
return self.environ['webdispatch.urlgenerator']
20 changes: 10 additions & 10 deletions webdispatch/tests/test_uritemplate.py
Expand Up @@ -127,17 +127,17 @@ def test_match_empty(self):

result = target.match(path)

compare(result["matchdict"], dict())
compare(result["matchlength"], 0)
compare(result.matchdict, dict())
compare(result.matchlength, 0)

def test_wildcard(self):
""" test matching pattern including wildcard"""
path = "hoge/{var1}/*"
target = self._make_one(path)
result = target.match("hoge/egg/bacon")

compare(result["matchdict"], dict(var1="egg"))
compare(result["matchlength"], 9)
compare(result.matchdict, dict(var1="egg"))
compare(result.matchlength, 9)

def test_match_no_match(self):
""" test no mathing"""
Expand All @@ -153,32 +153,32 @@ def test_match_match_one(self):
target = self._make_one(path)
result = target.match("a")

compare(result["matchdict"], dict(var1="a"))
compare(result["matchlength"], 1)
compare(result.matchdict, dict(var1="a"))
compare(result.matchlength, 1)

def test_match_match_complex_word(self):
""" test matching a string"""
path = "{var1}"
target = self._make_one(path)
result = target.match("abc")

compare(result["matchdict"], dict(var1="abc"))
compare(result.matchdict, dict(var1="abc"))

def test_match_match_many(self):
""" test matching pattern including two vars """
path = "{var1}/users/{var2}"
target = self._make_one(path)
result = target.match("a/users/egg")

compare(result["matchdict"], dict(var1="a", var2="egg"))
compare(result.matchdict, dict(var1="a", var2="egg"))

def test_match_conveter(self):
""" test matching pattern including specified converter """
path = "{var1:int}/users/{var2}"
target = self._make_one(path)
result = target.match("1/users/egg")

compare(result["matchdict"], dict(var1=1, var2="egg"))
compare(result.matchdict, dict(var1=1, var2="egg"))

def test_match_conveter_error(self):
""" test matching pattern including specified converter """
Expand All @@ -198,7 +198,7 @@ def test_match_custom_conveter(self):
target = self._make_one(path, converters=converters)
result = target.match("1/users/20140420")

compare(result["matchdict"],
compare(result.matchdict,
dict(var1="1", var2=datetime(2014, 4, 20)))

def test_substitue(self):
Expand Down
8 changes: 5 additions & 3 deletions webdispatch/tests/test_urldispatcher.py
Expand Up @@ -60,12 +60,14 @@ def test_lookup_none(self):

def test_lookup(self):
""" test looking up basic usage """
from webdispatch.uritemplate import MatchResult
target = self._make_one()
target.add('testing-route', 'a')
result = target.lookup('a')
compare(result, {'name': 'testing-route',
'matchdict': {},
'matchlength': 1})
compare(result, C(MatchResult,
name='testing-route',
matchdict={},
matchlength=1))

def test_generate(self):
""" test generating url """
Expand Down

0 comments on commit fd8598c

Please sign in to comment.