Skip to content

Commit

Permalink
Ship 0.1.10
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxim Avanov committed Apr 12, 2014
2 parents 7ae4325 + abdd56d commit bad835c
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 16 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1.9'
release = '0.1.10'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
23 changes: 9 additions & 14 deletions rhetoric/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from rhetoric.config.rendering import BUILTIN_RENDERERS
from rhetoric.config.routes import RoutesConfiguratorMixin
from rhetoric.config.views import ViewsConfiguratorMixin
from rhetoric.config.adt import ADTConfiguratorMixin
from rhetoric.path import caller_package
from rhetoric.exceptions import ConfigurationError
from rhetoric.view import view_config
Expand All @@ -21,7 +22,8 @@
class Configurator(
RoutesConfiguratorMixin,
ViewsConfiguratorMixin,
RenderingConfiguratorMixin):
RenderingConfiguratorMixin,
ADTConfiguratorMixin):

venusian = venusian
inspect = inspect
Expand All @@ -34,6 +36,9 @@ def __init__(self, route_prefix=None, api_version_getter=None):

self.routes = OrderedDict()
self.renderers = {}
# ADT extension
# -------------
self.adt = {}

self.setup_registry()

Expand Down Expand Up @@ -85,26 +90,16 @@ def scan(self, package=None, categories=None, onerror=None, ignore=None):

scanner = self.venusian.Scanner(config=self)
scanner.scan(package, categories=categories, onerror=onerror, ignore=ignore)
self.check_consistency()
self.check_routes_consistency()
self.check_adt_consistency()

def setup_registry(self):
# Add default renderers
# ---------------------
for name, renderer in BUILTIN_RENDERERS.items():
self.add_renderer(name, renderer)

def check_consistency(self):
for route_name, route in self.routes.items():
viewlist = route['viewlist']
if not viewlist:
raise ConfigurationError(
'Route name "{name}" is not associated with a view callable.'.format(name=route_name)
)
for route_item in viewlist:
if route_item.get('view') is None:
raise ConfigurationError(
'Route name "{name}" is not associated with a view callable.'.format(name=route_name)
)


def maybe_dotted(self, dotted):
if not isinstance(dotted, str):
Expand Down
108 changes: 108 additions & 0 deletions rhetoric/adt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import re
import venusian


ADT_VARIANT_NAME_RE = re.compile('[A-Z][0-9A-Z_]*')


class ADTMeta(type):
def __new__(mcs, class_name, bases, attrs):
cls = type.__new__(mcs, class_name, bases, attrs)
variants = set([name for name in attrs if ADT_VARIANT_NAME_RE.match(name)])
values = {attrs[var_name]: var_name for var_name in variants}
setattr(cls, '__adt__', {
'type': str(cls),
# set of adt variants
'variants': variants,
# dict of value => variant mappings
'values': values,
'cases': {},
# dict of value => ADTMatch instances.
# Used by .match() for O(1) result retrieval
'matches': {}
})
return cls


class adt(object):
__metaclass__ = ADTMeta
venusian = venusian

class Mismatch(Exception):
pass

@classmethod
def values(cls):
return set(cls.__adt__['values'].keys())

@classmethod
def match(cls, value):
"""
:rtype: :class:`rhetoric.adt.ADTMatch`
"""
try:
variant = cls.__adt__['values'][value]
except KeyError:
raise cls.Mismatch(
u'Variant value "{value}" is not a part of the type {type}: {values}'.format(
value=value,
type=cls.__adt__['type'],
values=u', '.join(['{val} => {var}'.format(val=val, var=var)
for val, var in cls.__adt__['values'].items()])
)
)

return cls.__adt__['matches'][variant]


def __init__(self, variant, case):
"""
:type variant: str
:type case: str
"""
cases_dict = self.__adt__['cases']
variants = self.__adt__['variants']

if variant not in variants:
raise TypeError(
'Variant {variant} does not belong to the type {type}'.format(
variant=variant,
type=self.__adt__['type']
)
)
cases_dict.setdefault(case, {v: None for v in variants})
self.variant = variant
self.case = case

def __call__(self, wrapped):

def callback(scanner, name, obj):
cases_dict = self.__adt__['cases']
case_implementations = cases_dict[self.case]
if case_implementations[self.variant] is not None:
raise TypeError(
'Variant {variant} of {type} is already bound to the case {case}: {impl}'.format(
variant=self.variant,
type=self.__adt__['type'],
case=self.case,
impl=str(obj)
)
)
case_implementations[self.variant] = obj
# Re-calculate matches
self.__adt__['matches'] = {
variant: ADTMatch({case: impl[variant] for case, impl in cases_dict.items()})
for variant in self.__adt__['variants']
}
scanner.config.update_adt_registry(self.__adt__)

info = self.venusian.attach(wrapped, callback, category='rhetoric')
return wrapped


class ADTMatch(object):
def __init__(self, attrs):
"""
:type attrs: dict
"""
self.__dict__.update(attrs)
31 changes: 31 additions & 0 deletions rhetoric/config/adt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
""" ADT stands for Algebraic data type
"""
from rhetoric.exceptions import ConfigurationError


class ADTConfiguratorMixin(object):

def update_adt_registry(self, adt_meta):
"""
:type adt_meta: dict
"""
adt_type = adt_meta['type']
self.adt[adt_type] = adt_meta

def check_adt_consistency(self):
for obj_id, adt_meta in self.adt.items():
for case_name, case_meta in adt_meta['cases'].items():
for variant, implementation in case_meta.items():
if implementation is None:
raise ConfigurationError(
'Case {case_name} of {type} is not exhaustive. '
'Here is the variant that is not matched: {variant} '
.format(
case_name=case_name,
type=str(adt_meta['type']),
variant=variant
)
)
# All good. We no longer need the adt meta.
delattr(self, 'adt')
15 changes: 15 additions & 0 deletions rhetoric/config/routes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from rhetoric.exceptions import ConfigurationError


class RoutesConfiguratorMixin(object):
def add_route(self, name, pattern, rules=None, extra_kwargs=None):
Expand All @@ -9,3 +11,16 @@ def add_route(self, name, pattern, rules=None, extra_kwargs=None):
'extra_kwargs': extra_kwargs,
'viewlist': [],
}

def check_routes_consistency(self):
for route_name, route in self.routes.items():
viewlist = route['viewlist']
if not viewlist:
raise ConfigurationError(
'Route name "{name}" is not associated with a view callable.'.format(name=route_name)
)
for route_item in viewlist:
if route_item.get('view') is None:
raise ConfigurationError(
'Route name "{name}" is not associated with a view callable.'.format(name=route_name)
)
4 changes: 4 additions & 0 deletions rhetoric/url.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re

from rhetoric.view import RegexURLPattern
from rhetoric.adt import ADTMeta
from django.core.urlresolvers import reverse


Expand Down Expand Up @@ -94,6 +95,9 @@ def create_django_route(name, pattern, rules=None, extra_kwargs=None, viewlist=N
if not rule:
# This default pattern is Pyramid's default.
rule = '[^/]+'
elif isinstance(rule, ADTMeta):
# rule is an ADT
rule = u'(?:{})'.format(u'|'.join(rule.values()))

result = u"(?P<{match_group_name}>{rule})".format(
match_group_name=match_group_name,
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def run_tests(self):

setup(
name='Rhetoric',
version='0.1.9',
version='0.1.10',
packages=find_packages(exclude=['tests']),
install_requires=[
'Django>=1.4',
Expand Down

0 comments on commit bad835c

Please sign in to comment.