#
# Copyright (C) 2020 Codethink Limited
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see .
#
# Authors:
# Tristan Van Berkom
from .._exceptions import LoadError
from ..exceptions import LoadErrorReason
from ..types import _ProjectInformation
# ProjectLoaders()
#
# An object representing all of the loaders for a given project.
#
class ProjectLoaders:
def __init__(self, project_name):
# The project name
self._name = project_name
# A list of all loaded loaders for this project
self._collect = []
# register_loader()
#
# Register a Loader for this project
#
# Args:
# loader (Loader): The loader to register
#
def register_loader(self, loader):
assert loader.project.name == self._name
self._collect.append(loader)
# assert_loaders():
#
# Asserts the validity of loaders for this project
#
# Raises:
# (LoadError): In case there is a CONFLICTING_JUNCTION error
#
def assert_loaders(self):
duplicates = {}
internal = {}
primary = []
for loader in self._collect:
duplicating, internalizing = self._search_project_relationships(loader)
if duplicating:
duplicates[loader] = duplicating
if internalizing:
internal[loader] = internalizing
if not (duplicating or internalizing):
primary.append(loader)
if len(primary) > 1:
self._raise_conflict(duplicates, internal)
elif primary and duplicates:
self._raise_conflict(duplicates, internal)
# loaded_projects()
#
# A generator which yeilds all of the instances
# of this loaded project.
#
# Yields:
# (_ProjectInformation): A descriptive project information object
#
def loaded_projects(self):
for loader in self._collect:
duplicating, internalizing = self._search_project_relationships(loader)
yield _ProjectInformation(
loader.project, loader.provenance_node, [str(l) for l in duplicating], [str(l) for l in internalizing]
)
# _search_project_relationships()
#
# Searches this loader's ancestry for projects which mark this
# loader as internal or duplicate
#
# Args:
# loader (Loader): The loader to search for duplicate markers of
#
# Returns:
# (list): A list of Loader objects who's project has marked
# this junction as a duplicate
# (list): A list of Loader objects who's project has marked
# this junction as internal
#
def _search_project_relationships(self, loader):
duplicates = []
internal = []
for parent in loader.ancestors():
if parent.project.junction_is_duplicated(self._name, loader):
duplicates.append(parent)
if parent.project.junction_is_internal(loader):
internal.append(parent)
return duplicates, internal
# _raise_conflict()
#
# Raises the LoadError indicating there was a conflict, this
# will list all of the instances in which the project has
# been loaded as the LoadError detail string
#
# Args:
# duplicates (dict): A table of duplicating Loaders, indexed
# by duplicated Loader
# internals (dict): A table of Loaders which mark a loader as internal,
# indexed by internal Loader
#
# Raises:
# (LoadError): In case there is a CONFLICTING_JUNCTION error
#
def _raise_conflict(self, duplicates, internals):
explanation = (
"Internal projects do not cause any conflicts. Conflicts can also be avoided\n"
+ "by marking every instance of the project as a duplicate."
)
lines = [self._loader_description(loader, duplicates, internals) for loader in self._collect]
detail = "{}\n{}".format("\n".join(lines), explanation)
raise LoadError(
"Project '{}' was loaded in multiple contexts".format(self._name),
LoadErrorReason.CONFLICTING_JUNCTION,
detail=detail,
)
# _loader_description()
#
# Args:
# loader (Loader): The loader to describe
# duplicates (dict): A table of duplicating Loaders, indexed
# by duplicated Loader
# internals (dict): A table of Loaders which mark a loader as internal,
# indexed by internal Loader
#
# Returns:
# (str): A string representing how this loader was loaded
#
def _loader_description(self, loader, duplicates, internals):
line = "{}\n".format(loader)
# Mention projects which have marked this project as a duplicate
duplicating = duplicates.get(loader)
if duplicating:
for dup in duplicating:
line += " Duplicated by: {}\n".format(dup)
# Mention projects which have marked this project as internal
internalizing = internals.get(loader)
if internalizing:
for internal in internalizing:
line += " Internal to: {}\n".format(internal)
return line
# LoaderContext()
#
# An object to keep track of overall context during the load process.
#
# Args:
# context (Context): The invocation context
#
class LoadContext:
def __init__(self, context):
# Keep track of global context required throughout the recursive load
self.context = context
self.rewritable = False
self.fetch_subprojects = None
self.task = None
# A table of all Loaders, indexed by project name
self._loaders = {}
# set_rewritable()
#
# Sets whether the projects are to be loaded in a rewritable fashion,
# this is used for tracking and is slightly more expensive in load time.
#
# Args:
# task (Task): The task to report progress on
#
def set_rewritable(self, rewritable):
self.rewritable = rewritable
# set_task()
#
# Sets the task for progress reporting.
#
# Args:
# task (Task): The task to report progress on
#
def set_task(self, task):
self.task = task
# set_fetch_subprojects()
#
# Sets the task for progress reporting.
#
# Args:
# task (callable): The callable for loading subprojects
#
def set_fetch_subprojects(self, fetch_subprojects):
self.fetch_subprojects = fetch_subprojects
# assert_loaders()
#
# Asserts that there are no conflicting projects loaded.
#
# Raises:
# (LoadError): A CONFLICTING_JUNCTION LoadError in the case of a conflict
#
def assert_loaders(self):
for _, loaders in self._loaders.items():
loaders.assert_loaders()
# register_loader()
#
# Registers a new loader in the load context, possibly
# raising an error in the case of a conflict.
#
# This must be called after a recursive load process has completed,
# and after the pipeline is resolved (which is to say that all related
# Plugin derived objects have been instantiated).
#
# Args:
# loader (Loader): The Loader object to register into context
#
def register_loader(self, loader):
project = loader.project
try:
project_loaders = self._loaders[project.name]
except KeyError:
project_loaders = ProjectLoaders(project.name)
self._loaders[project.name] = project_loaders
project_loaders.register_loader(loader)
# loaded_projects()
#
# A generator which yeilds all of the loaded projects
#
# Yields:
# (_ProjectInformation): A descriptive project information object
#
def loaded_projects(self):
for _, project_loaders in self._loaders.items():
yield from project_loaders.loaded_projects()