Skip to content

Commit

Permalink
New DynamicDunder base class.
Browse files Browse the repository at this point in the history
Lets us cleanly define a dynamic dunder which has a trigger and a result.
  • Loading branch information
Mandera committed Jun 2, 2023
1 parent d4caa41 commit 5f6804e
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 78 deletions.
88 changes: 88 additions & 0 deletions generalimport/dunders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from generalimport.generalimport_bottom import _inside_typing
from generalimport.import_catcher import ErrorPars


class DynamicDunder:
""" Inherit to define a dynamic dunder.
All subclasses' triggers are tested for truthy before a MissingOptionalDependency is raised.
Returns result() of first triggered dynamic dunder. """
subclasses: list[type["DynamicDunder"]] = []

def __init_subclass__(cls, **kwargs):
cls.subclasses.append(cls)

def __init__(self, error_pars: ErrorPars):
self.error_pars = error_pars

def trigger(self): return True
def result(self): ...


NON_CALLABLE_DUNDERS = (
# Callable
"__annotations__", "__closure__", "__code__", "__defaults__", "__globals__", "__kwdefaults__",
# Info
"__bases__", "__class__", "__dict__", "__doc__", "__module__", "__name__", "__qualname__", "__all__", "__slots__",
# Pydantic
"_nparams",
)
CALLABLE_CLASS_DUNDERS = [
# Lookup
"__class_getitem__",
]
CALLABLE_DUNDERS = [
# Binary
"__ilshift__", "__invert__", "__irshift__", "__ixor__", "__lshift__", "__rlshift__", "__rrshift__", "__rshift__",
# Callable
"__call__",
# Cast
"__bool__", "__bytes__", "__complex__", "__float__", "__int__", "__iter__", "__hash__",
# Compare
"__eq__", "__ge__", "__gt__", "__instancecheck__", "__le__", "__lt__", "__ne__", "__subclasscheck__",
# Context
"__enter__", "__exit__",
# Delete
"__delattr__", "__delitem__", "__delslice__",
# Info
"__sizeof__", "__subclasses__",
# Iterable
"__len__", "__next__", "__reversed__", "__contains__", "__getitem__", "__setitem__",
# Logic
"__and__", "__iand__", "__ior__", "__or__", "__rand__", "__ror__", "__rxor__", "__xor__",
# Lookup
"__dir__",
# Math
"__abs__", "__add__", "__ceil__", "__divmod__", "__floor__", "__floordiv__", "__iadd__", "__ifloordiv__",
"__imod__", "__imul__", "__ipow__", "__isub__", "__itruediv__", "__mod__", "__mul__", "__neg__", "__pos__",
"__pow__", "__radd__", "__rdiv__", "__rdivmod__", "__rfloordiv__", "__rmod__", "__rmul__", "__round__",
"__rpow__", "__rsub__", "__rtruediv__", "__sub__", "__truediv__", "__trunc__",
# Matrix
"__imatmul__", "__matmul__", "__rmatmul__",
# Object
"__init_subclass__", "__prepare__", "__set_name__",
# Pickle
"__getnewargs__", "__getnewargs_ex__", "__getstate__", "__reduce__", "__reduce_ex__",
# String
"__format__", "__fspath__", "__repr__", "__str__",
# Thread
"__aenter__", "__aexit__", "__aiter__", "__anext__", "__await__",
# Typing
"__origin__",
]


class DynamicEQ(DynamicDunder):
def trigger(self):
return _inside_typing() and self.error_pars.caller == "__eq__" and bool(self.error_pars.args)

def result(self):
other = self.error_pars.args[0]
return id(self) == id(other)


class DynamicHash(DynamicDunder):
def trigger(self):
return _inside_typing() and self.error_pars.caller == "__hash__"

def result(self):
return id(self)
112 changes: 35 additions & 77 deletions generalimport/fake_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,23 @@
import sys
import logging
from functools import partialmethod
from generalimport.exception import MissingOptionalDependency, MissingDependencyException
from generalimport.generalimport_bottom import _inside_typing

from generalimport.exception import MissingDependencyException
from generalimport.dunders import DynamicDunder, NON_CALLABLE_DUNDERS, CALLABLE_CLASS_DUNDERS, CALLABLE_DUNDERS
from generalimport.import_catcher import ErrorPars

EXCEPTION_NAMING_PATTERNS = ["Exception", "Error"]

logger = logging.getLogger("generalimport")


NON_CALLABLE_DUNDERS = (
# Callable
"__annotations__", "__closure__", "__code__", "__defaults__", "__globals__", "__kwdefaults__",
# Info
"__bases__", "__class__", "__dict__", "__doc__", "__module__", "__name__", "__qualname__", "__all__", "__slots__",
# Pydantic
"_nparams",
)


CALLABLE_CLASS_DUNDERS = [
# Lookup
"__class_getitem__",
]

