# source.py # # Copyright 2023 Geoffrey Coulaud # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # SPDX-License-Identifier: GPL-3.0-or-later import sys from abc import abstractmethod from collections.abc import Iterable from typing import Any, Collection, Generator, Optional from src.game import Game from src.importer.sources.location import Location # Type of the data returned by iterating on a Source SourceIterationResult = Optional[Game | tuple[Game, tuple[Any]]] class SourceIterable(Iterable): """Data producer for a source of games""" source: "Source" def __init__(self, source: "Source") -> None: self.source = source @abstractmethod def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ Method that returns a generator that produces games * Should be implemented as a generator method * May yield `None` when an iteration hasn't produced a game * In charge of handling per-game errors * Returns when exhausted """ class Source(Iterable): """Source of games. E.g an installed app with a config file that lists game directories""" source_id: str name: str variant: Optional[str] = None available_on: set[str] = set() iterable_class: type[SourceIterable] # NOTE: Locations must be set at __init__ time, not in the class definition. # They must not be shared between source instances. locations: Collection[Location] @property def full_name(self) -> str: """The source's full name""" full_name_ = self.name if self.variant: full_name_ += f" ({self.variant})" return full_name_ @property def game_id_format(self) -> str: """The string format used to construct game IDs""" return self.source_id + "_{game_id}" @property def is_available(self) -> bool: return sys.platform in self.available_on def make_executable(self, *args, **kwargs) -> str: """ Create a game executable command. Should be implemented by child classes. """ def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ Get an iterator for the source :raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable """ for location in self.locations: location.resolve() return iter(self.iterable_class(self)) class ExecutableFormatSource(Source): """Source class that uses a simple executable format to start games""" @property @abstractmethod def executable_format(self) -> str: """The executable format used to construct game executables""" def make_executable(self, *args, **kwargs) -> str: """Use the executable format to""" return self.executable_format.format(*args, **kwargs) # pylint: disable=abstract-method class URLExecutableSource(ExecutableFormatSource): """Source class that use custom URLs to start games""" url_format: str @property def executable_format(self) -> str: match sys.platform: case "win32": return "start " + self.url_format case "linux": return "xdg-open " + self.url_format case other: raise NotImplementedError( f"No URL handler command available for {other}" )