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
-
-
-
-[![Bitdeli 
Badge](https://d2weczhvl823v0.cloudfront.net/clarete/forbiddenfruit/trend.png)](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)


Reply via email to