Repository: nltk/nltk

Issue:
FeatStruct.reentrances() returns a list of id's
According to the documentation, FeatStruct.reentrances() should return a list of FeatStruct:

```
    Return a list of all feature structures that can be reached
    from ``self`` by multiple feature paths.
    :rtype: list(FeatStruct)
```

However, it returns a list of object id's, which makes the method practically useless. To make it return FeatStruct's, I think ._find_reentrances() need to store the object itself in the reentrances dict. This affects the following FeatStruct methods: .cyclic(), .reentrances(), ._repr() and ._str()

Alternatively, we can change the method doc, but as I said, the method becomes quite useless.

A third possibility is to return the feature paths themselves. But that's more difficult to implement I guess.

Infinite loop when modifying Tree children 
Tree:s (and ParentedTree:s) go into an infinite loop if you try to change or delete a child by using a string as key:

```
>>> t = nltk.Tree("(a b c (d e) g f r)")
>>> t["2"]
Tree('d', ['e'])
>>> t["2"] = 42
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/peter/Projekt/NLTK/nltk/nltk/tree.py", line 177, in __setitem__
    self[index[0]] = value
  (...)
  File "/Users/peter/Projekt/NLTK/nltk/nltk/tree.py", line 170, in __setitem__
    if isinstance(index, (int, slice)):
RuntimeError: maximum recursion depth exceeded in __instancecheck__
```

The reason is that neither `__setitem__` nor `__delitem__` tries to convert the key to an integer: `self[index[0]] = value`. `__getitem__` does that on the other hand: `return self[int(index[0])]`

The solution is simple, but there are two possibilities:
1. Convert string keys to integers as is done in `__getitem__`: replace `index[0]` with `int(index[0])` in `__setitem__` and `__delitem__`
2. Disallow string keys: replace `int(index[0])` with `index[0]` in `__getitem__`, and add a check `isinstance(index, (list, tuple))` to all three methods

Personally I prefer the second, since it is what Python does for lists.  I don't see the reason why `tree["123"]`should be allowed, and have the same meaning as `tree[1,2,3]`.  However, although I don't think the current behaviour is documented, the change might break some existing code.

What do the benevolent dictators think?



Discussion:
Thanks for spotting this problem.  I think it would be fine to delete this method.

Is it typical to index into a tree using strings? That seems pretty bizarre. I like the second option as well.

I agree that the existing API is inconsistent and that the type conversion is not useful functionality.  I also prefer the second option.


Resolving patch:
diff --git a/nltk/featstruct.py b/nltk/featstruct.py
--- a/nltk/featstruct.py
+++ b/nltk/featstruct.py
@@ -419,17 +419,6 @@ def cyclic(self):
         """
         return self._find_reentrances({})[id(self)]
 
-    def reentrances(self):
-        """
-        Return a list of all feature structures that can be reached
-        from ``self`` by multiple feature paths.
-
-        :rtype: list(FeatStruct)
-        """
-        reentrance_dict = self._find_reentrances({})
-        return [struct for (struct, reentrant) in reentrance_dict.items()
-                if reentrant]
-
     def walk(self):
         """
         Return an iterator that generates this feature structure, and
diff --git a/nltk/internals.py b/nltk/internals.py
--- a/nltk/internals.py
+++ b/nltk/internals.py
@@ -778,15 +778,16 @@ def slice_bounds(sequence, slice_obj, allow_step=False):
     # If allow_step is true, then include the step in our return
     # value tuple.
     if allow_step:
-        if slice_obj.step is None: slice_obj.step = 1
+        step = slice_obj.step
+        if step is None: step = 1
         # Use a recursive call without allow_step to find the slice
         # bounds.  If step is negative, then the roles of start and
         # stop (in terms of default values, etc), are swapped.
-        if slice_obj.step < 0:
+        if step < 0:
             start, stop = slice_bounds(sequence, slice(stop, start))
         else:
             start, stop = slice_bounds(sequence, slice(start, stop))
-        return start, stop, slice_obj.step
+        return start, stop, step
 
     # Otherwise, make sure that no non-default step value is used.
     elif slice_obj.step not in (None, 1):
diff --git a/nltk/tree.py b/nltk/tree.py
--- a/nltk/tree.py
+++ b/nltk/tree.py
@@ -158,18 +158,21 @@ def __radd__(self, v):
     def __getitem__(self, index):
         if isinstance(index, (int, slice)):
             return list.__getitem__(self, index)
-        else:
+        elif isinstance(index, (list, tuple)):
             if len(index) == 0:
                 return self
             elif len(index) == 1:
-                return self[int(index[0])]
+                return self[index[0]]
             else:
-                return self[int(index[0])][index[1:]]
+                return self[index[0]][index[1:]]
+        else:
+            raise TypeError("%s indices must be integers, not %s" %
+                            (type(self).__name__, type(index).__name__))
 
     def __setitem__(self, index, value):
         if isinstance(index, (int, slice)):
             return list.__setitem__(self, index, value)
