Source code for autopilot.utils.requires

"""
Stub module for specifying dependencies for Autopilot objects.

Draft for now, to be integrated in v0.5.0
"""
import typing
from importlib.util import find_spec
import sys
if sys.version_info.minor<8:
    from importlib_metadata import version
else:
    from importlib.metadata import version
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from packaging.specifiers import SpecifierSet

from autopilot.utils import types

if typing.TYPE_CHECKING:
    from importlib.machinery import ModuleSpec

[docs]@dataclass class Requirement(ABC): """ Base class for different kinds of requirements """ name: str version: SpecifierSet = field(default=SpecifierSet('')) @property @abstractmethod def met(self) -> bool: """ Check if a requirement is met Returns: bool: ``True`` if met, ``False`` otherwise """
[docs] @abstractmethod def resolve(self) -> bool: """ Try and resolve a requirement by getting packages, changing system settings, etc. Returns: bool: True if successful! """
[docs]@dataclass class Git_Spec: """ Specify a git repository or its subcomponents: branch, commit, or tag """ url: types.URL branch: typing.Optional[str] = None commit: typing.Optional[str] = None tag: typing.Optional[str] = None
[docs]@dataclass class Python_Package(Requirement): """ Attributes: package_name (str): If a package is named differently in package repositories than it is imported, specify the ``package_name`` (default is ``package_name == name``). The ``name`` will be used to test whether the package can be imported, and ``package_name`` used to install from the specified ``repository`` if not repository (:class:`~.autopilot.utils.types.URL`): The URL of a python package repository to use to install. Defaults to pypi git (class:`.Git_Spec`): Specify a package comes from a particular git repository, commit, or branch instead of from a package repository. If ``git`` is present, ``repository`` is ignored. """ package_name: typing.Optional[str] = None repository: types.URL = types.URL("https://pypi.org/simple") git: typing.Optional[Git_Spec] = None def __post_init__(self): if self.package_name is None: self.package_name = self.name @property def import_spec(self) -> typing.Union['ModuleSpec', bool]: """ The :class:`importlib.machinery.ModuleSpec` for :attr:`.name` , if present, otherwise False Returns: :class:`importlib.machinery.ModuleSpec` or False """ spec = find_spec(self.name) if spec: return spec else: return False @property def package_version(self) -> typing.Union[str, bool]: """ The version of the installed package, if found. Uses :attr:`.package_name` (name when installing, eg. ``auto-pi-lot`` ) which can differ from the :attr:`.name` (eg. ``autopilot`` ) of a package (used when importing) Returns: str: 'x.x.x' or False if not found """ if not self.import_spec: return False else: return version(self.package_name) @property def met(self) -> bool: """ Return ``True`` if python package is found in the PYTHONPATH that satisfies the ``SpecifierSet`` """ if self.import_spec and self.version.contains(self.package_version): return True else: return False # TODO: make a 'status' type that can differentiate between outdated vs. not installed packages
[docs] def resolve(self) -> bool: """ We're not supposed to Returns: """ raise NotImplementedError()
[docs]@dataclass class System_Library(Requirement): """ System-level package .. warning:: not implemented """
[docs]@dataclass class Requirements: """ Dataclass for a collection of requirements for a particular object. Each object should have at most one ``Requirements`` object, which may have many sub-requirements Attributes: requirements (list[Requirement]): List of requirements. (a singular requirement should have an identical API to requirements, the met and resolve methods) """ requirements: typing.Union[typing.List[Requirement]] @property def met(self) -> bool: """ Checks if the specified requirements are met Returns: bool: ``True`` if requirements are met, ``False`` if not """ all_met = [req.met for req in self.requirements] return all(all_met)
[docs] def resolve(self) -> bool: raise NotImplementedError
[docs] def __add__(self, other): """ Add requirement sets together .. warning:: Not Implemented Args: other (): Returns: """ raise NotImplementedError()