Repository: pallets/click

Issue:
Progressbar object is no more an iterator
Hi,

With Click < 7.0, iterating on the bar object was possible:
```
In [8]: with click.progressbar(range(10)) as bar:
   ...:     next(bar)
   ...:     next(bar)
   ...:     
  [#######-----------------------------]   20%
```

With Click 7.0, this is broken:
```
In [2]: with click.progressbar(range(10)) as bar:
   ...:     next(bar)
   ...:     next(bar)
   ...:     
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-85d302084fbb> in <module>()
      1 with click.progressbar(range(10)) as bar:
----> 2     next(bar)
      3     next(bar)
      4 

TypeError: 'ProgressBar' object is not an iterator
```

Is it on purpose ? Seems related to https://github.com/pallets/click/pull/487/commits/82f9c626ffedb284eafe67235b5592be6919106d (#487)


Discussion:
Bisect shows 8780a763aab11f69258acccbcb9d0d97e92ebadb, from #706, as the origin of the issue. Prior to that, `ProgressBar` had a `next` method.

@ThiefMaster and @dsully, were involved in that PR.
Since click is a py2/py3 compatible project, we should consider using [`six.Iterator`](https://pythonhosted.org/six/#six.Iterator) to make it easier to ensure that both the py2 and py3 interfaces are satisfied.

This also seems pretty easy to add a test for, based on the example given.
Due to various issues, we don't use six in the Pallets projects. I think there was actually an issue with streams due to six that was worked around in Click 7.

We try to implement only as much as we need in `_compat`.

I don't mind just adding `__next__` and doing `next = __next__`.
> Due to various issues, we don't use six in the Pallets projects. I think there was actually an issue with streams due to six that was worked around in Click 7.

I'm super-curious, but I'll save looking into this until after my workday.

`six.Iterator` is [a very, very simple class](https://github.com/benjaminp/six/blob/045096c975258ff1ef6ea3aa83c1615189b61850/six.py#L561-L564), so I certainly don't think we need to pull in `six` just for that.

Adding `__next__` and aliasing `next = __next__` on iterables is a more-than-reasonable solution. I'll try to remember to open a PR tonight.

Resolving patch:
diff --git a/click/_termui_impl.py b/click/_termui_impl.py
--- a/click/_termui_impl.py
+++ b/click/_termui_impl.py
@@ -108,6 +108,17 @@ def __iter__(self):
         self.render_progress()
         return self.generator()
 
+    def __next__(self):
+        # Iteration is defined in terms of a generator function,
+        # returned by iter(self); use that to define next(). This works
+        # because `self.iter` is an iterable consumed by that generator,
+        # so it is re-entry safe. Calling `next(self.generator())`
+        # twice works and does "what you want".
+        return next(iter(self))
+
+    # Python 2 compat
+    next = __next__
+
     def is_fast(self):
         return time.time() - self.start <= self.short_limit
 
@@ -270,11 +281,17 @@ def finish(self):
         self.finished = True
 
     def generator(self):
+        """Return a generator which yields the items added to the bar
+        during construction, and updates the progress bar *after* the
+        yielded block returns.
         """
-        Returns a generator which yields the items added to the bar during
-        construction, and updates the progress bar *after* the yielded block
-        returns.
-        """
+        # WARNING: the iterator interface for `ProgressBar` relies on
+        # this and only works because this is a simple generator which
+        # doesn't create or manage additional state. If this function
+        # changes, the impact should be evaluated both against
+        # `iter(bar)` and `next(bar)`. `next()` in particular may call
+        # `self.generator()` repeatedly, and this must remain safe in
+        # order for that interface to work.
         if not self.entered:
             raise RuntimeError('You need to use progress bars in a with block.')
 


Test patch:
diff --git a/tests/test_termui.py b/tests/test_termui.py
--- a/tests/test_termui.py
+++ b/tests/test_termui.py
@@ -185,6 +185,26 @@ def test_progressbar_iter_outside_with_exceptions(runner):
         assert False, 'Expected an exception because of abort-related inputs.'
 
 
+def test_progressbar_is_iterator(runner, monkeypatch):
+    fake_clock = FakeClock()
+
+    @click.command()
+    def cli():
+        with click.progressbar(range(10), label='test') as progress:
+            while True:
+                try:
+                    next(progress)
+                    fake_clock.advance_time()
+                except StopIteration:
+                    break
+
+    monkeypatch.setattr(time, 'time', fake_clock.time)
+    monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: True)
+
+    result = runner.invoke(cli, [])
+    assert result.exception is None
+
+
 def test_choices_list_in_prompt(runner, monkeypatch):
     @click.command()
     @click.option('-g', type=click.Choice(['none', 'day', 'week', 'month']),