├── .gitattributes ├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── README.rst ├── blindspin └── __init__.py ├── setup.py └── tests └── test_spinner.py /.gitattributes: -------------------------------------------------------------------------------- 1 | click_spinner/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | *.pyc 4 | *.pyo 5 | *.egg-ignore 6 | *.egg-info 7 | dist 8 | build 9 | docs/_build 10 | click.egg-info 11 | .tox 12 | .cache 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.4' 5 | - '3.5' 6 | notifications: 7 | email: 8 | - yoav@yoavram.com 9 | install: 10 | - pip install click 11 | - pip install six 12 | - python setup.py install 13 | script: py.test 14 | deploy: 15 | provider: pypi 16 | user: yoavram 17 | password: 18 | secure: sSpR5tBWQxRVwnmQjI4jQ0PBGA6/2HDbgJFFt8QgVWHG4lmW0K2K1F2o098j7iYFwZEx1VeNnYNIOOxf1OKilrBVCNv2EMjTQsB9wsHcT5KMEIFEaMEhactYwdZj2poF4FM+xpsfhf7Cuwc2rCojsqRr3bgsqBZj/lHSn1qAAetNvsgak6/9doYnx4tgH6NpW5DCA33vNOBx6v54EotJ1hJ/QtKg/OJUnd6jRKsdj/bLTw0MeB7TJXp9i7rbYV3y36o2Lw/Hz1Tm4dWQUKL4jEWk9/wJKK37auyDJrsREcm9qmS8r3m48l3sFUj1cWrt6AzwdFmdn0X+fS7eWuZ5nw29t5PimxQGsfC3DpICqNkwW95XXHA2HnLjWvlQV3VntdN80/rtLgDRkl6FEaMffo7Pgh/byDvCxiJy+S4brXm9KjKW1LEYjWRP66SiPWTJnZji0gY0bvmdFCRp09QcsGdbBSldsa8VmPkH6nlffdWMNfjPVtvU/x7WICUZHkUSwL1WhJT+v6/tef4I7kqRzVnAHPPxRzr7nuurI4F4OUm6lyzxFfxqUJleKm9pD3Oh5K7Yn9+xkypcxiDLRj0IpGyMDlixTDvcRRdBkRitQTayhDFR58wQ3LTwdlju4Iq347QYg30p0giEHQW37yo848Oa2zCAnI3rqj3/cWuRQ38= 19 | on: 20 | tags: true 21 | distributions: sdist bdist_wheel 22 | repo: click-contrib/click-spinner 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include click_spinner/_version.py 3 | include README.md 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | BlindSpin: Braille Spinner for Python 2 | ===================================== 3 | 4 | Sometimes you would just like to show the user some progress, 5 | but a progress bar is not suitable because you don’t know how much longer it would take. 6 | In these cases you might want to display a simple spinner using the `spinner()` function. 7 | 8 | Example usage:: 9 | 10 | with blindspin.spinner(): 11 | do_something() 12 | do_something_else() 13 | 14 | 15 | It looks like this: 16 | 17 | .. image:: http://media.kennethreitz.com.s3.amazonaws.com/spinner.gif 18 | 19 | Spinner class based on on a [gist by @cevaris](https://gist.github.com/cevaris/79700649f0543584009e). 20 | 21 | 22 | Install 23 | ------- 24 | 25 | :: 26 | 27 | $ pip install blindspin 28 | 29 | 30 | Supports Python 2.6, 2.7, and 3. 31 | 32 | Based on the work of: 33 | --------------------- 34 | 35 | - Yoav Ram (@yoavram) 36 | - Andreas Maier (@andy-maier) 37 | -------------------------------------------------------------------------------- /blindspin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import threading 5 | import time 6 | import itertools 7 | 8 | 9 | class Spinner(object): 10 | spinner_cycle = itertools.cycle(u'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏') 11 | 12 | def __init__(self, beep=False, force=False): 13 | self.beep = beep 14 | self.force = force 15 | self.stop_running = None 16 | self.spin_thread = None 17 | 18 | def start(self): 19 | if sys.stdout.isatty() or self.force: 20 | self.stop_running = threading.Event() 21 | self.spin_thread = threading.Thread(target=self.init_spin) 22 | self.spin_thread.start() 23 | 24 | def stop(self): 25 | if self.spin_thread: 26 | self.stop_running.set() 27 | self.spin_thread.join() 28 | 29 | def init_spin(self): 30 | while not self.stop_running.is_set(): 31 | next_val = next(self.spinner_cycle) 32 | if sys.version_info[0] == 2: 33 | next_val = next_val.encode('utf-8') 34 | sys.stdout.write(next_val) 35 | sys.stdout.flush() 36 | time.sleep(0.07) 37 | sys.stdout.write('\b') 38 | 39 | def __enter__(self): 40 | self.start() 41 | return self 42 | 43 | def __exit__(self, exc_type, exc_val, exc_tb): 44 | self.stop() 45 | if self.beep: 46 | sys.stdout.write('\7') 47 | sys.stdout.flush() 48 | return False 49 | 50 | 51 | def spinner(beep=False, force=False): 52 | """This function creates a context manager that is used to display a 53 | spinner on stdout as long as the context has not exited. 54 | 55 | The spinner is created only if stdout is not redirected, or if the spinner 56 | is forced using the `force` parameter. 57 | 58 | Parameters 59 | ---------- 60 | beep : bool 61 | Beep when spinner finishes. 62 | force : bool 63 | Force creation of spinner even when stdout is redirected. 64 | 65 | Example 66 | ------- 67 | 68 | with spinner(): 69 | do_something() 70 | do_something_else() 71 | 72 | """ 73 | return Spinner(beep, force) 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | from setuptools import setup 5 | 6 | with open('README.rst') as f: 7 | readme = f.read() 8 | 9 | 10 | if sys.argv[-1] == "publish": 11 | os.system("python setup.py sdist bdist_wheel upload") 12 | sys.exit() 13 | 14 | setup( 15 | name='blindspin', 16 | version='2.0.1', 17 | long_description=readme, 18 | packages=['blindspin'], 19 | url='https://github.com/kennethreitz/blindspin', 20 | license='MIT', 21 | author='Kennethreitz', 22 | author_email='me@kennethreitz.org', 23 | description='Braille Spinner for Click', 24 | extras_require={ 25 | 'test': [ 26 | 'click', 27 | 'pytest', 28 | 'six', 29 | ] 30 | } 31 | ) 32 | 33 | -------------------------------------------------------------------------------- /tests/test_spinner.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import time 4 | import tempfile 5 | from six import StringIO 6 | import click 7 | from click.testing import CliRunner 8 | 9 | import click_spinner 10 | 11 | 12 | def test_spinner(): 13 | @click.command() 14 | def cli(): 15 | with click_spinner.spinner(): 16 | for thing in range(10): 17 | pass 18 | 19 | runner = CliRunner() 20 | result = runner.invoke(cli, []) 21 | assert result.exception is None 22 | 23 | 24 | def test_spinner_resume(): 25 | @click.command() 26 | def cli(): 27 | spinner = click_spinner.Spinner() 28 | spinner.start() 29 | for thing in range(10): 30 | pass 31 | spinner.stop() 32 | spinner.start() 33 | for thing in range(10): 34 | pass 35 | spinner.stop() 36 | 37 | runner = CliRunner() 38 | result = runner.invoke(cli, []) 39 | assert result.exception is None 40 | 41 | 42 | def test_spinner_redirect(): 43 | @click.command() 44 | def cli(): 45 | stdout_io = StringIO() 46 | saved_stdout = sys.stdout 47 | sys.stdout = stdout_io # redirect stdout to a string buffer 48 | spinner = click_spinner.Spinner() 49 | spinner.start() 50 | time.sleep(1) # allow time for a few spins 51 | spinner.stop() 52 | sys.stdout = saved_stdout 53 | stdout_io.flush() 54 | stdout_str = stdout_io.getvalue() 55 | assert len(stdout_str) == 0 56 | 57 | runner = CliRunner() 58 | result = runner.invoke(cli, []) 59 | assert result.exception is None 60 | 61 | 62 | def test_spinner_redirect_force(): 63 | @click.command() 64 | def cli(): 65 | stdout_io = StringIO() 66 | saved_stdout = sys.stdout 67 | sys.stdout = stdout_io # redirect stdout to a string buffer 68 | spinner = click_spinner.Spinner(force=True) 69 | spinner.start() 70 | time.sleep(1) # allow time for a few spins 71 | spinner.stop() 72 | sys.stdout = saved_stdout 73 | stdout_io.flush() 74 | stdout_str = stdout_io.getvalue() 75 | assert len(stdout_str) > 0 76 | 77 | runner = CliRunner() 78 | result = runner.invoke(cli, []) 79 | assert result.exception is None 80 | 81 | 82 | def test_spinner_as(): 83 | @click.command() 84 | def cli(): 85 | spinner = click_spinner.spinner() 86 | with spinner as sp: 87 | assert sp == spinner 88 | 89 | runner = CliRunner() 90 | result = runner.invoke(cli, []) 91 | assert result.exception is None 92 | 93 | class CMException(Exception): 94 | pass 95 | 96 | 97 | def test_spinner_exc(): 98 | @click.command() 99 | def cli(): 100 | with click_spinner.spinner(): 101 | for thing in range(10): 102 | if thing == 5: 103 | raise CMException("foo") 104 | 105 | runner = CliRunner() 106 | result = runner.invoke(cli, []) 107 | assert isinstance(result.exception, CMException) 108 | --------------------------------------------------------------------------------