"""
The following functions provide a set of utilities for working with `Dependent`
and collections of `Dependent`.
* :func:`~revscoring.dependencies.solve` provides basic dependency solving
* :func:`~revscoring.dependencies.expand` provides minimal expansion of
dependency trees
* :func:`~revscoring.dependencies.dig` provides expansion of "root" dependents
-- dependents with no dependencies of their own
* :func:`~revscoring.dependencies.draw` provides a means to print a dependency
tree to the terminal (useful when debugging)
.. autofunction:: revscoring.dependencies.solve
.. autofunction:: revscoring.dependencies.expand
.. autofunction:: revscoring.dependencies.dig
.. autofunction:: revscoring.dependencies.draw
"""
import logging
import time
import traceback
from ..errors import CaughtDependencyError, DependencyError, DependencyLoop
logger = logging.getLogger(__name__)
[docs]def solve(dependents, context=None, cache=None, profile=None):
"""
Calculates a dependent's value by solving dependencies.
:Parameters:
dependents : :class:`revscoring.Dependent` | `iterable`
A dependent or collection of dependents to solve
context : `dict` | `iterable`
A mapping of injected dependency processers to use as context.
Can be specified as a set of new
:class:`revscoring.Dependent` or a map of
:class:`revscoring.Dependent`
pairs.
cache : `dict`
A cache of previously solved dependencies as
:class:`revscoring.Dependent`:`<value>` pairs
profile : `dict`
A mapping of :class:`revscoring.Dependent` to `list` of process
durations for generating the value. The provided `dict` will be
modified in-place and new durations will be appended.
:Returns:
The result of executing the dependents with all dependencies resolved.
If a single dependent is provided, the value will be returned. If a
collection of dependents is provided, a generator of values will be
returned
"""
cache = cache if cache is not None else {}
context = normalize_context(context)
if hasattr(dependents, '__iter__'):
# Multiple values -- return a generator
return _solve_many(dependents, context=context, cache=cache,
profile=profile)
else:
# Singular value -- return it's solution
dependent = dependents
value, _, _ = _solve(dependent, context=context, cache=cache,
profile=profile)
return value
[docs]def expand(dependents, context=None, cache=None):
"""
Calculates a dependent's value by solving dependencies.
:Parameters:
dependents : :class:`revscoring.Dependent` | `iterable`
A dependent or collection of dependents to solve
context : `dict` | `iterable`
A mapping of injected dependency processers to use as context.
Can be specified as a set of new
:class:`revscoring.Dependent` or a map of
:class:`revscoring.Dependent` pairs.
cache : `dict`
A cache of previously solved dependencies as `Dependent`:`<value>`
pairs
:Returns:
A generator over all dependents in the dependency tree with each
dependent occurring only once
"""
cache = set(cache or [])
context = normalize_context(context)
if hasattr(dependents, '__iter__'):
# Multiple values
return _expand_many(dependents, context, cache)
else:
# Singular value
dependent = dependents
return _expand(dependent, context, cache)
[docs]def draw(dependent, context=None, cache=None, depth=0):
"""
Returns a string representation of the the dependency tree for a single
:class:`revscoring.Dependent`.
:Parameters:
dependent : :class:`revscoring.Dependent`
The dependent to draw the dependencies for.
context : `dict` | `iterable`
A mapping of injected dependency processers to use as context.
Can be specified as a set of
:class:`revscoring.Dependent` or a map of
:class:`revscoring.Dependent` pairs.
cache : `dict` | `set`
A cache of previously solved dependencies as `Dependent`:`<value>`
pairs. When these items are reached while scanning the tree,
"CACHED" will be printed.
:Returns:
None
"""
return "\n".join(draw_lines(dependent, context, cache, depth)) + "\n"
def draw_lines(dependent, context, cache, depth):
cache = cache or {}
context = normalize_context(context)
if dependent in cache:
yield "\t" * depth + " - " + repr(dependent) + " CACHED"
else:
if dependent in context:
dependent = context[dependent]
yield "\t" * depth + " - " + repr(dependent)
# Check if we're a dependent with explicit dependencies
if hasattr(dependent, "dependencies"):
for dependency in dependent.dependencies:
yield from draw_lines(dependency, context, cache, depth + 1)
[docs]def dig(dependents, context=None, cache=None):
"""
Expands root dependencies. These are dependents at the bottom of the tree
-- :class:`revscoring.Dependent` with no dependencies of
their own.
:Parameters:
dependents : :class:`revscoring.Dependent` | `iterable`
A dependent or collection of dependents to scan
context : `dict` | `iterable`
A mapping of injected dependency processers to use as context.
Can be specified as a set of new
:class:`revscoring.Dependent` or a map of
:class:`revscoring.Dependent`
pairs.
cache : `dict` | `set`
A cache of previously solved dependencies to not scan beneath
:Returns:
A generator over root dependencies
"""
cache = set(cache or [])
context = normalize_context(context)
if hasattr(dependents, '__iter__'):
# Multiple values
return _dig_many(dependents, context, cache)
else:
# Singular value
dependent = dependents
return _dig(dependent, context, cache)
def normalize_context(context):
"""
Normalizes a context argument. This allows for context to be specified
either as a collection of contextual
:class:`revscoring.Dependent` or a `dict` of
:class:`revscoring.Dependent` pairs.
"""
if context is None:
return {}
elif isinstance(context, dict):
return context
elif hasattr(context, "__iter__"):
return {d: d for d in context}
else:
raise TypeError("'context' is not a dict or iterable: {0}"
.format(str(context)))
def _solve(dependent, context, cache, history=None, profile=None):
history = history or set()
# Check if we've already got a value for this dependency
if dependent in cache:
return cache[dependent], cache, history
# Check if a corresponding dependent was injected into the context
else:
# If a dependent is in context here, replace it.
if dependent in context:
dependent = context[dependent]
# Check if the dependency is callable.
if not callable(dependent):
raise RuntimeError("Can't solve dependency " + repr(dependent) +
". " + type(dependent).__name__ +
" is not callable.")
# Check if we're in a loop.
elif dependent in history:
raise DependencyLoop("Dependency loop detected at " +
repr(dependent))
# All is good. Time to generate a value
else:
# Add to history so we can detect any loops on the way down.
history.add(dependent)
# Check if we're a dependent with explicit dependencies
if hasattr(dependent, "dependencies"):
dependencies = dependent.dependencies
else:
# No dependencies? OK. Let's try that.
dependencies = []
# Generate args for process function from dependencies (if any)
args = []
for dependency in dependencies:
value, cache, history = _solve(dependency, context=context,
cache=cache, history=history,
profile=profile)
args.append(value)
# Generate value
try:
start = time.time()
value = dependent(*args)
duration = time.time() - start
if profile is not None:
if dependent in profile:
profile[dependent].append(duration)
else:
profile[dependent] = [duration]
except DependencyError as e:
raise
except Exception as e:
message = "Failed to process {0}: {1}".format(dependent, e)
tb = traceback.extract_stack()
formatted_exception = traceback.format_exc()
raise CaughtDependencyError(message, e, tb,
formatted_exception)
# Add value to cache
cache[dependent] = value
return cache[dependent], cache, history
def _solve_many(dependents, context, cache, profile=None):
for dependent in dependents:
value, cache, history = _solve(dependent, context=context, cache=cache,
profile=profile)
yield value
def _expand(dependent, context, cache):
if dependent not in cache:
yield dependent
cache.add(dependent)
if hasattr(dependent, "dependencies"):
yield from _expand_many(dependent.dependencies, context, cache)
def _expand_many(dependents, context, cache):
for dependent in dependents:
yield from _expand(dependent, context, cache)
def _dig(dependent, context, cache):
if hasattr(dependent, "dependencies"):
if len(dependent.dependencies) > 0:
yield from _dig_many(dependent.dependencies, context, cache)
else:
yield dependent
else:
yield dependent
def _dig_many(dependents, context, cache):
for dependent in dependents:
if dependent not in cache:
if dependent in context:
# Use contextual dependency
dependent = context[dependent]
cache.add(dependent)
yield from _dig(dependent, context, cache)