Skip to content

Commit

Permalink
Inject SuperProxy only in patch class methods
Browse files Browse the repository at this point in the history
  • Loading branch information
OmeGak committed Aug 21, 2024
1 parent 7e957bc commit f2abaee
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 94 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## v0.2.1

- Fixed maximum recursion depth exceeded when calling the original method of a
patched class calls `super()`.

## v0.2.0

- Added compatibility for Indico v3.3.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ prefer-active-python = true

[tool.poetry]
name = "indico-patcher"
version = "0.2.0"
version = "0.2.1"
description = "The Swiss Army knife to customize Indico"
repository = "https://github.com/unconventionaldotdev/indico-patcher"
readme = "README.md"
Expand Down
13 changes: 13 additions & 0 deletions src/indico_patcher/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from enum import EnumMeta
from types import FunctionType
from typing import Any
from typing import TypedDict
from typing import Union

from sqlalchemy.ext.hybrid import hybrid_property
Expand All @@ -24,3 +25,15 @@
class PatchedClass:
__patches__: list[type]
__unpatched__: dict[str, dict[str, Any]]


# Dictionary of property descriptor functions
class PropertyDescriptors(TypedDict, total=False):
fget: FunctionType | None
fset: FunctionType | None
fdel: FunctionType | None


# Dictionary of hybrid_property descriptor functions
class HybridPropertyDescriptors(PropertyDescriptors, total=False):
expr: FunctionType | None
67 changes: 45 additions & 22 deletions src/indico_patcher/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@
from __future__ import annotations

import sys
from collections.abc import Callable
from functools import partial
from types import FrameType
from types import FunctionType
from types import MappingProxyType
from typing import Any
from typing import cast

from sqlalchemy.ext.hybrid import hybrid_property

from .types import HybridPropertyDescriptors
from .types import PatchedClass
from .types import PropertyDescriptors
from .types import methodlike
from .types import propertylike

# TODO: Add `fset` and `fdel` descriptors once SuperProxy supports them
SUPER_ENABLED_DESCRIPTORS = {"fget"}
SUPPORTED_DESCRIPTORS = {"fget", "fset", "fdel", "expr"}


class SuperProxy:
"""A proxy for super that allows calling the original class' methods."""
Expand Down Expand Up @@ -53,10 +59,10 @@ def __getattribute__(self_, name: str) -> Any:
return partial(method, obj)

if classmethod := self.orig_class.__unpatched__["classmethods"].get(name):
return classmethod
return partial(classmethod.__func__, self.orig_class)

if staticmethod := self.orig_class.__unpatched__["staticmethods"].get(name):
return staticmethod
return partial(staticmethod)

