Repository: more-itertools/more-itertools (python)

Issue:
last() fails if __reversed__ is None, which is the case for the Mapping abc
Using `last()` on this class:
```
from collections.abc import Mapping
from more_itertools import last
class MyMapping(Mapping):
    def __getitem__(self, key):
        return 1
    def __iter__(self):
        return iter([1])
    def __len__(self):
        return 1

print(last(iter(MyMapping())))
last(MyMapping())
```

fails with:
```
Traceback (most recent call last):
  File "/nix/store/q3zqcqwmybfda7895g91d75pzbaqj6s5-python3-3.9.20-env/lib/python3.9/site-packages/more_itertools/more.py", line 271, in last
    return next(reversed(iterable))
TypeError: 'MyMapping' object is not reversible

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/ibriquem/ws/pyshl/repro.py", line 12, in <module>
    last(MyMapping())
  File "/nix/store/q3zqcqwmybfda7895g91d75pzbaqj6s5-python3-3.9.20-env/lib/python3.9/site-packages/more_itertools/more.py", line 275, in last
    raise ValueError(
ValueError: last() was called on an empty iterable, and no default value was provided.
```

While it works if you first call `iter()` on the object.

This is because `__reversed__` is set to None by the Mapping base class:
https://github.com/python/cpython/blob/4f28afe5456d8ed4a6fe0099337487043e29c912/Lib/_collections_abc.py#L792

I'd expect it to work.

Maybe [this code](https://github.com/more-itertools/more-itertools/blob/569e0ad654a85c77581b318305edc2eb7c4068ae/more_itertools/more.py#L274) could be changed from:
```
if hasattr(iterable, '__reversed__'):
```
to:
```
if getattr(iterable, '__reversed__', None) is not None:
```


Pull request description:
Issue 1001: fix last() when __reversed__ is None
Fixes #1001 by swapping `hasattr` with `getattr`.

We might consider a stronger check that `iterable.__reversed__` is callable, but that seems unnecessary given expected usage.

Patch:
diff --git a/more_itertools/more.py b/more_itertools/more.py
index 3cd42e8..44ec483 100755
--- a/more_itertools/more.py
+++ b/more_itertools/more.py
@@ -283,7 +283,7 @@ def last(iterable, default=_marker):
         if isinstance(iterable, Sequence):
             return iterable[-1]
         # Work around https://bugs.python.org/issue38525
-        if hasattr(iterable, '__reversed__'):
+        if getattr(iterable, '__reversed__', None):
             return next(reversed(iterable))
         return deque(iterable, maxlen=1)[-1]
     except (IndexError, TypeError, StopIteration):


Test patch:
diff --git a/tests/test_more.py b/tests/test_more.py
index b964312..03f6d65 100644
--- a/tests/test_more.py
+++ b/tests/test_more.py
@@ -180,6 +180,16 @@ class LastTests(TestCase):
                 with self.assertRaises(ValueError):
                     mi.last(iterable)
 
+    def test_reversed_is_none(self):
+        # See https://github.com/more-itertools/more-itertools/issues/1001
+        class ReversedIsNone:
+            __reversed__ = None
+
+            def __iter__(self):
+                return iter([1])
+
+        self.assertEqual(mi.last(ReversedIsNone()), 1)
+
 
 class NthOrLastTests(TestCase):
     """Tests for ``nth_or_last()``"""