Skip to content

Commit

Permalink
:fix: possible recursion problems
Browse files Browse the repository at this point in the history
Going through all members may be problematic when there is an infinite tree.
For example, float objects provide two data descriptor: real and imag.
Thus, you can have:

>>> f = 1.0
>>> f.imag.imag.imag.imag.imag.imag # ...

In this case, getfeatures just reaches the Maximum Depth Recursion.

To avoid this, getfeatures will only traverse through properties of type featuredprop.
  • Loading branch information
linkdd committed Jul 19, 2016
1 parent feed39c commit 6808268
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 25 deletions.
4 changes: 2 additions & 2 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@
# built documents.
#
# The short X.Y version.
version = '1.2'
version = '2.0'
# The full version, including alpha/beta/rc tags.
release = '1.2'
release = '2.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
11 changes: 9 additions & 2 deletions doc/source/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,22 @@ You can now get a list of all features provided by an instance:
# result == [(obj, JSONFeature)]
**NB:** ``getfeatures()`` also look for instance members recursively:
**NB:** ``getfeatures()`` look for featured properties (a special kind of
property, used to indicate that there may be resolvable features):

.. code-block:: python
from link.feature import featuredprop
class Dummy(object):
def __init__(self, *args, **kwargs):
super(Dummy, self).__init__(*args, **kwargs)
self.inner = MyClass()
self._inner = MyClass()
@featuredprop
def inner(self):
return self._inner
obj2 = Dummy()
result = getfeatures(obj2)
Expand Down
10 changes: 7 additions & 3 deletions link/feature/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# -*- coding: utf-8 -*-

from link.feature.core import Feature
from link.feature.core import Feature, featuredprop
from link.feature.core import addfeatures, getfeatures
from link.feature.core import hasfeature, getfeature


__all__ = ['Feature', 'addfeatures', 'getfeatures', 'hasfeature', 'getfeature']
__all__ = [
'Feature', 'featuredprop',
'addfeatures', 'getfeatures',
'hasfeature', 'getfeature'
]

__version__ = '1.2'
__version__ = '2.0'
28 changes: 18 additions & 10 deletions link/feature/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ def __init__(self, obj, *args, **kwargs):
self.obj = obj


class featuredprop(property):
"""
Special property to allow traversal by the ``getfeatures()`` function.
"""


def addfeatures(features):
"""
Add features to a class.
Expand Down Expand Up @@ -74,8 +80,8 @@ def _getfeatures(obj, cache):

return result

if obj not in cache:
cache.add(obj)
if id(obj) not in cache:
cache.add(id(obj))
bases = obj.__class__.mro()

for base in reversed(bases):
Expand All @@ -84,16 +90,18 @@ def _getfeatures(obj, cache):
for feature_cls in getattr(base, '__features__', [])
]

members = [
m
for n, m in getmembers(obj)
if not isroutine(m)
and not isclass(m)
and not n.startswith('__')
featuredpropnames = [
n
for n, m
in getmembers(
obj.__class__,
lambda m: isinstance(m, featuredprop)
)
]

for member in members:
result += _getfeatures(member, cache)
for propname in featuredpropnames:
attr = getattr(obj, propname)
result += _getfeatures(attr, cache)

return result

Expand Down
20 changes: 12 additions & 8 deletions link/feature/test/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from unittest import TestCase, main

from link.feature.core import Feature
from link.feature.core import Feature, featuredprop
from link.feature.core import addfeatures, getfeatures
from link.feature.core import hasfeature, getfeature

Expand All @@ -19,21 +19,25 @@ class TestFeature(TestCase):
def setUp(self):
# create circular reference for getfeatures tests
def inner_init(this, dummy):
self.dummy = dummy
self._dummy = dummy

self.inner_cls = type('InnerDummy', (object,), {
'__init__': inner_init
'__init__': inner_init,
'dummy': featuredprop(lambda s: s._dummy)
})

def dummy_init(this):
this.inner = self.inner_cls(this)
this.list_inner = [this.inner]
this.dict_inner = {
'key': this.inner
this._inner = self.inner_cls(this)
this._list_inner = [this._inner]
this._dict_inner = {
'key': this._inner
}

self.cls = type('Dummy', (object,), {
'__init__': dummy_init
'__init__': dummy_init,
'inner': featuredprop(lambda s: s._inner),
'list_inner': featuredprop(lambda s: s._list_inner),
'dict_inner': featuredprop(lambda s: s._dict_inner),
})

def test_addfeatures(self):
Expand Down

0 comments on commit 6808268

Please sign in to comment.