Skip to content

Commit

Permalink
Add DependenciesList to track ordered and depending entries like Appl…
Browse files Browse the repository at this point in the history
…icationWrappers, will also provide foundation for future configuration steps registration
  • Loading branch information
amol- committed Feb 25, 2016
1 parent 985a8d3 commit 19ac23e
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 40 deletions.
7 changes: 4 additions & 3 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from tg.appwrappers.i18n import I18NApplicationWrapper
from tg.appwrappers.identity import IdentityApplicationWrapper
from tg.appwrappers.session import SessionApplicationWrapper
from tg.configuration.utils import DependenciesList

try:
from xmlrpclib import loads, dumps
Expand Down Expand Up @@ -47,17 +48,17 @@ def make_app(controller_klass=None, environ=None, config_options=None, with_erro
tg.config['rendering_engines_options'] = default_config['rendering_engines_options']

config = default_config.copy()
config['application_wrappers'] = [
config['application_wrappers'] = DependenciesList(
I18NApplicationWrapper,
IdentityApplicationWrapper,
CacheApplicationWrapper,
SessionApplicationWrapper
]
)

if with_errors:
config['errorpage.enabled'] = True
config['errorpage.status_codes'] = [403, 404]
config['application_wrappers'].append(ErrorPageApplicationWrapper)
config['application_wrappers'].add(ErrorPageApplicationWrapper)

config['session.enabled'] = True
config['session.data_dir'] = session_dir
Expand Down
11 changes: 6 additions & 5 deletions tests/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1328,11 +1328,12 @@ class AppWrapper5:
conf.register_wrapper(AppWrapper5, after=AppWrapper3)
milestones.environment_loaded.reach()

assert conf.application_wrappers[0] == AppWrapper1
assert conf.application_wrappers[1] == AppWrapper2
assert conf.application_wrappers[2] == AppWrapper3
assert conf.application_wrappers[3] == AppWrapper4
assert conf.application_wrappers[4] == AppWrapper5
app_wrappers = list(conf.application_wrappers.values())
assert app_wrappers[0] == AppWrapper1
assert app_wrappers[1] == AppWrapper2
assert app_wrappers[2] == AppWrapper3
assert app_wrappers[3] == AppWrapper4
assert app_wrappers[4] == AppWrapper5

def test_wrap_app(self):
class RootController(TGController):
Expand Down
153 changes: 153 additions & 0 deletions tests/test_configuration_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
from nose.tools import raises

from tg.configuration.utils import DependenciesList

class DLEntry1: pass
class DLEntry2: pass
class DLEntry3: pass
class DLEntry4: pass
class DLEntry5: pass


class TestDependenciesList(object):
def test_basic_with_classes(self):
dl = DependenciesList()

dl.add(DLEntry2)
dl.add(DLEntry4, after=DLEntry3)
dl.add(DLEntry3)
dl.add(DLEntry1, after=False)
dl.add(DLEntry5, after=DLEntry3)

dl_values = list(dl.values())
assert dl_values[0] == DLEntry1
assert dl_values[1] == DLEntry2
assert dl_values[2] == DLEntry3
assert dl_values[3] == DLEntry4
assert dl_values[4] == DLEntry5

def test_basic_iter(self):
dl = DependenciesList()

dl.add(DLEntry2)
dl.add(DLEntry4, after=DLEntry3)
dl.add(DLEntry3)
dl.add(DLEntry1, after=False)
dl.add(DLEntry5, after=DLEntry3)

visisted = []
for key, value in dl:
visisted.append(key)

assert visisted == ['DLEntry1', 'DLEntry2', 'DLEntry3', 'DLEntry4', 'DLEntry5']

def test_basic_repr(self):
dl = DependenciesList()

dl.add(DLEntry2)
dl.add(DLEntry4, after=DLEntry3)
dl.add(DLEntry3)
dl.add(DLEntry1, after=False)
dl.add(DLEntry5, after=DLEntry3)

expected = "<DependenciesList ['DLEntry1', 'DLEntry2', 'DLEntry3', 'DLEntry4', 'DLEntry5']>"
assert repr(dl) == expected

def test_basic_with_ids(self):
dl = DependenciesList()

dl.add(DLEntry2, 'num2')
dl.add(DLEntry4, 'num4', after='num3')
dl.add(DLEntry3, 'num3')
dl.add(DLEntry1, 'num1', after=False)
dl.add(DLEntry5, 'num5', after='num3')

dl_values = list(dl.values())
assert dl_values[0] == DLEntry1
assert dl_values[1] == DLEntry2
assert dl_values[2] == DLEntry3
assert dl_values[3] == DLEntry4
assert dl_values[4] == DLEntry5

def test_reversed_with_ids(self):
dl = DependenciesList()

dl.add(DLEntry5, 'num5', after='num4')
dl.add(DLEntry4, 'num4', after='num3')
dl.add(DLEntry3, 'num3', after='num2')
dl.add(DLEntry2, 'num2', after='num1')
dl.add(DLEntry1, 'num1')

dl_values = list(dl.values())
assert dl_values[0] == DLEntry1
assert dl_values[1] == DLEntry2
assert dl_values[2] == DLEntry3
assert dl_values[3] == DLEntry4
assert dl_values[4] == DLEntry5

def test_multiple_with_ids(self):
dl = DependenciesList()
dl.add(DLEntry1)
dl.add(DLEntry4, after=DLEntry3)
dl.add(DLEntry5, after=DLEntry4)
dl.add(DLEntry2, after=DLEntry1)
dl.add(DLEntry3, after=DLEntry1)

dl_values = list(dl.values())
assert dl_values[0] == DLEntry1
assert dl_values[1] == DLEntry2
assert dl_values[2] == DLEntry3
assert dl_values[3] == DLEntry4
assert dl_values[4] == DLEntry5

def test_multiple_with_missing_step(self):
dl = DependenciesList()
dl.add(DLEntry1)
dl.add(DLEntry3, after=DLEntry2)
dl.add(DLEntry2, after=DLEntry5)

dl_values = list(dl.values())
assert dl_values[0] == DLEntry1
assert dl_values[1] == DLEntry2
assert dl_values[2] == DLEntry3

def test_objects_instead_of_classes(self):
dl = DependenciesList()
dl.add(DLEntry1(), 'DLEntry1')
dl.add(DLEntry3(), 'DLEntry3', after=DLEntry2)
dl.add(DLEntry2(), 'DLEntry2', after=DLEntry5)

dl_values = list(dl.values())
assert dl_values[0].__class__ == DLEntry1
assert dl_values[1].__class__ == DLEntry2
assert dl_values[2].__class__ == DLEntry3

@raises(ValueError)
def test_objects_must_have_key(self):
dl = DependenciesList()
dl.add(DLEntry1())

@raises(ValueError)
def test_after_cannot_be_an_instance(self):
dl = DependenciesList()
dl.add(DLEntry1(), key='DLEntry1')
dl.add(DLEntry2(), key='DLEntry2', after=DLEntry1())

def test_after_everything_else(self):
dl = DependenciesList()

dl.add(DLEntry2, after=True)
dl.add(DLEntry5, after=True)
dl.add(DLEntry4, after=DLEntry3)
dl.add(DLEntry3, after=DLEntry2)

dl.add(DLEntry1)

dl_values = list(dl.values())
assert dl_values[0] == DLEntry1
assert dl_values[1] == DLEntry2
assert dl_values[2] == DLEntry3
assert dl_values[3] == DLEntry4
assert dl_values[4] == DLEntry5

33 changes: 6 additions & 27 deletions tg/configuration/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import tg
from tg.util import Bunch, DottedFileNameFinder
from tg.configuration import milestones
from tg.configuration.utils import TGConfigError, coerce_config, get_partial_dict, coerce_options

from tg.configuration.utils import TGConfigError, coerce_config, get_partial_dict, coerce_options, \
DependenciesList
from tg.renderers.genshi import GenshiRenderer
from tg.renderers.json import JSONRenderer
from tg.renderers.jinja import JinjaRenderer
Expand Down Expand Up @@ -272,15 +272,12 @@ def __init__(self, minimal=False, root_controller=None):
self.call_on_shutdown = []
self.controller_caller = call_controller
self.controller_wrappers = []
self.application_wrappers = []
self.application_wrappers_dependencies = {False: [],
None: [],
True: []}
self.application_wrappers = DependenciesList()

#override this variable to customize how the tw2 middleware is set up
# override this variable to customize how the tw2 middleware is set up
self.custom_tw2_config = {}

#This is for minimal mode to set root controller manually
# This is for minimal mode to set root controller manually
if root_controller is not None:
self['tg.root_controller'] = root_controller

Expand Down Expand Up @@ -400,8 +397,7 @@ def register_wrapper(self, wrapper, after=None):
'milestone has been reached, the wrapper will be used only'
'for future TGApp instances.', wrapper)

