Source code for dgenerate.preprocessors.loader

# Copyright (c) 2023, Teriks
#
# dgenerate is distributed under the following BSD 3-Clause License
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
#    from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import inspect
import itertools
import sys
import typing

import dgenerate.plugin as _plugin
import dgenerate.preprocessors.exceptions as _exceptions
import dgenerate.preprocessors.preprocessor as _preprocessor
import dgenerate.preprocessors.preprocessorchain as _preprocessorchain
import dgenerate.textprocessing as _textprocessing
import dgenerate.types as _types


[docs] class Loader: search_modules: typing.Set """Additional module objects for this loader to search, aside from the preprocessors sub module.""" extra_classes: typing.Set """ Additional directly defined implementation classes. This is empty by default and is for allowing library users to quickly add a class implementing :py:class:`dgenerate.preprocessors.preprocessor.ImagePreprocessor` if desired without creating a new file for it. """
[docs] def __init__(self): self.search_modules = set() self.extra_classes = set()
[docs] def add_class(self, cls: typing.Type[_preprocessor.ImagePreprocessor]): """ Add an image preprocessor implementation by its class, this is unused by dgenerate and is provided for the convenience of library users. :param cls: class the implements :py:class:`dgenerate.preprocessors.preprocessor.ImagePreprocessor`, (not an instance, use the type itself) """ self.extra_classes.add(cls)
[docs] def load_plugin_modules(self, paths: _types.Paths): """ Add a list of python module directories or python files to :py:attr:`.Loader.search_modules` They will be newly imported or fetched if already previously imported. :param paths: python module directories, python file paths """ self.search_modules.update(_plugin.load_modules(paths))
def _load(self, path, device): call_by_name = path.split(';', 1)[0].strip() preprocessor_class = self.get_class_by_name(call_by_name) inherited_args = ['output-file', 'output-overwrite', 'device'] parser_accepted_args = preprocessor_class.get_accepted_args(call_by_name) if 'called-by-name' in parser_accepted_args: raise RuntimeError(f'called-by-name is a reserved ImagePreprocessor module argument, ' 'chose another argument name for your module.') for inherited_arg in inherited_args: if inherited_arg in parser_accepted_args: raise RuntimeError(f'{inherited_arg} is a reserved ImagePreprocessor module argument, ' 'chose another argument name for your module.') parser_accepted_args.append(inherited_arg) arg_parser = _textprocessing.ConceptUriParser("Image Preprocessor", parser_accepted_args) try: parsed_args = arg_parser.parse_concept_uri(path).args except _textprocessing.ConceptPathParseError as e: raise _exceptions.ImagePreprocessorArgumentError(str(e)) args_dict = {} for arg in preprocessor_class.get_default_args(call_by_name): args_dict[_textprocessing.dashdown(arg[0])] = arg[1] for k, v in parsed_args.items(): args_dict[_textprocessing.dashdown(k)] = v args_dict['output_file'] = parsed_args.get('output-file') args_dict['output_overwrite'] = parsed_args.get('output-overwrite', False) args_dict['device'] = parsed_args.get('device', device) args_dict['called_by_name'] = call_by_name for arg in preprocessor_class.get_required_args(call_by_name): if _textprocessing.dashdown(arg) not in args_dict: raise _exceptions.ImagePreprocessorArgumentError( f'Missing required argument "{arg}" for image preprocessor "{call_by_name}".') try: return preprocessor_class(**args_dict) except _exceptions.ImagePreprocessorArgumentError as e: raise _exceptions.ImagePreprocessorArgumentError( f'Invalid argument given to image preprocessor "{call_by_name}": {e}')
[docs] def get_available_classes(self) -> typing.List[typing.Type[_preprocessor.ImagePreprocessor]]: """ Return a list of all :py:class:`dgenerate.preprocessors.ImagePreprocessor` implementations visible to this loader. :return: list of :py:class:`dgenerate.preprocessors.ImagePreprocessor` """ found_classes = [] for mod in itertools.chain([sys.modules['dgenerate.preprocessors']], self.search_modules): def _excluded(cls): if not inspect.isclass(cls): return True if cls is _preprocessor.ImagePreprocessor: return True if not issubclass(cls, _preprocessor.ImagePreprocessor): return True if hasattr(cls, 'HIDDEN'): return cls.HIDDEN else: return False found_classes += [value for value in itertools.chain(_types.get_public_members(mod).values(), self.extra_classes) if not _excluded(value)] return found_classes
[docs] def get_class_by_name(self, preprocessor_name) -> typing.Type[_preprocessor.ImagePreprocessor]: """ Get a :py:class:`dgenerate.preprocessors.ImagePreprocessor` implementation from the loader using one of the implementations defined :py:attr:`dgenerate.preprocessors.ImagePreprocessor.NAMES` :raises RuntimeError: if more than one class was found using the provided name. :raises ImagePreprocessorNotFoundError: if the name could not be found. :param preprocessor_name: the name to search for :return: :py:class:`dgenerate.preprocessors.ImagePreprocessor` """ classes = [cls for cls in self.get_available_classes() if preprocessor_name in cls.get_names()] if len(classes) > 1: raise RuntimeError( f'Found more than one ImagePreprocessor with the name: {preprocessor_name}') if not classes: raise _exceptions.ImagePreprocessorNotFoundError( f'Found no image preprocessor with the name: {preprocessor_name}') return classes[0]
[docs] def get_all_names(self) -> _types.Names: """ Get all :py:attr:`dgenerate.preprocessors.ImagePreprocessor.NAMES` values visible to this loader. :return: list of names (strings) """ names = [] for cls in self.get_available_classes(): names += cls.get_names() return names
[docs] def get_help(self, preprocessor_name: _types.Name) -> str: """ Get the formatted help string for a specific preprocessor by name. :raises RuntimeError: if more than one class was found using the provided name. :raises ImagePreprocessorNotFoundError: if the name could not be found. :param preprocessor_name: the preprocessor name to search for :return: formatted help string """ return self.get_class_by_name(preprocessor_name).get_help(preprocessor_name)
[docs] def load(self, uri: typing.Union[_types.Uri, typing.Iterable[_types.Uri]], device: str = 'cpu') -> \ typing.Union[_preprocessor.ImagePreprocessor, _preprocessorchain.ImagePreprocessorChain, None]: """ Load an image preprocessor or multiple image preprocessors. They are loaded by URI, which is their name and any module arguments, for example: ``canny;lower=50;upper=100`` Specifying multiple preprocessors with a list will create an image preprocessor chain object. :raises RuntimeError: if more than one class was found using the provided name mentioned in the URI. :raises ImagePreprocessorNotFoundError: if the name mentioned in the URI could not be found. :raises ImagePreprocessorArgumentError: if the URI contained invalid arguments. :param uri: Preprocessor URI or list of URIs :param device: Request a specific rendering device, default is CPU :return: :py:class:`dgenerate.preprocessors.ImagePreprocessor` or :py:class:`dgenerate.preprocessors.ImagePreprocessorChain` """ if uri is None: raise ValueError('uri must not be None') if isinstance(uri, str): return self._load(uri, device) paths = list(uri) if not paths: return None if len(paths) == 1: return self._load(paths[0], device) return _preprocessorchain.ImagePreprocessorChain(self._load(i, device) for i in paths)
__all__ = _types.module_all()