CALLABLE_DUNDERS = [
# Binary
"__ilshift__", "__invert__", "__irshift__", "__ixor__", "__lshift__", "__rlshift__", "__rrshift__", "__rshift__",
# Callable
"__call__",
# Cast
"__bool__", "__bytes__", "__complex__", "__float__", "__int__", "__iter__", "__hash__",
# Compare
"__eq__", "__ge__", "__gt__", "__instancecheck__", "__le__", "__lt__", "__ne__", "__subclasscheck__",
# Context
"__enter__", "__exit__",
# Delete
"__delattr__", "__delitem__", "__delslice__",
# Info
"__sizeof__", "__subclasses__",
# Iterable
"__len__", "__next__", "__reversed__", "__contains__", "__getitem__", "__setitem__",
# Logic
"__and__", "__iand__", "__ior__", "__or__", "__rand__", "__ror__", "__rxor__", "__xor__",
# Lookup
"__dir__",
# Math
"__abs__", "__add__", "__ceil__", "__divmod__", "__floor__", "__floordiv__", "__iadd__", "__ifloordiv__",
"__imod__", "__imul__", "__ipow__", "__isub__", "__itruediv__", "__mod__", "__mul__", "__neg__", "__pos__",
"__pow__", "__radd__", "__rdiv__", "__rdivmod__", "__rfloordiv__", "__rmod__", "__rmul__", "__round__",
"__rpow__", "__rsub__", "__rtruediv__", "__sub__", "__truediv__", "__trunc__",
# Matrix
"__imatmul__", "__matmul__", "__rmatmul__",
# Object
"__init_subclass__", "__prepare__", "__set_name__",
# Pickle
"__getnewargs__", "__getnewargs_ex__", "__getstate__", "__reduce__", "__reduce_ex__",
# String
"__format__", "__fspath__", "__repr__", "__str__",
# Thread
"__aenter__", "__aexit__", "__aiter__", "__anext__", "__await__",
# Typing
"__origin__",
]



class FakeModule:
""" Behaves like a module but any attrs asked for always returns self.
Raises a ModuleNotFoundError when used in any way.
Unhandled use-cases: https://github.com/ManderaGeneral/generalimport/issues?q=is%3Aissue+is%3Aopen+label%3Aunhandled """
__path__ = []
# __args__ = [] # Doesn't seem necessary
SENTINEL = object()

def __init__(self, spec, trigger: Optional[str] = None, catcher=None):
self.name = spec.name
Expand All @@ -84,37 +30,49 @@ def __init__(self, spec, trigger: Optional[str] = None, catcher=None):
self.__spec__ = spec
self.__fake_module__ = True # Should not be needed, but let's keep it for safety?

@staticmethod
def _error_func(name, trigger, caller, catcher, args, kwargs):
required_by = f" (required by '{trigger}')" if trigger else ""
name_part = f"{name}{required_by} " if name else ""

@classmethod
def _dynamic_dunder_check(cls, error_pars: ErrorPars):
for dynamic_dunder_cls in DynamicDunder.subclasses:
dynamic_dunder = dynamic_dunder_cls(error_pars=error_pars)
if dynamic_dunder.trigger():
return dynamic_dunder.result()

return cls.SENTINEL

@classmethod
def _get_error_message(cls, error_pars: ErrorPars):
required_by = f" (required by '{error_pars.trigger}')" if error_pars.trigger else ""
name_part = f"{error_pars.name}{required_by} " if error_pars.name else ""

msg_list = [
f"Optional dependency {name_part}was used but it isn't installed.",
f"Triggered by '{caller}'.",
f"Triggered by '{error_pars.caller}'.",
]
if catcher and catcher.message:
msg_list.append(catcher.message)
if error_pars.catcher is not None and error_pars.catcher.message:
msg_list.append(error_pars.catcher.message)

msg = " ".join(msg_list)
return " ".join(msg_list)

logger.debug(msg=msg)
@classmethod
def _error_func(cls, error_pars):
result = cls._dynamic_dunder_check(error_pars=error_pars)
if result is not cls.SENTINEL:
return result

# print(name, trigger, caller, args, kwargs)
if _inside_typing():
if args and caller == "__eq__":
return name == args[0]
if caller == "__hash__":
return hash(name)
msg = cls._get_error_message(error_pars=error_pars)

logger.debug(msg=msg)
raise MissingDependencyException(msg=msg)

def error_func(self, _caller: str, *args, **kwargs):
self._error_func(name=self.name, trigger=self.trigger, caller=_caller, catcher=self.catcher, args=args, kwargs=kwargs)
error_pars = ErrorPars(name=self.name, trigger=self.trigger, caller=_caller, catcher=self.catcher, args=args, kwargs=kwargs)
return self._error_func(error_pars=error_pars)

@classmethod
def error_func_class(cls, _caller: str, *args, **kwargs):
cls._error_func(name=None, trigger=None, caller=_caller, catcher=None, args=args, kwargs=kwargs)
error_pars = ErrorPars(name=None, trigger=None, caller=_caller, catcher=None, args=args, kwargs=kwargs)
return cls._error_func(error_pars=error_pars)

@staticmethod
def _item_is_exception(item):
Expand All @@ -127,7 +85,7 @@ def _item_is_dunder(item):
def __getattr__(self, item):
fakemodule = FakeModule(spec=self.__spec__, trigger=item, catcher=self.catcher)
if self._item_is_exception(item=item) or self._item_is_dunder(item=item):
fakemodule.error_func(item)
return fakemodule.error_func(item)
return fakemodule


Expand Down
14 changes: 13 additions & 1 deletion generalimport/import_catcher.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import dataclasses
from logging import getLogger
from typing import Optional

from generalimport import _get_previous_frame_filename, _get_top_name, _get_scope_from_filename
from generalimport.generalimport_bottom import _get_previous_frame_filename, _get_top_name, _get_scope_from_filename


class ImportCatcher:
Expand Down Expand Up @@ -54,3 +56,13 @@ def _handle_scope(self):
filename = _get_previous_frame_filename(depth=6)
self.latest_scope_filename = filename
return filename.startswith(self._scope)


@dataclasses.dataclass
class ErrorPars:
name: Optional[str]
trigger: Optional[str]
caller: str
catcher: Optional[ImportCatcher]
args: list | tuple
kwargs: dict

0 comments on commit 5f6804e

Please sign in to comment.