self.application_wrappers_dependencies.setdefault(after, []).append(wrapper)
self._configure_application_wrappers()
self.application_wrappers.add(wrapper, after=after)

def register_rendering_engine(self, factory):
"""Registers a rendering engine ``factory``.
Expand Down Expand Up @@ -589,23 +585,6 @@ def _configure_transaction_manager(self):
log.debug('Disabling Transaction Manager as SQLAlchemy is not available')
self.use_transaction_manager = False

def _configure_application_wrappers(self):
# Those are the heads of the dependencies tree
DEPENDENCY_HEADS = (False, None, True)

# Clear in place, this is to avoid desync between self and config
self.application_wrappers[:] = []

registered_wrappers = self.application_wrappers_dependencies.copy()
visit_queue = deque(DEPENDENCY_HEADS)
while visit_queue:
current = visit_queue.popleft()
if current not in DEPENDENCY_HEADS:
self.application_wrappers.append(current)

dependant_wrappers = registered_wrappers.pop(current, [])
visit_queue.extendleft(reversed(dependant_wrappers))

def _configure_package_paths(self):
try:
self.package
Expand Down
98 changes: 98 additions & 0 deletions tg/configuration/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import inspect
from collections import deque

import itertools

from .milestones import config_ready


Expand Down Expand Up @@ -113,3 +118,96 @@ def create_global(cls):
def _load_config(self):
from tg.configuration import config
self.configure(**coerce_config(config, self.CONFIG_NAMESPACE, self.CONFIG_OPTIONS))


class DependenciesList(object):
"""Manages a list of entries which might depend one from the other.
This powers :meth:`.AppConfig.register_wrapper` and other features in
TurboGears2, making possible to register the wrappers right after
other wrappers or at the end of the wrappers chain.
"""
#: Those are the heads of the dependencies tree
#: - ``False`` means before everything else
#: - ``None`` means in the middle.
#: - ``True`` means after everything else.
DEPENDENCY_HEADS = (False, None, True)

def __init__(self, *entries):
self._dependencies = {}
self._ordered_elements = []
self._inserted_keys = []

for entry in entries:
self.add(entry)

def add(self, entry, key=None, after=None):
"""Adds an entry to the dependencies list.
:param entry: Entry that must be added to the list.
:param str|type|None key: An identifier of the object being inserted.
This is used by later insertions as ``after`` argument
to specify after which object the new one should be inserted.
:param str|type|None|False|True after: After which element this one should be inserted.
This is the ``key`` of a previously inserted item.
In case no item with ``key`` has been inserted, the
entry will be inserted in normal order of insertion.
Also accepts one of
:attr:`.DependenciesList.DEPENDENCY_HEADS` as key
to add entries at begin or end of the list.
"""
if key is None:
if inspect.isclass(entry):
key = entry.__name__
else:
# Inserting an object without a key would lead to unexpected ordering.
# we cannot use the object class as the key would not be unique across
# different instances.
raise ValueError('Inserting instances without a key is not allowed')

if after not in self.DEPENDENCY_HEADS and not isinstance(after, str):
if inspect.isclass(after):
after = after.__name__
else:
raise ValueError('after parameter must be a string, a type or a special value')

self._inserted_keys.append(key)
self._dependencies.setdefault(after, []).append((key, entry))
self._resolve_ordering()

def __repr__(self):
return '<DependenciesList %s>' % [x[0] for x in self._ordered_elements]

def __iter__(self):
return iter(self._ordered_elements)

def values(self):
"""Returns all the inserted values without their key as a generator"""
return (x[1]for x in self._ordered_elements)

def _resolve_ordering(self):
ordered_elements = []

existing_dependencies = set(self._inserted_keys) | set(self.DEPENDENCY_HEADS)
dependencies_without_heads = set(self._dependencies.keys()) - set(self.DEPENDENCY_HEADS)

# All entries that depend on a missing entry are converted
# to depend from None so they end up being in the middle.
dependencies = {}
for dependency in itertools.chain(self.DEPENDENCY_HEADS, dependencies_without_heads):
entries = self._dependencies.get(dependency, [])
if dependency not in existing_dependencies:
dependency = None
dependencies.setdefault(dependency, []).extend(entries)

# Resolve the dependencies and generate the ordered elements.
visit_queue = deque((head, head) for head in self.DEPENDENCY_HEADS)
while visit_queue:
current_key, current_obj = visit_queue.popleft()
if current_key not in self.DEPENDENCY_HEADS:
ordered_elements.append((current_key, current_obj))

element_dependencies = dependencies.pop(current_key, [])
visit_queue.extendleft(reversed(element_dependencies))

self._ordered_elements = ordered_elements
1 change: 1 addition & 0 deletions tg/util/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ def wrapper(*args, **kwargs):
raise ValueError('{} == {}'.format(v, check))
return v
return wrapper

Loading

0 comments on commit 19ac23e

Please sign in to comment.