Hello community,
here is the log from the commit of package python-forbiddenfruit for
openSUSE:Factory checked in at 2019-05-15 12:34:49
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-forbiddenfruit (Old)
and /work/SRC/openSUSE:Factory/.python-forbiddenfruit.new.5148 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-forbiddenfruit"
Wed May 15 12:34:49 2019 rev:3 rq:702963 version:0.1.3
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-forbiddenfruit/python-forbiddenfruit.changes
2019-03-19 09:59:07.692085233 +0100
+++
/work/SRC/openSUSE:Factory/.python-forbiddenfruit.new.5148/python-forbiddenfruit.changes
2019-05-15 12:34:53.620449564 +0200
@@ -1,0 +2,9 @@
+Fri May 10 08:48:55 UTC 2019 - [email protected]
+
+- version update to 0.1.3
+ * no upstream changelog
+- added sources
+ https://github.com/clarete/forbiddenfruit/issues/30
+ + COPYING.GPL
+
+-------------------------------------------------------------------
Old:
----
0.1.2.tar.gz
New:
----
0.1.3.tar.gz
COPYING.GPL
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-forbiddenfruit.spec ++++++
--- /var/tmp/diff_new_pack.TNifR6/_old 2019-05-15 12:34:55.840443679 +0200
+++ /var/tmp/diff_new_pack.TNifR6/_new 2019-05-15 12:34:55.840443679 +0200
@@ -18,15 +18,17 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
Name: python-forbiddenfruit
-Version: 0.1.2
+Version: 0.1.3
Release: 0
Summary: Python module to patch python built-in objects
-License: LGPL-3.0-or-later
+License: GPL-3.0-only OR MIT
Group: Development/Languages/Python
Url: https://github.com/clarete/forbiddenfruit
-Source:
https://github.com/clarete/forbiddenfruit/archive/%{version}.tar.gz
+Source0:
https://github.com/clarete/forbiddenfruit/archive/%{version}.tar.gz
+# https://github.com/clarete/forbiddenfruit/issues/30
+Source1: COPYING.GPL
BuildRequires: %{python_module devel}
-BuildRequires: %{python_module pytest}
+BuildRequires: %{python_module nose}
BuildRequires: %{python_module setuptools}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
@@ -38,6 +40,7 @@
%prep
%setup -q -n forbiddenfruit-%{version}
+cp %{SOURCE1} .
%build
%python_build
@@ -47,11 +50,11 @@
%python_expand %fdupes %{buildroot}%{$python_sitearch}
%check
-# imports fail
-#%%python_expand PYTHONPATH=$PYTHONPATH:%{buildroot}%{$python_sitearch}
py.test-%{$python_bin_suffix} -v
+%python_expand ln -s %{buildroot}%{$python_sitearch}/ffruit* tests/unit
+%python_expand nosetests-%{$python_bin_suffix}
%files %{python_files}
-%license COPYING
+%license COPYING.GPL COPYING.mit
%doc README.md
%{python_sitearch}/*
++++++ 0.1.2.tar.gz -> 0.1.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/forbiddenfruit-0.1.2/.travis.yml
new/forbiddenfruit-0.1.3/.travis.yml
--- old/forbiddenfruit-0.1.2/.travis.yml 2016-03-31 05:25:16.000000000
+0200
+++ new/forbiddenfruit-0.1.3/.travis.yml 2019-04-20 16:32:31.000000000
+0200
@@ -1,10 +1,11 @@
+dist: xenial
language: python
python:
- - 2.5
- - 2.6
- 2.7
- - 3.2
- - 3.3
+ - 3.4
+ - 3.5
+ - 3.6
+ - 3.7
script: make
install:
- pip install -r development.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/forbiddenfruit-0.1.2/COPYING.mit
new/forbiddenfruit-0.1.3/COPYING.mit
--- old/forbiddenfruit-0.1.2/COPYING.mit 1970-01-01 01:00:00.000000000
+0100
+++ new/forbiddenfruit-0.1.3/COPYING.mit 2019-04-20 16:32:31.000000000
+0200
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2013, 2019 Lincoln Clarete
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/forbiddenfruit-0.1.2/MANIFEST.in
new/forbiddenfruit-0.1.3/MANIFEST.in
--- old/forbiddenfruit-0.1.2/MANIFEST.in 2016-03-31 05:25:16.000000000
+0200
+++ new/forbiddenfruit-0.1.3/MANIFEST.in 2019-04-20 16:32:31.000000000
+0200
@@ -1,3 +1,4 @@
include README.md
include COPYING
include requirements.txt
+graft tests
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/forbiddenfruit-0.1.2/Makefile
new/forbiddenfruit-0.1.3/Makefile
--- old/forbiddenfruit-0.1.2/Makefile 2016-03-31 05:25:16.000000000 +0200
+++ new/forbiddenfruit-0.1.3/Makefile 2019-04-20 16:32:31.000000000 +0200
@@ -9,7 +9,7 @@
CUSTOM_PIP_INDEX=
# </variables>
-all: unit functional integration steadymark
+all: unit functional integration
unit:
@make run_test suite=unit
@@ -28,9 +28,6 @@
--cover-branches --verbosity=2 -s tests/$(suite) ; \
fi
-steadymark:
- @hash steadymark &> /dev/null && steadymark; echo # This echo tells
the shell that everything worked :P
-
prepare: clean install_deps build_test_stub
install_deps:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/forbiddenfruit-0.1.2/README.md
new/forbiddenfruit-0.1.3/README.md
--- old/forbiddenfruit-0.1.2/README.md 2016-03-31 05:25:16.000000000 +0200
+++ new/forbiddenfruit-0.1.3/README.md 2019-04-20 16:32:31.000000000 +0200
@@ -75,8 +75,3 @@
### Logo by
Kimberly Chandler, from The Noun Project
-
-
-
-[](https://bitdeli.com/free
"Bitdeli Badge")
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/forbiddenfruit-0.1.2/forbiddenfruit/__init__.py
new/forbiddenfruit-0.1.3/forbiddenfruit/__init__.py
--- old/forbiddenfruit-0.1.2/forbiddenfruit/__init__.py 2016-03-31
05:25:16.000000000 +0200
+++ new/forbiddenfruit-0.1.3/forbiddenfruit/__init__.py 2019-04-20
16:32:31.000000000 +0200
@@ -1,3 +1,47 @@
+# forbiddenfruit - Patch built-in python objects
+#
+# Copyright (c) 2013,2019 Lincoln de Sousa <[email protected]>
+#
+# This program is dual licensed under GPLv3 and MIT.
+#
+# GPLv3
+# -----
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# MIT
+# ---
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import sys
+from types import FunctionType
import ctypes
import inspect
from functools import wraps
@@ -9,43 +53,180 @@
# Python 3 support
import builtins as __builtin__
-__version__ = '0.1.2'
+__version__ = '0.1.3'
__all__ = 'curse', 'curses', 'reverse'
-Py_ssize_t = \
- hasattr(ctypes.pythonapi, 'Py_InitModule4_64') \
- and ctypes.c_int64 or ctypes.c_int
+Py_ssize_t = ctypes.c_int64 if ctypes.sizeof(ctypes.c_void_p) == 8 else
ctypes.c_int32
+
+
+# dictionary holding references to the allocated function resolution
+# arrays to type objects
+tp_as_dict = {}
+# container to cfunc callbacks
+tp_func_dict = {}
class PyObject(ctypes.Structure):
+ def incref(self):
+ self.ob_refcnt += 1
+
+ def decref(self):
+ self.ob_refcnt -= 1
+
+class PyFile(ctypes.Structure):
+ pass
+
+PyObject_p = ctypes.py_object
+Inquiry_p = ctypes.CFUNCTYPE(ctypes.c_int, PyObject_p)
+# return type is void* to allow ctypes to convert python integers to
+# plain PyObject*
+UnaryFunc_p = ctypes.CFUNCTYPE(ctypes.py_object, PyObject_p)
+BinaryFunc_p = ctypes.CFUNCTYPE(ctypes.py_object, PyObject_p, PyObject_p)
+TernaryFunc_p = ctypes.CFUNCTYPE(ctypes.py_object, PyObject_p, PyObject_p,
PyObject_p)
+LenFunc_p = ctypes.CFUNCTYPE(Py_ssize_t, PyObject_p)
+SSizeArgFunc_p = ctypes.CFUNCTYPE(ctypes.py_object, PyObject_p, Py_ssize_t)
+SSizeObjArgProc_p = ctypes.CFUNCTYPE(ctypes.c_int, PyObject_p, Py_ssize_t,
PyObject_p)
+ObjObjProc_p = ctypes.CFUNCTYPE(ctypes.c_int, PyObject_p, PyObject_p)
+
+FILE_p = ctypes.POINTER(PyFile)
+
+
+def get_not_implemented():
+ namespace = {}
+ name = "_Py_NotImplmented"
+ not_implemented = ctypes.cast(
+ ctypes.pythonapi._Py_NotImplementedStruct, ctypes.py_object)
+
+ ctypes.pythonapi.PyDict_SetItem(
+ ctypes.py_object(namespace),
+ ctypes.py_object(name),
+ not_implemented
+ )
+ return namespace[name]
+
+
+# address of the _Py_NotImplementedStruct singleton
+NotImplementedRet = get_not_implemented()
+
+class PyNumberMethods(ctypes.Structure):
+ _fields_ = [
+ ('nb_add', BinaryFunc_p),
+ ('nb_subtract', BinaryFunc_p),
+ ('nb_multiply', BinaryFunc_p),
+ ('nb_remainder', BinaryFunc_p),
+ ('nb_divmod', BinaryFunc_p),
+ ('nb_power', BinaryFunc_p),
+ ('nb_negative', UnaryFunc_p),
+ ('nb_positive', UnaryFunc_p),
+ ('nb_absolute', UnaryFunc_p),
+ ('nb_bool', Inquiry_p),
+ ('nb_invert', UnaryFunc_p),
+ ('nb_lshift', BinaryFunc_p),
+ ('nb_rshift', BinaryFunc_p),
+ ('nb_and', BinaryFunc_p),
+ ('nb_xor', BinaryFunc_p),
+ ('nb_or', BinaryFunc_p),
+ ('nb_int', UnaryFunc_p),
+ ('nb_reserved', ctypes.c_void_p),
+ ('nb_float', UnaryFunc_p),
+
+ ('nb_inplace_add', BinaryFunc_p),
+ ('nb_inplace_subtract', BinaryFunc_p),
+ ('nb_inplace_multiply', BinaryFunc_p),
+ ('nb_inplace_remainder', BinaryFunc_p),
+ ('nb_inplace_power', TernaryFunc_p),
+ ('nb_inplace_lshift', BinaryFunc_p),
+ ('nb_inplace_rshift', BinaryFunc_p),
+ ('nb_inplace_and', BinaryFunc_p),
+ ('nb_inplace_xor', BinaryFunc_p),
+ ('nb_inplace_or', BinaryFunc_p),
+
+ ('nb_floor_divide', BinaryFunc_p),
+ ('nb_true_divide', BinaryFunc_p),
+ ('nb_inplace_floor_divide', BinaryFunc_p),
+ ('nb_inplace_true_divide', BinaryFunc_p),
+
+ ('nb_index', BinaryFunc_p),
+
+ ('nb_matrix_multiply', BinaryFunc_p),
+ ('nb_inplace_matrix_multiply', BinaryFunc_p),
+ ]
+
+class PySequenceMethods(ctypes.Structure):
+ _fields_ = [
+ ('sq_length', LenFunc_p),
+ ('sq_concat', BinaryFunc_p),
+ ('sq_repeat', SSizeArgFunc_p),
+ ('sq_item', SSizeArgFunc_p),
+ ('was_sq_slice', ctypes.c_void_p),
+ ('sq_ass_item', SSizeObjArgProc_p),
+ ('was_sq_ass_slice', ctypes.c_void_p),
+ ('sq_contains', ObjObjProc_p),
+ ('sq_inplace_concat', BinaryFunc_p),
+ ('sq_inplace_repeat', SSizeArgFunc_p),
+ ]
+
+class PyMappingMethods(ctypes.Structure):
+ pass
+
+class PyTypeObject(ctypes.Structure):
+ pass
+
+class PyAsyncMethods(ctypes.Structure):
pass
+
PyObject._fields_ = [
('ob_refcnt', Py_ssize_t),
- ('ob_type', ctypes.POINTER(PyObject)),
+ ('ob_type', ctypes.POINTER(PyTypeObject)),
+]
+
+PyTypeObject._fields_ = [
+ # varhead
+ ('ob_base', PyObject),
+ ('ob_size', Py_ssize_t),
+ # declaration
+ ('tp_name', ctypes.c_char_p),
+ ('tp_basicsize', Py_ssize_t),
+ ('tp_itemsize', Py_ssize_t),
+ ('tp_dealloc', ctypes.CFUNCTYPE(None, PyObject_p)),
+ ('printfunc', ctypes.CFUNCTYPE(ctypes.c_int, PyObject_p, FILE_p,
ctypes.c_int)),
+ ('getattrfunc', ctypes.CFUNCTYPE(PyObject_p, PyObject_p, ctypes.c_char_p)),
+ ('setattrfunc', ctypes.CFUNCTYPE(ctypes.c_int, PyObject_p,
ctypes.c_char_p, PyObject_p)),
+ ('tp_as_async', ctypes.CFUNCTYPE(PyAsyncMethods)),
+ ('tp_repr', ctypes.CFUNCTYPE(PyObject_p, PyObject_p)),
+ ('tp_as_number', ctypes.POINTER(PyNumberMethods)),
+ ('tp_as_sequence', ctypes.POINTER(PySequenceMethods)),
+ ('tp_as_mapping', ctypes.POINTER(PyMappingMethods)),
+ # ...
]
+# redundant dict of pointee types, because ctypes doesn't allow us
+# to extract the pointee type from the pointer
+PyTypeObject_as_types_dict = {
+ 'tp_as_async': PyAsyncMethods,
+ 'tp_as_number': PyNumberMethods,
+ 'tp_as_sequence': PySequenceMethods,
+ 'tp_as_mapping': PyMappingMethods,
+}
+
+
class SlotsProxy(PyObject):
_fields_ = [('dict', ctypes.POINTER(PyObject))]
def patchable_builtin(klass):
- # It's important to create variables here, we want those objects alive
- # within this whole scope.
name = klass.__name__
target = klass.__dict__
- # Hardcore introspection to find the `PyProxyDict` object that contains the
- # precious `dict` attribute.
proxy_dict = SlotsProxy.from_address(id(target))
namespace = {}
- # This is the way I found to `cast` this `proxy_dict.dict` into a python
- # object, cause the `from_address()` function returns the `py_object`
- # version
+ # This code casts `proxy_dict.dict` into a python object and
+ # `from_address()` returns `py_object`
ctypes.pythonapi.PyDict_SetItem(
ctypes.py_object(namespace),
ctypes.py_object(name),
@@ -58,8 +239,8 @@
def __filtered_dir__(obj=None):
name = hasattr(obj, '__name__') and obj.__name__ or obj.__class__.__name__
if obj is None:
- # Return names from the local scope of the calling frame, taking into
- # account indirection added by __filtered_dir__
+ # Return names from the local scope of the calling frame,
+ # taking into account indirection added by __filtered_dir__
calling_frame = inspect.currentframe().f_back
return sorted(calling_frame.f_locals.keys())
return sorted(set(__dir__(obj)).difference(__hidden_elements__[name]))
@@ -69,6 +250,137 @@
__dir__ = dir
__builtin__.dir = __filtered_dir__
+# build override infomation for dunder methods
+as_number = ('tp_as_number', [
+ ("add", "nb_add"),
+ ("sub", "nb_subtract"),
+ ("mul", "nb_multiply"),
+ ("mod", "nb_remainder"),
+ ("pow", "nb_power"),
+ ("neg", "nb_negative"),
+ ("pos", "nb_positive"),
+ ("abs", "nb_absolute"),
+ ("bool", "nb_bool"),
+ ("inv", "nb_invert"),
+ ("lshift", "nb_lshift"),
+ ("rshift", "nb_rshift"),
+ ("and", "nb_and"),
+ ("xor", "nb_xor"),
+ ("or", "nb_or"),
+ ("int", "nb_int"),
+ ("float", "nb_float"),
+ ("iadd", "nb_inplace_add"),
+ ("isub", "nb_inplace_subtract"),
+ ("imul", "nb_inplace_multiply"),
+ ("imod", "nb_inplace_remainder"),
+ ("ipow", "nb_inplace_power"),
+ ("ilshift", "nb_inplace_lshift"),
+ ("irshift", "nb_inplace_rshift"),
+ ("iadd", "nb_inplace_and"),
+ ("ixor", "nb_inplace_xor"),
+ ("ior", "nb_inplace_or"),
+ ("floordiv", "nb_floor_divide"),
+ ("div", "nb_true_divide"),
+ ("ifloordiv", "nb_inplace_floor_divide"),
+ ("idiv", "nb_inplace_true_divide"),
+ ("index", "nb_index"),
+ ("matmul", "nb_matrix_multiply"),
+ ("imatmul", "nb_inplace_matrix_multiply"),
+])
+
+as_sequence = ("tp_as_sequence", [
+ ("len", "sq_length"),
+ ("concat", "sq_concat"),
+ ("repeat", "sq_repeat"),
+ ("getitem", "sq_item"),
+ ("setitem", "sq_ass_item"),
+ ("contains", "sq_contains"),
+ ("iconcat", "sq_inplace_concat"),
+ ("irepeat", "sq_inplace_repeat"),
+])
+
+as_async = ("tp_as_async", [
+ ("await", "am_await"),
+ ("aiter", "am_aiter"),
+ ("anext", "am_anext"),
+])
+
+override_dict = {}
+for override in [as_number, as_sequence, as_async]:
+ tp_as_name = override[0]
+ for dunder, impl_method in override[1]:
+ override_dict["__{}__".format(dunder)] = (tp_as_name, impl_method)
+
+# divmod isn't a dunder, still make it overridable
+override_dict['divmod()'] = ('tp_as_number', "nb_divmod")
+
+
+def _is_dunder(func_name):
+ return func_name.startswith("__") and func_name.endswith("__")
+
+
+def _curse_special(klass, attr, func):
+ """
+ Curse one of the "dunder" methods, i.e. methods beginning with __ which
have a
+ precial resolution code path
+ """
+ assert isinstance(func, FunctionType)
+
+ tp_as_name, impl_method = override_dict[attr]
+
+ # get the pointer to the correct tp_as_* structure
+ # or create it if it doesn't exist
+ tyobj = PyTypeObject.from_address(id(klass))
+ tp_as_ptr = getattr(tyobj, tp_as_name)
+ struct_ty = PyTypeObject_as_types_dict[tp_as_name]
+ if not tp_as_ptr:
+ # allocate new array
+ tp_as_obj = struct_ty()
+ tp_as_dict[(klass, attr)] = tp_as_obj
+ tp_as_new_ptr = ctypes.cast(ctypes.addressof(tp_as_obj),
+ ctypes.POINTER(struct_ty))
+
+ setattr(tyobj, tp_as_name, tp_as_new_ptr)
+ tp_as = tp_as_ptr[0]
+
+
+ # find the C function type
+ for fname, ftype in struct_ty._fields_:
+ if fname == impl_method:
+ cfunc_t = ftype
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ """
+ This wrapper returns the address of the resulting object as a
+ python integer which is then converted to a pointer by ctypes
+ """
+ try:
+ return func(*args, **kwargs)
+ except NotImplementedError:
+ return NotImplementedRet
+
+ cfunc = cfunc_t(wrapper)
+ tp_func_dict[(klass, attr)] = cfunc
+
+ setattr(tp_as, impl_method, cfunc)
+
+
+def _revert_special(klass, attr):
+ tp_as_name, impl_method = override_dict[attr]
+ tyobj = PyTypeObject.from_address(id(klass))
+ tp_as_ptr = getattr(tyobj, tp_as_name)
+ if tp_as_ptr:
+ tp_as = tp_as_ptr[0]
+
+ struct_ty = PyTypeObject_as_types_dict[tp_as_name]
+ for fname, ftype in struct_ty._fields_:
+ if fname == impl_method:
+ cfunc_t = ftype
+
+ setattr(tp_as, impl_method,
+ ctypes.cast(ctypes.c_void_p(None), cfunc_t))
+
def curse(klass, attr, value, hide_from_dir=False):
"""Curse a built-in `klass` with `attr` set to `value`
@@ -93,15 +405,24 @@
>>> "yo".hello()
"yoyo"
"""
+ if _is_dunder(attr):
+ if sys.version_info < (3, 3):
+ raise NotImplementedError(
+ "Dunder overloading is only supported on Python >= 3.3")
+ _curse_special(klass, attr, value)
+ return
+
dikt = patchable_builtin(klass)
old_value = dikt.get(attr, None)
old_name = '_c_%s' % attr # do not use .format here, it breaks py2.{5,6}
- if old_value:
- dikt[old_name] = old_value
+
+ # Patch the thing
+ dikt[attr] = value
if old_value:
- dikt[attr] = value
+ hide_from_dir = False # It was already in dir
+ dikt[old_name] = old_value
try:
dikt[attr].__name__ = old_value.__name__
@@ -111,8 +432,8 @@
dikt[attr].__qualname__ = old_value.__qualname__
except AttributeError:
pass
- else:
- dikt[attr] = value
+
+ ctypes.pythonapi.PyType_Modified(ctypes.py_object(klass))
if hide_from_dir:
__hidden_elements__[klass.__name__].append(attr)
@@ -140,9 +461,14 @@
AttributeError: 'str' object has no attribute 'strip'
"""
+ if _is_dunder(attr):
+ _revert_special(klass, attr)
+
dikt = patchable_builtin(klass)
del dikt[attr]
+ ctypes.pythonapi.PyType_Modified(ctypes.py_object(klass))
+
def curses(klass, name):
"""Decorator to add decorated method named `name` the class `klass`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/forbiddenfruit-0.1.2/setup.py
new/forbiddenfruit-0.1.3/setup.py
--- old/forbiddenfruit-0.1.2/setup.py 2016-03-31 05:25:16.000000000 +0200
+++ new/forbiddenfruit-0.1.3/setup.py 2019-04-20 16:32:31.000000000 +0200
@@ -1,7 +1,11 @@
# forbiddenfruit - Patch built-in python objects
#
-# Copyright (c) 2013 Lincoln de Sousa <[email protected]>
+# Copyright (c) 2013,2019 Lincoln de Sousa <[email protected]>
#
+# This program is dual licensed under GPLv3 and MIT.
+#
+# GPLv3
+# -----
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -14,6 +18,28 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# MIT
+# ---
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
import os
from setuptools import setup, find_packages, Extension
@@ -21,18 +47,23 @@
ffruit = Extension('ffruit', sources=['tests/unit/ffruit.c'])
+
local_file = lambda f: \
open(os.path.join(os.path.dirname(__file__), f)).read()
if __name__ == '__main__':
setup(
name='forbiddenfruit',
- version='0.1.2',
+ version='0.1.3',
description='Patch python built-in objects',
long_description=local_file('README.md'),
author='Lincoln de Sousa',
- author_email='[email protected]',
+ author_email='[email protected]',
url='https://github.com/clarete/forbiddenfruit',
packages=find_packages(exclude=['*tests*']),
ext_modules=[ffruit],
+ classifiers=[
+ 'License :: OSI Approved :: GNU General Public License v3 or later
(GPLv3+)',
+ 'License :: OSI Approved :: MIT License',
+ ],
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/forbiddenfruit-0.1.2/tests/unit/test_forbidden_fruit.py
new/forbiddenfruit-0.1.3/tests/unit/test_forbidden_fruit.py
--- old/forbiddenfruit-0.1.2/tests/unit/test_forbidden_fruit.py 2016-03-31
05:25:16.000000000 +0200
+++ new/forbiddenfruit-0.1.3/tests/unit/test_forbidden_fruit.py 2019-04-20
16:32:31.000000000 +0200
@@ -1,10 +1,21 @@
+import sys
from datetime import datetime
from forbiddenfruit import curses, curse, reverse
+from types import FunctionType
+from nose.tools import nottest, istest
# Our stub! :)
from . import ffruit
+
+def almost_equal(a, b, e=0.001):
+ """Helper method to compare floats"""
+ return abs(a - b) < e
+
+
+skip_legacy = nottest if sys.version_info < (3, 3) else istest
+
def test_cursing_a_builting_class():
# Given that I have a function that returns *blah*
@@ -120,20 +131,21 @@
assert obj.my_method() == "YoYo"
-def test_overriding_dict_pop():
+def test_overriding_list_append():
"The `curse` function should be able to curse existing symbols"
# Given that I have an instance of a python class
- obj = {'a': 1, 'b': 2}
+ obj = []
# When I curse an instance method
- curse(dict, "pop", lambda self, key: self[key])
+ fn = lambda self, v: self._c_append(v) or self
+ foo = curse(list, "append", fn)
# Then I see that my object was cursed properly
- assert obj.pop('a') == 1
- assert obj.pop('b') == 2
- assert 'a' in obj
- assert 'b' in obj
+ assert obj.append(1) == [1]
+ assert obj.append(2) == [1, 2]
+ assert 1 in obj
+ assert 2 in obj
def test_curses_decorator():
@@ -161,3 +173,118 @@
# Then I see that `dir()` correctly returns a sorted list of those names
assert 'some_name' in dir()
assert dir() == sorted(locals().keys())
+
+
+@skip_legacy
+def test_dunder_func_chaining():
+ """Overload * (mul) operator to to chaining between functions"""
+ def matmul_chaining(self, other):
+ if not isinstance(other, FunctionType):
+ raise NotImplementedError()
+ def wrapper(*args, **kwargs):
+ res = other(*args, **kwargs)
+ if hasattr(res, "__iter__"):
+ return self(*res)
+ return self(res)
+
+ return wrapper
+
+ curse(FunctionType, "__mul__", matmul_chaining)
+ f = lambda x, y: x * y
+ g = lambda x: (x, x)
+
+ squared = f * g
+
+ for i in range(0, 10, 2):
+ assert squared(i) == i ** 2
+
+
+@skip_legacy
+def test_dunder_list_map():
+ """Overload * (__mul__) operator to apply function to a list"""
+ def map_list(func, list_):
+ if not callable(func):
+ raise NotImplementedError()
+ return map(func, list_)
+
+ curse(list, "__mul__", map_list)
+
+ list_ = list(range(10))
+ times_2 = lambda x: x * 2
+
+ assert list(times_2 * list_) == list(range(0, 20, 2))
+
+
+@skip_legacy
+def test_dunder_unary():
+ """Overload ~ operator to compute a derivative of function"""
+ def derive_func(func):
+ e = 0.001
+ def wrapper(x):
+ """Poor man's derivation"""
+ x_0 = x - e
+ x_1 = x + e
+ y_delta = func(x_1) - func(x_0)
+ return y_delta / (2 * e)
+ return wrapper
+
+ curse(FunctionType, "__inv__", derive_func)
+
+ f = lambda x: x**2 + x
+ # true derivation
+ f_ = lambda x: 2*x + 1
+
+ assert almost_equal((~f)(10), f_(10))
+
+
+@skip_legacy
+def test_sequence_dunder():
+ def derive_func(func, deriv_grad):
+ if deriv_grad == 0:
+ return func
+
+ e = 0.0000001
+ def wrapper(x):
+ return (func(x + e) - func(x - e)) / (2 * e)
+ if deriv_grad == 1:
+ return wrapper
+ return wrapper[deriv_grad - 1]
+
+ curse(FunctionType, "__getitem__", derive_func)
+
+ # a function an its derivations
+ f = lambda x: x ** 3 - 2 * x ** 2
+ f_1 = lambda x: 3 * x ** 2 - 4 * x
+ f_2 = lambda x: 6 * x - 4
+
+ for x in range(0, 10):
+ x = float(x) / 10.
+ assert almost_equal(f(x), f[0](x))
+ assert almost_equal(f_1(x), f[1](x))
+ # our hacky derivation becomes numerically unstable here
+ assert almost_equal(f_2(x), f[2](x), e=.01)
+
+
+@skip_legacy
+def test_dunder_list_revert():
+ """Test reversion of a curse with dunders"""
+ def map_list(func, list_):
+ if not callable(func):
+ raise NotImplementedError()
+ return map(func, list_)
+
+ curse(list, "__add__", map_list)
+
+ list_ = list(range(10))
+ times_2 = lambda x: x * 2
+
+ assert list(times_2 + list_) == list(range(0, 20, 2))
+
+ reverse(list, "__add__")
+ try:
+ times_2 + list_
+ except TypeError:
+ pass
+ else:
+ # should always raise an exception
+ assert False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/forbiddenfruit-0.1.2/tox.ini
new/forbiddenfruit-0.1.3/tox.ini
--- old/forbiddenfruit-0.1.2/tox.ini 2016-03-31 05:25:16.000000000 +0200
+++ new/forbiddenfruit-0.1.3/tox.ini 2019-04-20 16:32:31.000000000 +0200
@@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
-envlist = py24, py25, py26, py27, py30, py31, py32, py33
+envlist = py27, py30, py33, py34, py35, py36, py37
[testenv]
commands = make
++++++ COPYING.GPL ++++++
++++ 674 lines (skipped)