Source code for spyder.api.utils

# -----------------------------------------------------------------------------
# Copyright (c) 2021- Spyder Project Contributors
#
# Released under the terms of the MIT License
# (see LICENSE.txt in the project root directory for details)
# -----------------------------------------------------------------------------

"""
Helper functions to work with the Spyder plugin API.
"""

from __future__ import annotations

from abc import ABCMeta as BaseABCMeta
import collections.abc
import sys
import typing

if sys.version_info < (3, 10):
    from typing_extensions import ParamSpec
else:
    from typing import ParamSpec  # noqa: ICN003

_P = ParamSpec("_P")
_T = typing.TypeVar("_T")


[docs] def get_class_values(cls) -> list[str]: """ Get the attribute values for the class enumerations used in our API. Idea from `Stack Overflow <https://stackoverflow.com/a/17249228/438386>`__. Parameters ---------- cls Class object to list the enumeration values of. Returns ------- list[str] String attribute values from a Spyder pseudo-"enum" class. """ return [v for (k, v) in cls.__dict__.items() if k[:1] != "_"]
[docs] class PrefixNode: """Utility class used to represent a prefixed string tuple."""
[docs] def __init__(self, path: tuple[str, ...] | None = None) -> None: """ Representation of a prefixed string tuple. Parameters ---------- path : tuple[str, ...] | None, optional Underlying prefixed string tuple. The default is None. Returns ------- None """ self.children = {} self.path = path
def __iter__(self): prefix = [((self.path,), self)] while prefix != []: current_prefix, node = prefix.pop(0) prefix += [ (current_prefix + (c,), node.children[c]) for c in node.children ] yield current_prefix
[docs] def add_path(self, path: tuple[str, ...]) -> None: """ Add a path to the prefix node. Parameters ---------- path : tuple[str, ...] Underlying prefixed string tuple. Returns ------- None """ prefix, *rest = path if prefix not in self.children: self.children[prefix] = PrefixNode(prefix) if len(rest) > 0: child = self.children[prefix] child.add_path(rest)
[docs] class PrefixedTuple(PrefixNode): """Utility class to store and iterate over prefixed string tuples.""" def __iter__(self): for key in self.children: child = self.children[key] for prefix in child: yield prefix
[docs] class classproperty(property): """ Decorator to declare class constants requiring computation as properties. Idea from `Stack Overflow <https://stackoverflow.com/a/7864317/438386>`__. """
[docs] def __get__(self, cls, owner): return classmethod(self.fget).__get__(None, owner)()
[docs] class DummyAttribute: """Dummy class to mark abstract attributes.""" pass
[docs] def abstract_attribute( obj: collections.abc.Callable[_P, _T] | DummyAttribute | None = None, ) -> _T: """ Decorator to mark abstract attributes. Must be used in conjunction with the :class:`abc.ABCMeta` metaclass. Parameters ---------- obj: collections.abc.Callable[_P, _T] | DummyAttribute | None, optional The callable attribute to mark as abstract, a new instance of :class:`DummyAttribute` by default. Returns ------- _T The result of executing the callable attribute ``obj``. """ if obj is None: obj = DummyAttribute() setattr(obj, "__is_abstract_attribute__", True) return obj # type: ignore
[docs] class ABCMeta(BaseABCMeta): """ Metaclass to mark abstract classes. Adds support for abstract attributes. If a class has abstract attributes and is instantiated, a NotImplementedError is raised. Usage ----- .. code-block:: python class MyABC(metaclass=ABCMeta): @abstract_attribute def my_abstract_attribute(self): pass class MyClassOK(MyABC): def __init__(self): self.my_abstract_attribute = 1 class MyClassNotOK(MyABC): pass Raises ------ NotImplementedError When it's not possible to instantiate an abstract class with abstract attributes. """
[docs] def __call__(cls, *args, **kwargs): # Collect all abstract-attribute names from the entire MRO abstract_attr_names = set() for base in cls.__mro__: for name, value in base.__dict__.items(): if getattr(value, "__is_abstract_attribute__", False): abstract_attr_names.add(name) for name, value in cls.__dict__.items(): if not getattr(value, "__is_abstract_attribute__", False): abstract_attr_names.discard(name) if abstract_attr_names: raise NotImplementedError( "Can't instantiate abstract class " "{} with abstract attributes: {}".format( cls.__name__, ", ".join(abstract_attr_names) ) ) return super().__call__(*args, **kwargs)