#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''The main class that is used to create and store Context instances.
This setup is what makes ways.api.Context objects into flyweight objects.
TODO:
Remove this class. It could just be a list with functions.
'''
# IMPORT STANDARD LIBRARIES
# scspell-id: 3c62e4aa-c280-11e7-be2b-382c4ac59cfd
import collections
# IMPORT WAYS LIBRARIES
import ways
# IMPORT LOCAL LIBRARIES
from ..helper import common
class _AssignmentFactory(object):
'''A Flyweight factory used to create and hold onto Context instances.'''
def __init__(self, class_type):
'''Create the factory that registers a specific class type of object.
Args:
class_type (classobj): The class to instantiate with this factory.
'''
super(_AssignmentFactory, self).__init__()
self._class_type = class_type
self._instances = collections.defaultdict(dict)
def get_instance(self, hierarchy, assignment, force=False):
'''Get an instance of our class if it exists and make it if does not.
Args:
hierarchy (tuple[str] or str):
The position where all the plugins for our instance would live.
assignment (str): The placement for the instance.
force (:obj:`bool`, optional):
If False and the Context has no plugins, return None.
If True, return the empty Context. Default is False.
Returns:
class_type or NoneType:
An instance of our stored class. If the given hierarchy does not
have any Plugin objects defined for it, it's considered 'empty'.
To avoid errors in our code later, we return None, unless
force is True.
'''
if isinstance(hierarchy, self._class_type):
# A Context object was passed, by mistake. Just return it
return hierarchy
# Get our instance, if there is one
try:
return self._instances[hierarchy][assignment]
except KeyError:
pass
# Make our instance
return self._make_and_store_new_instance(hierarchy, assignment, force=force)
def _make_and_store_new_instance(self, hierarchy, assignment, force=False):
'''Create and store our new class instance.
Args:
hierarchy (tuple[str] or str):
The position where all the plugins for our instance would live.
assignment (str):
The priority mapping for this instance's plugins.
force (:obj:`bool`, optional):
If False and the Context has no plugins, return None.
If True, return the empty Context. Default is False.
Returns:
A new instance of our class type.
'''
def make_and_store_instance(hierarchy, assignment):
'''Create some instance cache it for later, if needed.'''
instance = self._class_type(hierarchy, assignment=assignment)
self._instances[hierarchy][assignment] = instance
return instance
hierarchy = common.split_hierarchy(hierarchy)
# If no plugins were defined or if the plugins are not
# "not findable" (like an incomplete Context Plugin)
# we return None to avoid making an undefined Context
#
try:
if not ways.PLUGIN_CACHE['hierarchy'][hierarchy][assignment]:
raise KeyError
except KeyError:
# Is the user specified a null assignment (aka they want all plugins
# from every assignment) and there are plugins, just pass it through
#
is_forcible = (not assignment and ways.PLUGIN_CACHE['hierarchy'].get(hierarchy))
if force or is_forcible:
# Register the context, even though it does not have plugins
return make_and_store_instance(hierarchy, assignment)
# Return nothing, no Plugin objects were found so no Context
# will be built
#
return None
plugins = []
# Add any Context objects that these plugins depend on
hierarchies = []
for plugin in ways.PLUGIN_CACHE['hierarchy'][hierarchy][assignment]:
try:
used = plugin.get_uses()
except AttributeError:
used = []
hierarchies.extend(used)
for uses in hierarchies:
plugins.append(
self.get_instance(uses, assignment=assignment, force=True))
for plugin in ways.PLUGIN_CACHE['hierarchy'][hierarchy][assignment]:
plugins.append(plugin)
if not force and not plugins:
return None
return make_and_store_instance(hierarchy, assignment)
def clear(self):
'''Remove every Context instance that this object knows about.
If the user tries to get a Context after this method is run,
a new instance for the Context will be created and returned.
Running this method is not recommended because Ways is not meant to
forget Context objects after they have been created.
'''
self._instances.clear()
[docs]class AliasAssignmentFactory(_AssignmentFactory):
'''Extend the _AssignmentFactory object to include Context aliases.'''
def __init__(self, class_type):
'''Create this object and our empty alias dictionary.
Args:
class_type (classobj): The class to instantiate with this factory.
'''
super(AliasAssignmentFactory, self).__init__(class_type=class_type)
self.aliases = dict()
[docs] def is_aliased(self, hierarchy):
'''bool: If this hierarchy is an alias for another hierarchy.'''
return hierarchy in self.aliases and self.aliases.get(hierarchy) != hierarchy
[docs] def resolve_alias(self, hierarchy):
'''Assuming that the given hierarchy is an alias, follow the alias.
Args:
hierarchy (tuple[str] or str):
The location to look for our instance. In this method,
hierarchy is expected to be an alias for another hierarchy
so we look for the real hierarchy, here.
Returns:
tuple[str]:
The base hierarchy that this alias is meant to represent.
'''
current = tuple()
while current != hierarchy:
# On the first run, current will be empty so we just assign it
# to a fake hierarchy so that the try/except will not immediately
# break
#
if current == tuple():
current = hierarchy
# Keep following the aliases until we get to the real hierarchy
try:
current = self.aliases[current]
except KeyError:
break
resolved_hierarchy = common.split_hierarchy(current)
return resolved_hierarchy
# pylint: disable=arguments-differ
[docs] def get_instance(self, hierarchy, assignment, follow_alias=False, force=False):
'''Get an instance of our class if it exists and make it if does not.
Args:
hierarchy (tuple[str] or str):
The location to look for our instance.
assignment (str):
The category/grouping of the instance.
follow_alias (:obj:`bool`, optional):
If True, the instance's hierarchy is assumed to be an alias
for another hierarchy and the returned instance will use
the "real" hierarchy. If False, the instance will stay as
the aliased hierarchy, completely unmodified.
Default is False.
force (:obj:`bool`, optional):
If False and the Context has no plugins, return None.
If True, an empty Context is returned. Default is False.
Returns:
self._class_type() or NoneType:
An instance of our preferred class. If the Context that is
called does not have any Plugin objects defined for it, it's
considered 'empty'. To avoid problems in our code later,
we return None, by default unless force is True.
'''
if isinstance(hierarchy, self._class_type):
# A Context object was passed, by mistake. Just return it again
return hierarchy
hierarchy = common.split_hierarchy(hierarchy)
instance = super(AliasAssignmentFactory, self).get_instance(
hierarchy=hierarchy, assignment=assignment, force=force)
if not self.is_aliased(hierarchy):
return instance
resolved_hierarchy = self.resolve_alias(hierarchy)
if not follow_alias:
return instance
# Follow the alias to get a Context with the 'real' hierarchy
return super(AliasAssignmentFactory, self).get_instance(
hierarchy=resolved_hierarchy, assignment=assignment, force=force)
[docs] def clear(self):
'''Remove all the stored aliases in this instance.'''
super(AliasAssignmentFactory, self).clear()
self.aliases = dict()