-        else:
+        elif isinstance(index, (list, tuple)):
             if len(index) == 0:
                 raise IndexError('The tree position () may not be '
                                  'assigned to.')
@@ -177,17 +180,23 @@ def __setitem__(self, index, value):
                 self[index[0]] = value
             else:
                 self[index[0]][index[1:]] = value
+        else:
+            raise TypeError("%s indices must be integers, not %s" %
+                            (type(self).__name__, type(index).__name__))
 
     def __delitem__(self, index):
         if isinstance(index, (int, slice)):
             return list.__delitem__(self, index)
-        else:
+        elif isinstance(index, (list, tuple)):
             if len(index) == 0:
                 raise IndexError('The tree position () may not be deleted.')
             elif len(index) == 1:
                 del self[index[0]]
             else:
                 del self[index[0]][index[1:]]
+        else:
+            raise TypeError("%s indices must be integers, not %s" %
+                            (type(self).__name__, type(index).__name__))
 
     #////////////////////////////////////////////////////////////
     # Basic tree operations
@@ -887,17 +896,20 @@ def __delitem__(self, index):
             # Remove the child from our child list.
             super(AbstractParentedTree, self).__delitem__(index)
 
-        # del ptree[()]
-        elif len(index) == 0:
-            raise IndexError('The tree position () may not be deleted.')
-
-        # del ptree[(i,)]
-        elif len(index) == 1:
-            del self[index[0]]
+        elif isinstance(index, (list, tuple)):
+            # del ptree[()]
+            if len(index) == 0:
+                raise IndexError('The tree position () may not be deleted.')
+            # del ptree[(i,)]
+            elif len(index) == 1:
+                del self[index[0]]
+            # del ptree[i1, i2, i3]
+            else:
+                del self[index[0]][index[1:]]
 
-        # del ptree[i1, i2, i3]
         else:
-            del self[index[0]][index[1:]]
+            raise TypeError("%s indices must be integers, not %s" %
+                            (type(self).__name__, type(index).__name__))
 
     def __setitem__(self, index, value):
         # ptree[start:stop] = value
@@ -940,17 +952,20 @@ def __setitem__(self, index, value):
             # Update our child list.
             super(AbstractParentedTree, self).__setitem__(index, value)
 
-        # ptree[()] = value
-        elif len(index) == 0:
-            raise IndexError('The tree position () may not be assigned to.')
-
-        # ptree[(i,)] = value
-        elif len(index) == 1:
-            self[index[0]] = value
+        elif isinstance(index, (list, tuple)):
+            # ptree[()] = value
+            if len(index) == 0:
+                raise IndexError('The tree position () may not be assigned to.')
+            # ptree[(i,)] = value
+            elif len(index) == 1:
+                self[index[0]] = value
+            # ptree[i1, i2, i3] = value
+            else:
+                self[index[0]][index[1:]] = value
 
-        # ptree[i1, i2, i3] = value
         else:
-            self[index[0]][index[1:]] = value
+            raise TypeError("%s indices must be integers, not %s" %
+                            (type(self).__name__, type(index).__name__))
 
     def append(self, child):
         if isinstance(child, Tree):


Test patch:
diff --git a/nltk/test/doctest_driver.py b/nltk/test/doctest_driver.py
--- a/nltk/test/doctest_driver.py
+++ b/nltk/test/doctest_driver.py
@@ -969,6 +969,9 @@ def update(names, optionflags, verbosity):
                       "interrupt, then report the interrupt and continue")
 
 # Output Comparison options
+IGNORE_EXCEPTION_DETAIL_OPT = Option("--ignore_exception_detail",
+               action="store_const", dest="ignore_exception_detail", const=1, default=0,
+               help="Ignore exception details in the expected output.")
 ELLIPSIS_OPT = Option("--ellipsis",
                action="store_const", dest="ellipsis", const=1, default=0,
                help="Allow \"...\" to be used for ellipsis in the "
@@ -995,7 +998,7 @@ def main():
     optparser.add_option_group(reporting_group)
 
     compare_group = OptionGroup(optparser, 'Output Comparison')
-    compare_group.add_options([ELLIPSIS_OPT, NORMWS_OPT])
+    compare_group.add_options([IGNORE_EXCEPTION_DETAIL_OPT, ELLIPSIS_OPT, NORMWS_OPT])
     optparser.add_option_group(compare_group)
 
     # Extract optionflags and the list of file names.
@@ -1005,8 +1008,9 @@ def main():
     optionflags = (optionvals.udiff * REPORT_UDIFF |
                    optionvals.cdiff * REPORT_CDIFF |
                    optionvals.ellipsis * ELLIPSIS |
+                   optionvals.ignore_exception_detail * IGNORE_EXCEPTION_DETAIL |
                    optionvals.normws * NORMALIZE_WHITESPACE)
-
+ 
     # Check coverage, if requested
     if optionvals.coverage:
         coverage.use_cache(True, cache_file=optionvals.coverage)