# # 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()