aboutsummaryrefslogtreecommitdiffstats
path: root/env/lib/python3.10/site-packages/pikepdf/_augments.py
diff options
context:
space:
mode:
authorLibravatarLibravatar Biswakalyan Bhuyan <biswa@surgot.in> 2022-11-13 23:46:45 +0530
committerLibravatarLibravatar Biswakalyan Bhuyan <biswa@surgot.in> 2022-11-13 23:46:45 +0530
commit9468226a9e2e2ab8cdd599f1d8538e860ca86120 (patch)
tree0a77ada226d6db80639f96b438bf83e4e756edb5 /env/lib/python3.10/site-packages/pikepdf/_augments.py
downloadidcard-9468226a9e2e2ab8cdd599f1d8538e860ca86120.tar.gz
idcard-9468226a9e2e2ab8cdd599f1d8538e860ca86120.tar.bz2
idcard-9468226a9e2e2ab8cdd599f1d8538e860ca86120.zip
id card generator
Diffstat (limited to 'env/lib/python3.10/site-packages/pikepdf/_augments.py')
-rw-r--r--env/lib/python3.10/site-packages/pikepdf/_augments.py151
1 files changed, 151 insertions, 0 deletions
diff --git a/env/lib/python3.10/site-packages/pikepdf/_augments.py b/env/lib/python3.10/site-packages/pikepdf/_augments.py
new file mode 100644
index 0000000..88fc6e5
--- /dev/null
+++ b/env/lib/python3.10/site-packages/pikepdf/_augments.py
@@ -0,0 +1,151 @@
+# SPDX-FileCopyrightText: 2022 James R. Barlow
+# SPDX-License-Identifier: MPL-2.0
+
+"""A peculiar method of monkeypatching C++ binding classes with Python methods."""
+
+from __future__ import annotations
+
+import inspect
+import platform
+import sys
+from typing import Any, Callable, TypeVar
+
+if sys.version_info >= (3, 8):
+ from typing import Protocol
+else:
+ from typing_extensions import Protocol # pragma: no cover
+
+
+class AugmentedCallable(Protocol):
+ """Protocol for any method, with attached booleans."""
+
+ _augment_override_cpp: bool
+ _augment_if_no_cpp: bool
+
+ def __call__(self, *args, **kwargs) -> Any:
+ """Any function.""" # pragma: no cover
+
+
+def augment_override_cpp(fn: AugmentedCallable) -> AugmentedCallable:
+ """Replace the C++ implementation, if there is one."""
+ fn._augment_override_cpp = True
+ return fn
+
+
+def augment_if_no_cpp(fn: AugmentedCallable) -> AugmentedCallable:
+ """Provide a Python implementation if no C++ implementation exists."""
+ fn._augment_if_no_cpp = True
+ return fn
+
+
+def _is_inherited_method(meth: Callable) -> bool:
+ # Augmenting a C++ with a method that cls inherits from the Python
+ # object is never what we want.
+ return meth.__qualname__.startswith('object.')
+
+
+def _is_augmentable(m: Any) -> bool:
+ return (
+ inspect.isfunction(m) and not _is_inherited_method(m)
+ ) or inspect.isdatadescriptor(m)
+
+
+Tcpp = TypeVar('Tcpp')
+T = TypeVar('T')
+
+
+def augments(cls_cpp: type[Tcpp]):
+ """Attach methods of a Python support class to an existing class.
+
+ This monkeypatches all methods defined in the support class onto an
+ existing class. Example:
+
+ .. code-block:: python
+
+ @augments(ClassDefinedInCpp)
+ class SupportClass:
+ def foo(self):
+ pass
+
+ The Python method 'foo' will be monkeypatched on ClassDefinedInCpp. SupportClass
+ has no meaning on its own and should not be used, but gets returned from
+ this function so IDE code inspection doesn't get too confused.
+
+ We don't subclass because it's much more convenient to monkeypatch Python
+ methods onto the existing Python binding of the C++ class. For one thing,
+ this allows the implementation to be moved from Python to C++ or vice
+ versa. It saves having to implement an intermediate Python subclass and then
+ ensures that the C++ superclass never 'leaks' to pikepdf users. Finally,
+ wrapper classes and subclasses can become problematic if the call stack
+ crosses the C++/Python boundary multiple times.
+
+ Any existing methods may be used, regardless of whether they are defined
+ elsewhere in the support class or in the target class.
+
+ For data fields to work, the target class must be
+ tagged ``py::dynamic_attr`` in pybind11.
+
+ Strictly, the target class does not have to be C++ or derived from pybind11.
+ This works on pure Python classes too.
+
+ THIS DOES NOT work for class methods.
+
+ (Alternative ideas: https://github.com/pybind/pybind11/issues/1074)
+ """
+ OVERRIDE_WHITELIST = {'__eq__', '__hash__', '__repr__'}
+ if platform.python_implementation() == 'PyPy':
+ # Either PyPy or pybind11's interface to PyPy automatically adds a __getattr__
+ OVERRIDE_WHITELIST |= {'__getattr__'} # pragma: no cover
+
+ def class_augment(cls: type[T], cls_cpp: type[Tcpp] = cls_cpp) -> type[T]:
+
+ # inspect.getmembers has different behavior on PyPy - in particular it seems
+ # that a typical PyPy class like cls will have more methods that it considers
+ # methods than CPython does. Our predicate should take care of this.
+ for name, member in inspect.getmembers(cls, predicate=_is_augmentable):
+ if name == '__weakref__':
+ continue
+ if (
+ hasattr(cls_cpp, name)
+ and hasattr(cls, name)
+ and name not in getattr(cls, '__abstractmethods__', set())
+ and name not in OVERRIDE_WHITELIST
+ and not getattr(getattr(cls, name), '_augment_override_cpp', False)
+ ):
+ if getattr(getattr(cls, name), '_augment_if_no_cpp', False):
+ # If tagged as "augment if no C++", we only want the binding to be
+ # applied when the primary class does not provide a C++
+ # implementation. Usually this would be a function that not is
+ # provided by pybind11 in some template.
+ continue
+
+ # If the original C++ class and Python support class both define the
+ # same name, we generally have a conflict, because this is augmentation
+ # not inheritance. However, if the method provided by the support class
+ # is an abstract method, then we can consider the C++ version the
+ # implementation. Also, pybind11 provides defaults for __eq__,
+ # __hash__ and __repr__ that we often do want to override directly.
+
+ raise RuntimeError(
+ f"C++ {cls_cpp} and Python {cls} both define the same "
+ f"non-abstract method {name}: "
+ f"{getattr(cls_cpp, name, '')!r}, "
+ f"{getattr(cls, name, '')!r}"
+ )
+ if inspect.isfunction(member):
+ setattr(cls_cpp, name, member)
+ installed_member = getattr(cls_cpp, name)
+ installed_member.__qualname__ = member.__qualname__.replace(
+ cls.__name__, cls_cpp.__name__
+ )
+ elif inspect.isdatadescriptor(member):
+ setattr(cls_cpp, name, member)
+
+ def disable_init(self):
+ # Prevent initialization of the support class
+ raise NotImplementedError(self.__class__.__name__ + '.__init__')
+
+ cls.__init__ = disable_init # type: ignore
+ return cls
+
+ return class_augment