# Fallback to the original class' member
return getattr(obj, name) if obj else getattr(self.orig_class, name)
Expand Down Expand Up @@ -102,10 +108,9 @@ def patch_member(orig_class: PatchedClass, member_name: str, member: Any) -> Non
# TODO: Patch relationship
# TODO: Patch deferred columns
if isinstance(member, property):
# TODO: Add `fset` and `fdel` descriptors once SuperProxy supports them
_patch_propertylike(orig_class, member_name, member, "properties", ("fget",))
_patch_propertylike(orig_class, member_name, member, "properties", ("fget", "fset", "fdel"))
elif isinstance(member, hybrid_property):
_patch_propertylike(orig_class, member_name, member, "hybrid_properties", ("fget", "expression"))
_patch_propertylike(orig_class, member_name, member, "hybrid_properties", ("fget", "fset", "fdel", "expr"))
elif isinstance(member, FunctionType):
_patch_methodlike(orig_class, member_name, member, "methods")
elif isinstance(member, classmethod):
Expand Down Expand Up @@ -140,16 +145,20 @@ def _patch_propertylike(orig_class: PatchedClass, prop_name: str, prop: property
"""
if category not in {"properties", "hybrid_properties"}:
raise ValueError(f"Unsupported category '{category}'")
if unsupported_fnames := set(fnames) - {"fget", "expression"}:
if unsupported_fnames := set(fnames) - SUPPORTED_DESCRIPTORS:
raise ValueError(f"Unsupported descriptor method '{list(unsupported_fnames)[0]}'")
# Keep a reference to the original property-like member
_store_unpatched(orig_class, prop_name, category)
# Inject super() in the property descriptor methods
# TODO: Figure out how to avoid casting
funcs: PropertyDescriptors | HybridPropertyDescriptors = cast(PropertyDescriptors | HybridPropertyDescriptors, {
fname: _inject_super_proxy(getattr(prop, fname), orig_class) if fname in SUPER_ENABLED_DESCRIPTORS else
getattr(prop, fname)
for fname in fnames
})
new_prop = property(**funcs) if isinstance(prop, property) else hybrid_property(**funcs)
# Replace the original property-like member
setattr(orig_class, prop_name, prop)
# Override super() in the property descriptor methods
for fname in fnames:
if func := getattr(prop, fname, None):
func.__globals__["super"] = SuperProxy(orig_class)
setattr(orig_class, prop_name, new_prop)


def _patch_methodlike(orig_class: PatchedClass, method_name: str, method: methodlike, category: str) -> None:
Expand All @@ -158,19 +167,20 @@ def _patch_methodlike(orig_class: PatchedClass, method_name: str, method: method
:param orig_class: The class to patch
:param method_name: The name of the method-like member to patch in the class
:param method: The method-like object to replace the original member with
:param category: The category of unpached members to store the original member in
:param category: The category of unpatched members to store the original member in
"""
if category not in {"methods", "classmethods", "staticmethods"}:
raise ValueError(f"Unsupported category '{category}'")
# Keep a reference to the original method
# Keep a reference to the original method-like member
_store_unpatched(orig_class, method_name, category)
# Override super() in the method globals
# XXX: Type is casted and type checking is disabled because mypy infers the wrong types
# for __func__ in classmethods (https://github.com/python/mypy/issues/3482)
func = method if isinstance(method, FunctionType) else cast(FunctionType, method.__func__)
new_func = _inject_super_proxy(func, orig_class)
new_method = classmethod(new_func) if isinstance(method, classmethod) else new_func
# Replace the original method
setattr(orig_class, method_name, method)
# Override super() in the method
# XXX: Type is declared explicitly and type checking is disabled because mypy
# infers wrong types for classmethods and staticmethods (https://github.com/python/mypy/issues/3482)
func: Callable = method if isinstance(method, FunctionType) else method.__func__
func.__globals__["super"] = SuperProxy(orig_class)
setattr(orig_class, method_name, new_method)


def _store_unpatched(orig_class: PatchedClass, member_name: str, category: str) -> None:
Expand All @@ -179,6 +189,19 @@ def _store_unpatched(orig_class: PatchedClass, member_name: str, category: str)
:param orig_class: The class to store the reference in
:param member_name: The name of the member to store the reference for
"""
if hasattr(orig_class, member_name):
# None can be a valid value for the member, so we need to check if the member is in the class dict
if member_name in orig_class.__dict__:
# TODO: Log warning if member is already patched
orig_class.__unpatched__[category][member_name] = getattr(orig_class, member_name)
member = orig_class.__dict__[member_name]
orig_class.__unpatched__[category][member_name] = member


def _inject_super_proxy(func: FunctionType, orig_class: PatchedClass) -> FunctionType:
"""Return a new function from which super() will call SuperProxy().
:param func: The function that will get SuperProxy injected
:param orig_class: The original class that will be passed to SuperProxy
"""
globals = func.__globals__.copy()
globals["super"] = SuperProxy(orig_class)
return FunctionType(func.__code__, globals, func.__name__, func.__defaults__, func.__closure__)
Loading

0 comments on commit f2abaee

Please sign in to comment.