pax_global_header00006660000000000000000000000064137522071010014510gustar00rootroot0000000000000052 comment=c5f6ef233d5a3ef6c3db9f44aef03a789cb4a6ce python-halo-0.0.31/000077500000000000000000000000001375220710100140335ustar00rootroot00000000000000python-halo-0.0.31/.coveragerc000066400000000000000000000000351375220710100161520ustar00rootroot00000000000000[report] show_missing = True python-halo-0.0.31/.editorconfig000066400000000000000000000001731375220710100165110ustar00rootroot00000000000000root = true [*] end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 [*.py] indent_size = 4 python-halo-0.0.31/.github/000077500000000000000000000000001375220710100153735ustar00rootroot00000000000000python-halo-0.0.31/.github/CONTRIBUTING.md000066400000000000000000000021261375220710100176250ustar00rootroot00000000000000# How to contribute? ## Development Guidelines Please read [development guidelines](https://github.com/manrajgrover/halo/blob/master/DEVELOPMENT.md) inorder to setup dev environment and run tests. ## Steps to contribute * Look for a issue or open a new one in [project issues](https://github.com/manrajgrover/halo/issues) * Fork the project * Clone to your machine based on your forked project * Create a new branch with an intuitive name. Take a look in concept of [feature branch](https://martinfowler.com/bliki/FeatureBranch.html) * Code your change / fix / new feature * Run tests * When the tests pass you are free to commit and push * Open a Pull Request with a description and the issue reference ## Best Practices Let's try to keep the code as simple and clean as we can. Some good pratices to follow during the contributing process: - **Respect the PEP8**: don't forget to check the [PEP8](https://www.python.org/dev/peps/pep-0008/) complains; - **Write Tests**: **always** write tests for your code - **Keep the Cordiality**: be polite and kind; words like please and thank you are welcome :) python-halo-0.0.31/.github/ISSUE_TEMPLATE.md000066400000000000000000000012611375220710100201000ustar00rootroot00000000000000 ## Description ### System settings - Operating System: - Terminal in use: - Python version: - Halo version: - `pip freeze` output: ### Error ### Expected behaviour ## Steps to recreate ## People to notify python-halo-0.0.31/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000011651375220710100211770ustar00rootroot00000000000000 ## Description of new feature, or changes ## Checklist - [ ] Your branch is up-to-date with the base branch - [ ] You've included at least one test if this is a new feature - [ ] All tests are passing ## Related Issues and Discussions ## People to notify python-halo-0.0.31/.gitignore000066400000000000000000000023621375220710100160260ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # Coverage cover/ # tests tests/*.txt # OS-specific files .DS_Store # IDE settings .vscode/ # Idea .ideapython-halo-0.0.31/.pylintrc000066400000000000000000000356321375220710100157110ustar00rootroot00000000000000[MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. ignore= # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). init-hook= import sys, os if 'VIRTUAL_ENV' not in os.environ: \ sys.exit(0) ve_dir = os.environ['VIRTUAL_ENV'] ve_dir in sys.path or sys.path.insert(0, ve_dir) activate_this = os.path.join(os.path.join(ve_dir, 'bin'), 'activate_this.py') # Fix for windows if not os.path.exists(activate_this): \ activate_this = os.path.join(os.path.join(ve_dir, 'Scripts'), 'activate_this.py') with open(activate_this) as fp: \ exec(fp.read(), dict(__file__=activate_this)) # Use multiple processes to speed up Pylint. jobs=1 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once).You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable= [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio).You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 [BASIC] # Naming hint for argument names argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct argument names argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Naming hint for attribute names attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct attribute names attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Naming hint for class attribute names class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ # Naming hint for class names class-name-hint=[A-Z_][a-zA-Z0-9]+$ # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Naming hint for constant names const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression matching correct constant names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming hint for function names function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct function names function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_ # Include a hint for the correct naming format with invalid-name include-naming-hint=no # Naming hint for inline iteration names inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Naming hint for method names method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct method names method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Naming hint for module names module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. property-classes=abc.abstractproperty # Naming hint for variable names variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct variable names variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=100 # Maximum number of lines in a module max-module-lines=1000 # List of optional constructs for which whitespace checking is disabled. `dict- # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. # `trailing-comma` allows a space between comma and closing bracket: (a, ). # `empty-line` allows space-only lines. no-space-check=trailing-comma,dict-separator # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format logging-modules=logging [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [SPELLING] # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local,Enum # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules=setup # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_,_cb # A regular expression matching the name of dummy variables (i.e. expectedly # not used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,future.builtins [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict,_fields,_replace,_source,_make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs [DESIGN] # Maximum number of arguments for function / method max-args=5 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in a if statement max-bool-expr=5 # Maximum number of branch for function / method body max-branches=12 # Maximum number of locals for function / method body max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of statements in function / method body max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,TERMIOS,Bastion,rexec # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=Exception python-halo-0.0.31/.travis.yml000066400000000000000000000006141375220710100161450ustar00rootroot00000000000000language: python cache: pip dist: xenial os: linux git: depth: 5 matrix: include: - python: '3.5' env: TOXENV=lint - python: '3.5' env: TOXENV=py35 - python: '3.6' env: TOXENV=py36 - python: '3.7' env: TOXENV=py37 - python: '3.8' env: TOXENV=py38 fast_finish: true install: - pip install tox coveralls script: - tox -e $TOXENV after_success: - coveralls python-halo-0.0.31/DEVELOPMENT.md000066400000000000000000000013171375220710100161410ustar00rootroot00000000000000## Development We need to clone the project and prepare the dev environment: ```bash $ git clone https://github.com/manrajgrover/halo.git // or using ssh: git@github.com:manrajgrover/halo.git $ cd halo $ pip install -e . ``` This will install all requirements to use `halo`. You may want to create a virtual environment specifically for this. To install development dependencies, run: ```bash $ pip install -r requirements-dev.txt ``` #### Testing Before submitting a pull request, make sure the code passes all the tests and is clean of lint errors: ```bash $ tox ``` To run tests for specific environment, run: For Python 3.6: ```bash $ tox -e py36 ``` For checking lint issues: ```bash $ tox -e lint ``` python-halo-0.0.31/LICENSE000066400000000000000000000020551375220710100150420ustar00rootroot00000000000000MIT License Copyright (c) 2017 Manraj Singh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-halo-0.0.31/MANIFEST.in000066400000000000000000000001301375220710100155630ustar00rootroot00000000000000include requirements.txt include requirements-dev.txt include LICENSE include README.md python-halo-0.0.31/README.md000066400000000000000000000135301375220710100153140ustar00rootroot00000000000000


halo

[![Build Status](https://travis-ci.com/manrajgrover/halo.svg?branch=master)](https://travis-ci.com/manrajgrover/halo) [![Build status](https://ci.appveyor.com/api/projects/status/wa6t414gltr403ff?svg=true)](https://ci.appveyor.com/project/manrajgrover/halo) [![Coverage Status](https://coveralls.io/repos/github/manrajgrover/halo/badge.svg?branch=master)](https://coveralls.io/github/manrajgrover/halo?branch=master) [![PyPI](https://img.shields.io/pypi/v/halo.svg)](https://github.com/manrajgrover/halo) ![awesome](https://img.shields.io/badge/awesome-yes-green.svg) [![Downloads](https://pepy.tech/badge/halo)](https://pepy.tech/project/halo) [![Downloads](https://pepy.tech/badge/halo/month)](https://pepy.tech/project/halo/month) > Beautiful spinners for terminal, IPython and Jupyter ![halo](https://raw.github.com/manrajgrover/halo/master/art/doge_spin.svg?sanitize=true) ## Install ```shell $ pip install halo ``` ## Usage ```py from halo import Halo spinner = Halo(text='Loading', spinner='dots') spinner.start() # Run time consuming work here # You can also change properties for spinner as and when you want spinner.stop() ``` Alternatively, you can use halo with Python's `with` statement: ```py from halo import Halo with Halo(text='Loading', spinner='dots'): # Run time consuming work here ``` Finally, you can use halo as a decorator: ```py from halo import Halo @Halo(text='Loading', spinner='dots') def long_running_function(): # Run time consuming work here pass long_running_function() ``` ## API #### `Halo([text|text_color|spinner|animation|placement|color|interval|stream|enabled])` ##### `text` *Type*: `str` Text shown along with spinner. ##### `text_color` *Type*: `str` *Values*: `grey`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` Color of the spinner text. Defaults to `None`. ##### `spinner` *Type*: `str|dict` If string, it should be one of the spinners listed in the given [json](https://github.com/sindresorhus/cli-spinners/blob/dac4fc6571059bb9e9bc204711e9dfe8f72e5c6f/spinners.json) file. If a dict is passed, it should define `interval` and `frames`. Something like: ```py { 'interval': 100, 'frames': ['-', '+', '*', '+', '-'] } ``` Defaults to `dots` spinner. For Windows users, it defaults to `line` spinner. ##### `animation` *Type*: `str` *Values*: `bounce`, `marquee` Animation to apply to the text if it's too large and doesn't fit in the terminal. If no animation is defined, the text will be ellipsed. ##### `placement` *Type*: `str` *Values*: `left`, `right` Which side of the text the spinner should be displayed. Defaults to `left` ##### `color` *Type*: `str` *Values*: `grey`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` Color of the spinner. Defaults to `cyan`. ##### `interval` *Type*: `float` Interval between each frame. Defaults to spinner interval (recommended). ##### `stream` *Type*: `file` Stream to write the output. Defaults to `sys.stdout`. ##### `enabled` *Type*: `bool` Enable or disable the spinner. Defaults to `True`. ### Methods Following are the methods available: #### `spinner.start([text])` Starts the spinner. If `text` is passed, it is set as spinner text. Returns the instance. #### `spinner.stop()` Stops and clears the spinner. Returns the instance. #### `spinner.clear()` Clears the spinner. Returns the instance. #### `spinner.render()` Manually renders a new frame. Returns the instance. #### `spinner.frame()` Returns next frame to be rendered. #### `spinner.succeed([text])` ##### `text`: *Type*: `str` Stops the spinner and changes symbol to `✔`. If text is provided, it is persisted else current text is persisted. Returns the instance. #### `spinner.fail([text])` ##### `text`: *Type*: `str` Stops the spinner and changes symbol to `✖`. If text is provided, it is persisted else current text is persisted. Returns the instance. #### `spinner.warn([text])` ##### `text`: *Type*: `str` Stops the spinner and changes symbol to `⚠`. If text is provided, it is persisted else current text is persisted. Returns the instance. #### `spinner.info([text])` ##### `text`: *Type*: `str` Stops the spinner and changes symbol to `ℹ`. If text is provided, it is persisted else current text is persisted. Returns the instance. #### `spinner.stop_and_persist([symbol|text])` Stops the spinner and changes symbol and text. Returns the instance. ##### `symbol` *Type*: `str` Symbol to replace the spinner with. Defaults to `' '`. ##### `text` *Type*: `str` Text to be persisted. Defaults to instance text. ![Persist spin](https://raw.github.com/manrajgrover/halo/master/art/persist_spin.svg?sanitize=true) #### `spinner.text` Change the text of spinner. #### `spinner.color` Change the color of spinner #### `spinner.spinner` Change the spinner itself. #### `spinner.enabled` Enable or disable the spinner. ## How to contribute? Please see [Contributing guidelines](https://github.com/manrajgrover/halo/blob/master/.github/CONTRIBUTING.md) for more information. ## Like it? 🌟 this repo to show support. Let me know you liked it on [Twitter](https://twitter.com/manrajsgrover). Also, share the [project](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fmanrajgrover%2Fhalo&via=manrajsgrover&text=Checkout%20%23halo%20-%20a%20beautiful%20%23terminal%20%23spinners%20library%20for%20%23python&hashtags=github%2C%20pypi). ## Related * [py-spinners](https://github.com/manrajgrover/py-spinners) - Spinners in Python * [py-log-symbols](https://github.com/manrajgrover/py-log-symbols) - Log Symbols in Python * [ora](https://github.com/sindresorhus/ora) - Elegant terminal spinners in JavaScript (inspiration behind this project) ## License [MIT](https://github.com/manrajgrover/halo/blob/master/LICENSE) © Manraj Singh python-halo-0.0.31/appveyor.yml000066400000000000000000000021751375220710100164300ustar00rootroot00000000000000init: - ps: echo $env:TOXENV cache: - '%LOCALAPPDATA%\pip\Cache' clone_depth: 5 environment: fast_finish: true matrix: - TOXENV: 'py35' TOXPYTHON: C:\Python35\python.exe PYTHON_HOME: C:\Python35 PYTHON_VERSION: '3.5' PYTHON_ARCH: '32' - TOXENV: 'py36' TOXPYTHON: C:\Python36\python.exe PYTHON_HOME: C:\Python36 PYTHON_VERSION: '3.6' PYTHON_ARCH: '32' - TOXENV: 'py37' TOXPYTHON: C:\Python37\python.exe PYTHON_HOME: C:\Python37 PYTHON_VERSION: '3.7' PYTHON_ARCH: '32' - TOXENV: 'py38' TOXPYTHON: C:\Python38\python.exe PYTHON_HOME: C:\Python38 PYTHON_VERSION: '3.8' PYTHON_ARCH: '32' - TOXENV: 'lint' TOXPYTHON: C:\Python35\python.exe PYTHON_HOME: C:\Python35 PYTHON_VERSION: '3.5' PYTHON_ARCH: '32' matrix: fast_finish: true install: - '%PYTHON_HOME%\Scripts\pip --version' - '%PYTHON_HOME%\Scripts\pip install tox' - '%PYTHON_HOME%\Scripts\tox --version' - '%PYTHON_HOME%\Scripts\virtualenv --version' build: off test_script: - '%PYTHON_HOME%\Scripts\tox -e %TOXENV%' python-halo-0.0.31/art/000077500000000000000000000000001375220710100146215ustar00rootroot00000000000000python-halo-0.0.31/art/doge_spin.svg000066400000000000000000000510051375220710100173120ustar00rootroot00000000000000$pythonexamples/doge_spin.pySuchSpinsSuchSpinsSuchSpinsSuchSpinsSuchSpinsSuchSpinsSuchSpinsSuchSpinsSuchSpinsSuchSpinsMuchColorsMuchColorsMuchColorsMuchColorsMuchColorsMuchColorsMuchColorsMuchColorsMuchColorsMuchColors💛Veryemojis💙Veryemojis💜Veryemojis💚VeryemojisVeryemojis🦄Wow! python-halo-0.0.31/art/halo.png000066400000000000000000000137751375220710100162670ustar00rootroot00000000000000PNG  IHDR ^PLTE""33DD"DDUU"ff"ww"3ww33DD̻UDD̻DUUUUfwUfUDUD""3fUfUfUfUݻUDݻDݪU̪U̪D̙DݪD̙ÜÜDDDwDwDUw3ݙwDfD"3"wU3Dw3"f3wD3DDUݙU3D3""D3"ݙDw33wwfwD3UUݻfݙwݪ̪fD̈ݻwwf"ݻw̙ݻUݙDUf"UDUff3" MtRNS"3DUfwF*bKGD`ŷ| pHYsHHFk>fIDATx\֕m06a0Hy("(cGjY3I햝LٝݤidӤm6'9^ 0_o!{?}q=Wo.r\(G" X,]rʲy ~E"y8>_L yR,̇p Ko`p膃4,.a,E"7(lD#\P@d!x"LɊ(JoJTIb6N kd2eԹHtZ≤f6f(+RS傔!5-ȥK(2'SB6گ lh M.JLVH%ñ ](3ťim%~_;=Ap˒M'' #[R\^aQHLRYQV,\jw3#v St ILͿ-q!Q}dAM8A;hzG/ׅ,q',j+:ZoF+hj7\?/X6R|ȹtrOΏA"*lSc M1` ?kPhcTcLꪢN8p$0`KE6 "WFTÀ ]ӽR5*C'g`QX8HD1V#p{fil W dPUe[`emxUG{2#D,Iƍh )qр 625e\Rgo6-Pʵ5l̚{@ބ}o3U9& F\N&t8Gkd(~W~gr6ڳCmr|-s\P:,Nͱa{.lvݧp}R!5DZ /T !`Ds'őYNlOKnBkb]afogvQ+(Iͮ]Xm>T|*8<=L@pI:Hp RmBхk pgwvڠ`A8:Jޫ09VƂ1?$m$vaý&S堎TJFWUdMuw`ў15An } Aj'' ύ!4BCƤq~ރ޿GA~H>0iF4Tb,_?M]'Y˜X f:=0a'J?wW@g$L3ǝf AW?@ P522?18;pt&'3gTk; ׻yDsqN(Ja4]RYheq+bPamŖx2rx?]4,,@f:]0*Y|?&jBP-)@JP*_ׅB!h{:ĸkTo^UNv:(!0Kݤ~FT0r66#@b*\Ǩ?, IT5<:> d>hu5xOwU>wC6Uqz߀YdNɘwCW+XeuIp+w_])]EbnQr)"ݳ N`=`}<(9m(!+,,L a=fvE0+ܖyp p:(#GG kgڷ&AY `8ӮJapgR$0wҬ0FkffLB`&#s`IMo=b&*{U/sg)i .ÂkF|w{[Y-F—7>Z*tdĨNU.ʪ g ,ly(9yL[X<+D 0^=|Ɵ mTqzUڨ>{_~B:g/߇Rla>t IB3,S4Hrԡj;7#;=S͎▮*vދO8Y GHa/= yp`w<@B-3xXTP@=EA9T%p2(DlH<~ (lEf<@3v:#!d./5Ӊ, ;zA0L8`nb /?Z SϷZ>"(sBP=a.m"ς ꛝp(Pmu`E7/FۑGH Dנ3X(3)r4¡W|0~DFA{^Yp EZ5dM"3 _|񯩣w#< X beq c14ׯ AjG$(fGb)3,|i_~` %v~FvVo y7@-)RѩlSUB.{*zu0c۵V7_~~BP 3:GNHu\aIy,w͆]o^YGד C B޶jj<&n[iN-HˌlNQ G^hb]'feD_\ AnòIPʆϦ^hIi6+SUՠƣs`}UROEdiDhu/?hxpAR>L\ JQ*h,[*5ѽQ8L,ZÜcG0Ԑ6_ӧh4ȒF:?̓2C>f0qo%)̥ψy/iV˥9Ǯ"#ZI=k|dpl.UC-lPEWo}=B<)O[E4Z-q4,GCkJHgqW:![BHE6.HJ%20 z2A$OTDL<OM 5 @AvꖹZԝgPR] [(t3F.  $5O Dk8hd8ή6fA1AfAH=O<<)h^P@0rC)|냵MAp\H 7鱪c,k) sq48Ex9[˚aи3K)YA~jdXcٞ,:qB@F8Kϭ?hedՄ(34u<Cl^L08|:MTV;}:g:LxW +4;V' õFFz%[{elFpV"+ K,Dr(n3flh&U}r/]ҽ力'h,SBB0뢟mSԁ%PZ^[9#ymn8Oiı(+o^C4,`y<4((n^fNS s|42. U)ph~G$^%Լ0ԬեGw&)X.+ $c!߇U伍 ʍ tȷv(Q(tQ[OrXq\&&S&. "=IA:v^9 nov;--@{ d DD@%#n]cu>}lބpjDl>aMP0 }"~7vٔ gC\K[- 6 I6[p.&mE.j NK!.h=r "lzkLMT FP@\aJ.H[B2~Y9ʐABi40PڷeI̤Tr! /υDJ.O*-ޔĭ̺ LW|IϿ ɉS;)2LOBD"݀_wB.(.ᅩre%pܚ D@V9J}@ 7RיAef.-_s_"<N| o+1sO;GW Mфn)E?w[UbEy4uy]^kk[%tEXtdate:create2017-10-01T05:11:00-04:00%tEXtdate:modify2017-10-01T05:11:00-04:00ϪCIENDB`python-halo-0.0.31/art/persist_spin.svg000066400000000000000000000464561375220710100201030ustar00rootroot00000000000000$pythonexamples/persist_spin.pyLoadingsuccessLoadingsuccessLoadingsuccessLoadingsuccessLoadingsuccessLoadingsuccessLoadingsuccessLoadingsuccessLoadingfailedLoadingfailedLoadingfailedLoadingfailedLoadingfailedLoadingfailedLoadingfailedLoadingunicornLoadingunicornLoadingunicornLoadingunicornLoadingunicornLoadingunicorn🦄LoadingunicornLoadingsuccessLoadingsuccessLoadingsuccessLoadingfailedLoadingfailedLoadingfailedLoadingfailedLoadingunicornLoadingunicornLoadingunicornLoadingunicorn python-halo-0.0.31/examples/000077500000000000000000000000001375220710100156515ustar00rootroot00000000000000python-halo-0.0.31/examples/__init__.py000066400000000000000000000000001375220710100177500ustar00rootroot00000000000000python-halo-0.0.31/examples/colored_text_spin.py000066400000000000000000000013271375220710100217520ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Example for doge spinner ;) """ from __future__ import unicode_literals import os import sys import time sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from halo import Halo spinner = Halo(text='Such Spins', text_color= 'cyan', color='green', spinner='dots') try: spinner.start() time.sleep(2) spinner.text = 'Much Colors' spinner.color = 'magenta' spinner.text_color = 'green' time.sleep(2) spinner.text = 'Very emojis' spinner.spinner = 'hearts' spinner.text_color = 'magenta' time.sleep(2) spinner.stop_and_persist(symbol='🦄 '.encode('utf-8'), text='Wow!') except (KeyboardInterrupt, SystemExit): spinner.stop() python-halo-0.0.31/examples/context_manager.py000066400000000000000000000006051375220710100214020ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Example for context manager """ import os import sys import time sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from halo import Halo with Halo(text='Loading', spinner='dots'): # Run time consuming work here time.sleep(4) with Halo(text='Loading 2', spinner='dots'): # Run time consuming work here time.sleep(4) python-halo-0.0.31/examples/custom_spins.py000066400000000000000000000007651375220710100207610ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Example for custom spinner """ from __future__ import unicode_literals import os import sys import time sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from halo import Halo spinner = Halo( text='Custom Spins', spinner={ 'interval': 100, 'frames': ['-', '+', '*', '+', '-'] } ) try: spinner.start() time.sleep(2) spinner.succeed('It works!') except (KeyboardInterrupt, SystemExit): spinner.stop() python-halo-0.0.31/examples/doge_spin.py000066400000000000000000000011571375220710100201760ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Example for doge spinner ;) """ from __future__ import unicode_literals import os import sys import time sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from halo import Halo spinner = Halo(text='Such Spins', spinner='dots') try: spinner.start() time.sleep(2) spinner.text = 'Much Colors' spinner.color = 'magenta' time.sleep(2) spinner.text = 'Very emojis' spinner.spinner = 'hearts' time.sleep(2) spinner.stop_and_persist(symbol='🦄'.encode('utf-8'), text='Wow!') except (KeyboardInterrupt, SystemExit): spinner.stop() python-halo-0.0.31/examples/loader_spin.py000066400000000000000000000011041375220710100205160ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Example for spinner that looks like loader """ from __future__ import unicode_literals import os import time import sys import random sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from halo import Halo spinner = Halo(text='Downloading dataset.zip', spinner='dots') try: spinner.start() for i in range(100): spinner.text = '{}% Downloaded dataset.zip'.format(i) time.sleep(random.random()) spinner.succeed('Downloaded dataset.zip') except (KeyboardInterrupt, SystemExit): spinner.stop() python-halo-0.0.31/examples/long_text.py000066400000000000000000000013001375220710100202200ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Example for text wrapping animation """ from __future__ import unicode_literals import os import sys import time sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from halo import Halo spinner = Halo(text='This is a text that it is too long. In fact, it exceeds the eighty column standard ' 'terminal width, which forces the text frame renderer to add an ellipse at the end of the ' 'text. This should definitely make it more than 180!', spinner='dots', animation='marquee') try: spinner.start() time.sleep(15) spinner.succeed('End!') except (KeyboardInterrupt, SystemExit): spinner.stop() python-halo-0.0.31/examples/notebook.ipynb000066400000000000000000000124371375220710100205430ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import time\n", "\n", "os.sys.path.append(os.path.dirname(os.path.abspath('./')))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# HaloNotebook" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from halo import HaloNotebook as Halo" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Test example codes\n", "\n", "This frontend (for example, a static rendering on GitHub or NBViewer) doesn't currently support widgets. If you wonder results, run notebook manually.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `persist_spin.py`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "success_message = 'Loading success'\n", "failed_message = 'Loading failed'\n", "unicorn_message = 'Loading unicorn'\n", "\n", "spinner = Halo(text=success_message, spinner='dots')\n", "\n", "try:\n", " spinner.start()\n", " time.sleep(1)\n", " spinner.succeed()\n", " spinner.start(failed_message)\n", " time.sleep(1)\n", " spinner.fail()\n", " spinner.start(unicorn_message)\n", " time.sleep(1)\n", " spinner.stop_and_persist(symbol='🦄'.encode('utf-8'), text=unicorn_message)\n", "except (KeyboardInterrupt, SystemExit):\n", " spinner.stop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `long_text.py`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "spinner = Halo(text='This is a text that it is too long. In fact, it exceeds the eighty column standard '\n", " 'terminal width, which forces the text frame renderer to add an ellipse at the end of the '\n", " 'text. This should definitely make it more than 180!', spinner='dots', animation='marquee')\n", "\n", "try:\n", " spinner.start()\n", " time.sleep(5)\n", " spinner.succeed('End!')\n", "except (KeyboardInterrupt, SystemExit):\n", " spinner.stop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `loader_spin.py`" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "spinner = Halo(text='Downloading dataset.zip', spinner='dots')\n", "\n", "try:\n", " spinner.start()\n", " for i in range(100):\n", " spinner.text = '{}% Downloaded dataset.zip'.format(i)\n", " time.sleep(0.05)\n", " spinner.succeed('Downloaded dataset.zip')\n", "except (KeyboardInterrupt, SystemExit):\n", " spinner.stop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `doge_spin.py`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "spinner = Halo(text='Such Spins', spinner='dots')\n", "\n", "try:\n", " spinner.start()\n", " time.sleep(1)\n", " spinner.text = 'Much Colors'\n", " spinner.color = 'magenta'\n", " time.sleep(1)\n", " spinner.text = 'Very emojis'\n", " spinner.spinner = 'hearts'\n", " time.sleep(1)\n", " spinner.stop_and_persist(symbol='🦄 '.encode('utf-8'), text='Wow!')\n", "except (KeyboardInterrupt, SystemExit):\n", " spinner.stop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `custom_spins.py`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "spinner = Halo(\n", " text='Custom Spins',\n", " spinner={\n", " 'interval': 100,\n", " 'frames': ['-', '+', '*', '+', '-']\n", " }\n", ")\n", "\n", "try:\n", " spinner.start()\n", " time.sleep(2)\n", " spinner.succeed('It works!')\n", "except (KeyboardInterrupt, SystemExit):\n", " spinner.stop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `context_manager.py`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with Halo(text='Loading', spinner='dots'):\n", " # Run time consuming work here\n", " time.sleep(2)\n", "\n", "with Halo(text='Loading 2', spinner='dots') as spinner:\n", " # Run time consuming work here\n", " time.sleep(2)\n", " spinner.succeed('Done!')" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.3" } }, "nbformat": 4, "nbformat_minor": 2 } python-halo-0.0.31/examples/persist_spin.py000066400000000000000000000013301375220710100207420ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Example for persisting spinner """ from __future__ import unicode_literals import os import sys import time sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from halo import Halo success_message = 'Loading success' failed_message = 'Loading failed' unicorn_message = 'Loading unicorn' spinner = Halo(text=success_message, spinner='dots') try: spinner.start() time.sleep(1) spinner.succeed() spinner.start(failed_message) time.sleep(1) spinner.fail() spinner.start(unicorn_message) time.sleep(1) spinner.stop_and_persist(symbol='🦄'.encode('utf-8'), text=unicorn_message) except (KeyboardInterrupt, SystemExit): spinner.stop() python-halo-0.0.31/examples/stream_change.py000066400000000000000000000005401375220710100210220ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Example for changing stream """ from __future__ import unicode_literals, absolute_import import os import sys import time sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from halo import Halo import sys spinner = Halo(stream=sys.stderr) spinner.start('Loading') time.sleep(1) spinner.stop() python-halo-0.0.31/halo/000077500000000000000000000000001375220710100147565ustar00rootroot00000000000000python-halo-0.0.31/halo/__init__.py000066400000000000000000000003551375220710100170720ustar00rootroot00000000000000# -*- coding: utf-8 -*- __author__ = 'Manraj Singh' __email__ = 'manrajsinghgrover@gmail.com' import logging from .halo import Halo from .halo_notebook import HaloNotebook logging.getLogger(__name__).addHandler(logging.NullHandler()) python-halo-0.0.31/halo/_utils.py000066400000000000000000000056631375220710100166410ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Utilities for Halo library. """ import codecs import platform import six try: from shutil import get_terminal_size except ImportError: from backports.shutil_get_terminal_size import get_terminal_size from colorama import init from termcolor import colored init(autoreset=True) def is_supported(): """Check whether operating system supports main symbols or not. Returns ------- boolean Whether operating system supports main symbols or not """ os_arch = platform.system() if os_arch != 'Windows': return True return False def get_environment(): """Get the environment in which halo is running Returns ------- str Environment name """ try: from IPython import get_ipython except ImportError: return 'terminal' try: shell = get_ipython().__class__.__name__ if shell == 'ZMQInteractiveShell': # Jupyter notebook or qtconsole return 'jupyter' elif shell == 'TerminalInteractiveShell': # Terminal running IPython return 'ipython' else: return 'terminal' # Other type (?) except NameError: return 'terminal' def colored_frame(frame, color): """Color the frame with given color and returns. Parameters ---------- frame : str Frame to be colored color : str Color to be applied Returns ------- str Colored frame """ return colored(frame, color, attrs=['bold']) def is_text_type(text): """Check if given parameter is a string or not Parameters ---------- text : * Parameter to be checked for text type Returns ------- bool Whether parameter is a string or not """ if isinstance(text, six.text_type) or isinstance(text, six.string_types): return True return False def decode_utf_8_text(text): """Decode the text from utf-8 format Parameters ---------- text : str String to be decoded Returns ------- str Decoded string """ try: return codecs.decode(text, 'utf-8') except (TypeError, ValueError): return text def encode_utf_8_text(text): """Encodes the text to utf-8 format Parameters ---------- text : str String to be encoded Returns ------- str Encoded string """ try: return codecs.encode(text, 'utf-8', 'ignore') except (TypeError, ValueError): return text def get_terminal_columns(): """Determine the amount of available columns in the terminal Returns ------- int Terminal width """ terminal_size = get_terminal_size() # If column size is 0 either we are not connected # to a terminal or something else went wrong. Fallback to 80. if terminal_size.columns == 0: return 80 else: return terminal_size.columns python-halo-0.0.31/halo/cursor.py000066400000000000000000000024511375220710100166470ustar00rootroot00000000000000""" Source: https://stackoverflow.com/a/10455937/2692667 """ import sys import os if os.name == "nt": import ctypes class _CursorInfo(ctypes.Structure): _fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)] def hide(stream=sys.stdout): """Hide cursor. Parameters ---------- stream: sys.stdout, Optional Defines stream to write output to. """ if os.name == "nt": ci = _CursorInfo() handle = ctypes.windll.kernel32.GetStdHandle(-11) ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) ci.visible = False ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) elif os.name == "posix": stream.write("\033[?25l") stream.flush() def show(stream=sys.stdout): """Show cursor. Parameters ---------- stream: sys.stdout, Optional Defines stream to write output to. """ if os.name == "nt": ci = _CursorInfo() handle = ctypes.windll.kernel32.GetStdHandle(-11) ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) ci.visible = True ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) elif os.name == "posix": stream.write("\033[?25h") stream.flush() python-halo-0.0.31/halo/halo.py000066400000000000000000000403421375220710100162560ustar00rootroot00000000000000# -*- coding: utf-8 -*- # pylint: disable=unsubscriptable-object """Beautiful terminal spinners in Python. """ from __future__ import absolute_import, unicode_literals import atexit import functools import sys import threading import time import halo.cursor as cursor from log_symbols.symbols import LogSymbols from spinners.spinners import Spinners from halo._utils import ( colored_frame, decode_utf_8_text, get_environment, get_terminal_columns, is_supported, is_text_type, encode_utf_8_text, ) class Halo(object): """Halo library. Attributes ---------- CLEAR_LINE : str Code to clear the line """ CLEAR_LINE = "\033[K" SPINNER_PLACEMENTS = ( "left", "right", ) def __init__( self, text="", color="cyan", text_color=None, spinner=None, animation=None, placement="left", interval=-1, enabled=True, stream=sys.stdout, ): """Constructs the Halo object. Parameters ---------- text : str, optional Text to display. text_color : str, optional Color of the text. color : str, optional Color of the text to display. spinner : str|dict, optional String or dictionary representing spinner. String can be one of 60+ spinners supported. animation: str, optional Animation to apply if text is too large. Can be one of `bounce`, `marquee`. Defaults to ellipses. placement: str, optional Side of the text to place the spinner on. Can be `left` or `right`. Defaults to `left`. interval : integer, optional Interval between each frame of the spinner in milliseconds. enabled : boolean, optional Spinner enabled or not. stream : io, optional Output. """ self._color = color self._animation = animation self.spinner = spinner self.text = text self._text_color = text_color self._interval = ( int(interval) if int(interval) > 0 else self._spinner["interval"] ) self._stream = stream self.placement = placement self._frame_index = 0 self._text_index = 0 self._spinner_thread = None self._stop_spinner = None self._spinner_id = None self.enabled = enabled environment = get_environment() def clean_up(): """Handle cell execution""" self.stop() if environment in ("ipython", "jupyter"): from IPython import get_ipython ip = get_ipython() ip.events.register("post_run_cell", clean_up) else: # default terminal atexit.register(clean_up) def __enter__(self): """Starts the spinner on a separate thread. For use in context managers. Returns ------- self """ return self.start() def __exit__(self, type, value, traceback): """Stops the spinner. For use in context managers.""" self.stop() def __call__(self, f): """Allow the Halo object to be used as a regular function decorator.""" @functools.wraps(f) def wrapped(*args, **kwargs): with self: return f(*args, **kwargs) return wrapped @property def spinner(self): """Getter for spinner property. Returns ------- dict spinner value """ return self._spinner @spinner.setter def spinner(self, spinner=None): """Setter for spinner property. Parameters ---------- spinner : dict, str Defines the spinner value with frame and interval """ self._spinner = self._get_spinner(spinner) self._frame_index = 0 self._text_index = 0 @property def text(self): """Getter for text property. Returns ------- str text value """ return self._text["original"] @text.setter def text(self, text): """Setter for text property. Parameters ---------- text : str Defines the text value for spinner """ self._text = self._get_text(text) @property def text_color(self): """Getter for text color property. Returns ------- str text color value """ return self._text_color @text_color.setter def text_color(self, text_color): """Setter for text color property. Parameters ---------- text_color : str Defines the text color value for spinner """ self._text_color = text_color @property def color(self): """Getter for color property. Returns ------- str color value """ return self._color @color.setter def color(self, color): """Setter for color property. Parameters ---------- color : str Defines the color value for spinner """ self._color = color @property def placement(self): """Getter for placement property. Returns ------- str spinner placement """ return self._placement @placement.setter def placement(self, placement): """Setter for placement property. Parameters ---------- placement: str Defines the placement of the spinner """ if placement not in self.SPINNER_PLACEMENTS: raise ValueError( "Unknown spinner placement '{0}', available are {1}".format( placement, self.SPINNER_PLACEMENTS ) ) self._placement = placement @property def spinner_id(self): """Getter for spinner id Returns ------- str Spinner id value """ return self._spinner_id @property def animation(self): """Getter for animation property. Returns ------- str Spinner animation """ return self._animation @animation.setter def animation(self, animation): """Setter for animation property. Parameters ---------- animation: str Defines the animation of the spinner """ self._animation = animation self._text = self._get_text(self._text["original"]) def _check_stream(self): """Returns whether the stream is open, and if applicable, writable Returns ------- bool Whether the stream is open """ if self._stream.closed: return False try: # Attribute access kept separate from invocation, to avoid # swallowing AttributeErrors from the call which should bubble up. check_stream_writable = self._stream.writable except AttributeError: pass else: return check_stream_writable() return True def _write(self, s): """Write to the stream, if writable Parameters ---------- s : str Characters to write to the stream """ if self._check_stream(): self._stream.write(s) def _hide_cursor(self): """Disable the user's blinking cursor """ if self._check_stream() and self._stream.isatty(): cursor.hide(stream=self._stream) def _show_cursor(self): """Re-enable the user's blinking cursor """ if self._check_stream() and self._stream.isatty(): cursor.show(stream=self._stream) def _get_spinner(self, spinner): """Extracts spinner value from options and returns value containing spinner frames and interval, defaults to 'dots' spinner. Parameters ---------- spinner : dict, str Contains spinner value or type of spinner to be used Returns ------- dict Contains frames and interval defining spinner """ default_spinner = Spinners["dots"].value if spinner and type(spinner) == dict: return spinner if is_supported(): if all([is_text_type(spinner), spinner in Spinners.__members__]): return Spinners[spinner].value else: return default_spinner else: return Spinners["line"].value def _get_text(self, text): """Creates frames based on the selected animation Returns ------- self """ animation = self._animation stripped_text = text.strip() # Check which frame of the animation is the widest max_spinner_length = max([len(i) for i in self._spinner["frames"]]) # Subtract to the current terminal size the max spinner length # (-1 to leave room for the extra space between spinner and text) terminal_width = get_terminal_columns() - max_spinner_length - 1 text_length = len(stripped_text) frames = [] if terminal_width < text_length and animation: if animation == "bounce": """ Make the text bounce back and forth """ for x in range(0, text_length - terminal_width + 1): frames.append(stripped_text[x : terminal_width + x]) frames.extend(list(reversed(frames))) elif "marquee": """ Make the text scroll like a marquee """ stripped_text = stripped_text + " " + stripped_text[:terminal_width] for x in range(0, text_length + 1): frames.append(stripped_text[x : terminal_width + x]) elif terminal_width < text_length and not animation: # Add ellipsis if text is larger than terminal width and no animation was specified frames = [stripped_text[: terminal_width - 6] + " (...)"] else: frames = [stripped_text] return {"original": text, "frames": frames} def clear(self): """Clears the line and returns cursor to the start. of line Returns ------- self """ self._write("\r") self._write(self.CLEAR_LINE) return self def _render_frame(self): """Renders the frame on the line after clearing it. """ if not self.enabled: # in case we're disabled or stream is closed while still rendering, # we render the frame and increment the frame index, so the proper # frame is rendered if we're reenabled or the stream opens again. return self.clear() frame = self.frame() output = "\r{}".format(frame) try: self._write(output) except UnicodeEncodeError: self._write(encode_utf_8_text(output)) def render(self): """Runs the render until thread flag is set. Returns ------- self """ while not self._stop_spinner.is_set(): self._render_frame() time.sleep(0.001 * self._interval) return self def frame(self): """Builds and returns the frame to be rendered Returns ------- self """ frames = self._spinner["frames"] frame = frames[self._frame_index] if self._color: frame = colored_frame(frame, self._color) self._frame_index += 1 self._frame_index = self._frame_index % len(frames) text_frame = self.text_frame() return "{0} {1}".format( *[ (text_frame, frame) if self._placement == "right" else (frame, text_frame) ][0] ) def text_frame(self): """Builds and returns the text frame to be rendered Returns ------- self """ if len(self._text["frames"]) == 1: if self._text_color: return colored_frame(self._text["frames"][0], self._text_color) # Return first frame (can't return original text because at this point it might be ellipsed) return self._text["frames"][0] frames = self._text["frames"] frame = frames[self._text_index] self._text_index += 1 self._text_index = self._text_index % len(frames) if self._text_color: return colored_frame(frame, self._text_color) return frame def start(self, text=None): """Starts the spinner on a separate thread. Parameters ---------- text : None, optional Text to be used alongside spinner Returns ------- self """ if text is not None: self.text = text if self._spinner_id is not None: return self if not (self.enabled and self._check_stream()): return self self._hide_cursor() self._stop_spinner = threading.Event() self._spinner_thread = threading.Thread(target=self.render) self._spinner_thread.setDaemon(True) self._render_frame() self._spinner_id = self._spinner_thread.name self._spinner_thread.start() return self def stop(self): """Stops the spinner and clears the line. Returns ------- self """ if self._spinner_thread and self._spinner_thread.is_alive(): self._stop_spinner.set() self._spinner_thread.join() if self.enabled: self.clear() self._frame_index = 0 self._spinner_id = None self._show_cursor() return self def succeed(self, text=None): """Shows and persists success symbol and text and exits. Parameters ---------- text : None, optional Text to be shown alongside success symbol. Returns ------- self """ return self.stop_and_persist(symbol=LogSymbols.SUCCESS.value, text=text) def fail(self, text=None): """Shows and persists fail symbol and text and exits. Parameters ---------- text : None, optional Text to be shown alongside fail symbol. Returns ------- self """ return self.stop_and_persist(symbol=LogSymbols.ERROR.value, text=text) def warn(self, text=None): """Shows and persists warn symbol and text and exits. Parameters ---------- text : None, optional Text to be shown alongside warn symbol. Returns ------- self """ return self.stop_and_persist(symbol=LogSymbols.WARNING.value, text=text) def info(self, text=None): """Shows and persists info symbol and text and exits. Parameters ---------- text : None, optional Text to be shown alongside info symbol. Returns ------- self """ return self.stop_and_persist(symbol=LogSymbols.INFO.value, text=text) def stop_and_persist(self, symbol=" ", text=None): """Stops the spinner and persists the final frame to be shown. Parameters ---------- symbol : str, optional Symbol to be shown in final frame text: str, optional Text to be shown in final frame Returns ------- self """ if not self.enabled: return self symbol = decode_utf_8_text(symbol) if text is not None: text = decode_utf_8_text(text) else: text = self._text["original"] text = text.strip() if self._text_color: text = colored_frame(text, self._text_color) self.stop() output = "{0} {1}\n".format( *[(text, symbol) if self._placement == "right" else (symbol, text)][0] ) try: self._write(output) except UnicodeEncodeError: self._write(encode_utf_8_text(output)) return self python-halo-0.0.31/halo/halo_notebook.py000066400000000000000000000061571375220710100201640ustar00rootroot00000000000000from __future__ import absolute_import, print_function, unicode_literals import sys import threading import halo.cursor as cursor from halo import Halo from halo._utils import colored_frame, decode_utf_8_text class HaloNotebook(Halo): def __init__( self, text="", color="cyan", text_color=None, spinner=None, placement="left", animation=None, interval=-1, enabled=True, stream=sys.stdout, ): super(HaloNotebook, self).__init__( text=text, color=color, text_color=text_color, spinner=spinner, placement=placement, animation=animation, interval=interval, enabled=enabled, stream=stream, ) self.output = self._make_output_widget() def _make_output_widget(self): from ipywidgets.widgets import Output return Output() # TODO: using property and setter def _output(self, text=""): return ({"name": "stdout", "output_type": "stream", "text": text},) def clear(self): if not self.enabled: return self with self.output: self.output.outputs += self._output("\r") self.output.outputs += self._output(self.CLEAR_LINE) self.output.outputs = self._output() return self def _render_frame(self): frame = self.frame() output = "\r{}".format(frame) with self.output: self.output.outputs += self._output(output) def start(self, text=None): if text is not None: self.text = text if not self.enabled or self._spinner_id is not None: return self if self._stream.isatty(): cursor.hide() self.output = self._make_output_widget() from IPython.display import display display(self.output) self._stop_spinner = threading.Event() self._spinner_thread = threading.Thread(target=self.render) self._spinner_thread.setDaemon(True) self._render_frame() self._spinner_id = self._spinner_thread.name self._spinner_thread.start() return self def stop_and_persist(self, symbol=" ", text=None): """Stops the spinner and persists the final frame to be shown. Parameters ---------- symbol : str, optional Symbol to be shown in final frame text: str, optional Text to be shown in final frame Returns ------- self """ if not self.enabled: return self symbol = decode_utf_8_text(symbol) if text is not None: text = decode_utf_8_text(text) else: text = self._text["original"] text = text.strip() if self._text_color: text = colored_frame(text, self._text_color) self.stop() output = "\r{} {}\n".format( *[(text, symbol) if self._placement == "right" else (symbol, text)][0] ) with self.output: self.output.outputs = self._output(output) python-halo-0.0.31/requirements-dev.txt000066400000000000000000000001031375220710100200650ustar00rootroot00000000000000coverage>=4.4.1 nose>=1.3.7 pylint>=1.7.2 tox>=2.8.2 twine>=1.12.1 python-halo-0.0.31/requirements.txt000066400000000000000000000002231375220710100173140ustar00rootroot00000000000000backports.shutil_get_terminal_size>=1.0.0;python_version < '3.3' log_symbols>=0.0.14 spinners>=0.0.24 termcolor>=1.1.0 colorama>=0.3.9 six>=1.12.0 python-halo-0.0.31/setup.cfg000066400000000000000000000000501375220710100156470ustar00rootroot00000000000000[metadata] description-file = README.md python-halo-0.0.31/setup.py000066400000000000000000000027331375220710100155520ustar00rootroot00000000000000import io from setuptools import ( setup, find_packages, ) # pylint: disable=no-name-in-module,import-error def dependencies(file): with open(file) as f: return f.read().splitlines() with io.open("README.md", encoding="utf-8") as infile: long_description = infile.read() setup( name="halo", packages=find_packages(exclude=("tests", "examples")), version="0.0.31", license="MIT", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only", ], python_requires=">=3.4", description="Beautiful terminal spinners in Python", long_description=long_description, long_description_content_type="text/markdown", author="Manraj Singh", author_email="manrajsinghgrover@gmail.com", url="https://github.com/manrajgrover/halo", keywords=[ "console", "loading", "indicator", "progress", "cli", "spinner", "spinners", "terminal", "term", "busy", "wait", "idle", ], install_requires=dependencies("requirements.txt"), tests_require=dependencies("requirements-dev.txt"), include_package_data=True, extras_require={"ipython": ["IPython==5.7.0", "ipywidgets==7.1.0",]}, ) python-halo-0.0.31/tests/000077500000000000000000000000001375220710100151755ustar00rootroot00000000000000python-halo-0.0.31/tests/__init__.py000066400000000000000000000000001375220710100172740ustar00rootroot00000000000000python-halo-0.0.31/tests/_utils.py000066400000000000000000000024471375220710100170550ustar00rootroot00000000000000"""Utilities for tests. """ import codecs import re def strip_ansi(string): """Strip ANSI encoding from given string. Parameters ---------- string : str String from which encoding needs to be removed Returns ------- str Encoding free string """ pattern = r'(\x1b\[|\x9b)[^@-_]*[@-_]|\x1b[@-_]' return re.sub(pattern, '', string, flags=re.I) def find_colors(string): """Find colors from given string Parameters ---------- string : str String from which colors need to be find Returns ------- str List of found colors """ return re.findall(r'\[\d\dm', string, flags=re.I) def decode_utf_8_text(text): """Decodes the text from utf-8 format. Parameters ---------- text : str Text to be decoded Returns ------- str Decoded text """ try: return codecs.decode(text, 'utf-8') except (TypeError, ValueError): return text def encode_utf_8_text(text): """Encodes the text to utf-8 format Parameters ---------- text : str Text to be encoded Returns ------- str Encoded text """ try: return codecs.encode(text, 'utf-8') except (TypeError, ValueError): return text python-halo-0.0.31/tests/test_halo.py000066400000000000000000000515371375220710100175440ustar00rootroot00000000000000# -*- coding: utf-8 -*- """This module tests Halo spinners. """ import io import os import re import sys import time import unittest try: from cStringIO import StringIO except ImportError: from io import StringIO from spinners.spinners import Spinners from halo import Halo from halo._utils import get_terminal_columns, is_supported from tests._utils import strip_ansi, find_colors, encode_utf_8_text, decode_utf_8_text from termcolor import COLORS if sys.version_info.major == 2: get_coded_text = encode_utf_8_text else: get_coded_text = decode_utf_8_text if is_supported(): frames = [get_coded_text(frame) for frame in Spinners['dots'].value['frames']] default_spinner = Spinners['dots'].value else: frames = [get_coded_text(frame) for frame in Spinners['line'].value['frames']] default_spinner = Spinners['line'].value class SpecificException(Exception): """A unique exc class we know only our tests would raise""" class TestHalo(unittest.TestCase): """Test Halo enum for attribute values. """ TEST_FOLDER = os.path.dirname(os.path.abspath(__file__)) def setUp(self): """Set up things before beginning of each test. """ self._stream_file = os.path.join(self.TEST_FOLDER, 'test.txt') self._stream = io.open(self._stream_file, 'w+') self._stream_no_tty = StringIO() def _get_test_output(self, no_ansi=True): """Clean the output from stream and return it in list form. Returns ------- list Clean output from stream """ self._stream.seek(0) data = self._stream.readlines() output = {} output_text = [] output_colors = [] for line in data: clean_line = strip_ansi(line.strip('\n')) if no_ansi else line.strip('\n') if clean_line != '': output_text.append(get_coded_text(clean_line)) colors_found = find_colors(line.strip('\n')) if colors_found: tmp = [] for color in colors_found: tmp.append(re.sub(r'[^0-9]', '', color, flags=re.I)) output_colors.append(tmp) output['text'] = output_text output['colors'] = output_colors return output def test_basic_spinner(self): """Test the basic of basic spinners. """ spinner = Halo(text='foo', spinner='dots', stream=self._stream) spinner.start() time.sleep(1) spinner.stop() output = self._get_test_output()['text'] self.assertEqual(output[0], '{} foo'.format(frames[0])) self.assertEqual(output[1], '{} foo'.format(frames[1])) self.assertEqual(output[2], '{} foo'.format(frames[2])) def test_text_spinner_color(self): """Test basic spinner with available colors color (both spinner and text) """ for color, color_int in COLORS.items(): self._stream_file = os.path.join(self.TEST_FOLDER, 'test.txt') self._stream = io.open(self._stream_file, 'w+') spinner = Halo( text='foo', text_color=color, color=color, spinner='dots', stream=self._stream ) spinner.start() time.sleep(1) spinner.stop() output = self._get_test_output()['colors'] # check if spinner colors match self.assertEqual(color_int, int(output[0][0])) self.assertEqual(color_int, int(output[1][0])) self.assertEqual(color_int, int(output[2][0])) # check if text colors match self.assertEqual(color_int, int(output[0][1])) self.assertEqual(color_int, int(output[1][1])) self.assertEqual(color_int, int(output[2][1])) def test_spinner_getter(self): instance = Halo() if is_supported(): default_spinner_value = "dots" else: default_spinner_value = "line" instance.spinner = default_spinner_value self.assertEqual(default_spinner, instance.spinner) instance.spinner = "This_spinner_do_not_exist" self.assertEqual(default_spinner, instance.spinner) instance.spinner = -123 self.assertEqual(default_spinner, instance.spinner) def test_text_stripping(self): """Test the text being stripped before output. """ spinner = Halo(text='foo\n', spinner='dots', stream=self._stream) spinner.start() time.sleep(1) spinner.succeed('foo\n') output = self._get_test_output()['text'] self.assertEqual(output[0], '{} foo'.format(frames[0])) self.assertEqual(output[1], '{} foo'.format(frames[1])) self.assertEqual(output[2], '{} foo'.format(frames[2])) pattern = re.compile(r'(✔|v) foo', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) def test_text_ellipsing(self): """Test the text gets ellipsed if it's too long """ text = 'This is a text that it is too long. In fact, it exceeds the eighty column standard ' \ 'terminal width, which forces the text frame renderer to add an ellipse at the end of the ' \ 'text. ' * 6 spinner = Halo(text=text, spinner='dots', stream=self._stream) spinner.start() time.sleep(1) spinner.succeed('End!') output = self._get_test_output()['text'] terminal_width = get_terminal_columns() # -6 of the ' (...)' ellipsis, -2 of the spinner and space self.assertEqual(output[0], '{} {} (...)'.format(frames[0], text[:terminal_width - 6 - 2])) self.assertEqual(output[1], '{} {} (...)'.format(frames[1], text[:terminal_width - 6 - 2])) self.assertEqual(output[2], '{} {} (...)'.format(frames[2], text[:terminal_width - 6 - 2])) pattern = re.compile(r'(✔|v) End!', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) def test_text_animation(self): """Test the text gets animated when it is too long """ text = 'This is a text that it is too long. In fact, it exceeds the eighty column standard ' \ 'terminal width, which forces the text frame renderer to add an ellipse at the end of the ' \ 'text. ' * 6 spinner = Halo(text=text, spinner='dots', stream=self._stream, animation='marquee') spinner.start() time.sleep(1) spinner.succeed('End!') output = self._get_test_output()['text'] terminal_width = get_terminal_columns() self.assertEqual(output[0], '{} {}'.format(frames[0], text[:terminal_width - 2])) self.assertEqual(output[1], '{} {}'.format(frames[1], text[1:terminal_width - 1])) self.assertEqual(output[2], '{} {}'.format(frames[2], text[2:terminal_width])) pattern = re.compile(r'(✔|v) End!', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) def test_context_manager(self): """Test the basic of basic spinners used through the with statement. """ with Halo(text='foo', spinner='dots', stream=self._stream): time.sleep(1) output = self._get_test_output()['text'] self.assertEqual(output[0], '{} foo'.format(frames[0])) self.assertEqual(output[1], '{} foo'.format(frames[1])) self.assertEqual(output[2], '{} foo'.format(frames[2])) def test_context_manager_exceptions(self): """Test Halo context manager allows exceptions to bubble up """ with self.assertRaises(SpecificException): with Halo(text='foo', spinner='dots', stream=self._stream): raise SpecificException def test_decorator_spinner(self): """Test basic usage of spinners with the decorator syntax.""" @Halo(text="foo", spinner="dots", stream=self._stream) def decorated_function(): time.sleep(1) decorated_function() output = self._get_test_output()['text'] self.assertEqual(output[0], '{} foo'.format(frames[0])) self.assertEqual(output[1], '{} foo'.format(frames[1])) self.assertEqual(output[2], '{} foo'.format(frames[2])) def test_decorator_exceptions(self): """Test Halo decorator allows exceptions to bubble up""" @Halo(text="foo", spinner="dots", stream=self._stream) def decorated_function(): raise SpecificException with self.assertRaises(SpecificException): decorated_function() def test_initial_title_spinner(self): """Test Halo with initial title. """ spinner = Halo('bar', stream=self._stream) spinner.start() time.sleep(1) spinner.stop() output = self._get_test_output()['text'] self.assertEqual(output[0], '{} bar'.format(frames[0])) self.assertEqual(output[1], '{} bar'.format(frames[1])) self.assertEqual(output[2], '{} bar'.format(frames[2])) def test_id_not_created_before_start(self): """Test Spinner ID not created before start. """ spinner = Halo(stream=self._stream) self.assertEqual(spinner.spinner_id, None) def test_ignore_multiple_start_calls(self): """Test ignoring of multiple start calls. """ spinner = Halo(stream=self._stream) spinner.start() spinner_id = spinner.spinner_id spinner.start() self.assertEqual(spinner.spinner_id, spinner_id) spinner.stop() def test_chaining_start(self): """Test chaining start with constructor """ spinner = Halo(stream=self._stream).start() spinner_id = spinner.spinner_id self.assertIsNotNone(spinner_id) spinner.stop() def test_succeed(self): """Test succeed method """ spinner = Halo(stream=self._stream) spinner.start('foo') spinner.succeed('foo') output = self._get_test_output()['text'] pattern = re.compile(r'(✔|v) foo', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_succeed_with_new_text(self): """Test succeed method with new text """ spinner = Halo(stream=self._stream) spinner.start('foo') spinner.succeed('bar') output = self._get_test_output()['text'] pattern = re.compile(r'(✔|v) bar', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_info(self): """Test info method """ spinner = Halo(stream=self._stream) spinner.start('foo') spinner.info() output = self._get_test_output()['text'] pattern = re.compile(r'(ℹ|¡) foo', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_fail(self): """Test fail method """ spinner = Halo(stream=self._stream) spinner.start('foo') spinner.fail() output = self._get_test_output()['text'] pattern = re.compile(r'(✖|×) foo', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_warning(self): """Test warn method """ spinner = Halo(stream=self._stream) spinner.start('foo') spinner.warn('Warning!') output = self._get_test_output()['text'] pattern = re.compile(r'(⚠|!!) Warning!', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_spinner_getters_setters(self): """Test spinner getters and setters. """ spinner = Halo() self.assertEqual(spinner.text, '') self.assertIsNone(spinner.text_color, None) self.assertEqual(spinner.color, 'cyan') self.assertIsNone(spinner.spinner_id) spinner.spinner = 'dots12' spinner.text = 'bar' spinner.text_color = 'red' spinner.color = 'red' self.assertEqual(spinner.text, 'bar') self.assertEqual(spinner.text_color, 'red') self.assertEqual(spinner.color, 'red') if is_supported(): self.assertEqual(spinner.spinner, Spinners['dots12'].value) else: self.assertEqual(spinner.spinner, default_spinner) spinner.spinner = 'dots11' if is_supported(): self.assertEqual(spinner.spinner, Spinners['dots11'].value) else: self.assertEqual(spinner.spinner, default_spinner) spinner.spinner = 'foo_bar' self.assertEqual(spinner.spinner, default_spinner) # Color is None spinner.text_color = None spinner.color = None spinner.start() spinner.stop() self.assertIsNone(spinner.text_color) self.assertIsNone(spinner.color) def test_unavailable_spinner_defaults(self): """Test unavailable spinner defaults. """ spinner = Halo('dot') self.assertEqual(spinner.text, 'dot') self.assertEqual(spinner.spinner, default_spinner) def test_if_enabled(self): """Test if spinner is enabled """ spinner = Halo(text='foo', enabled=False, stream=self._stream) spinner.start() time.sleep(1) spinner.fail() output = self._get_test_output()['text'] self.assertEqual(len(output), 0) self.assertEqual(output, []) def test_writing_disabled_on_closed_stream(self): """Test no I/O is performed on closed streams """ # BytesIO supports the writable() method, while StringIO does not, in # some versions of Python. We want to check whether the stream is # writable (e.g. for file streams which can be open but not writable), # but only if the stream supports it — otherwise we assume # open == writable. for io_class in (io.StringIO, io.BytesIO): stream = io_class() stream.close() # sanity checks self.assertTrue(stream.closed) self.assertRaises(ValueError, stream.isatty) self.assertRaises(ValueError, stream.write, u'') try: spinner = Halo(text='foo', stream=stream) spinner.start() time.sleep(0.5) spinner.stop() except ValueError as e: self.fail('Attempted to write to a closed stream: {}'.format(e)) def test_closing_stream_before_stopping(self): """Test no I/O is performed on streams closed before stop is called """ stream = io.StringIO() spinner = Halo(text='foo', stream=stream) spinner.start() time.sleep(0.5) # no exception raised after closing the stream means test was successful try: stream.close() time.sleep(0.5) spinner.stop() except ValueError as e: self.fail('Attempted to write to a closed stream: {}'.format(e)) def test_closing_stream_before_persistent(self): """Test no I/O is performed on streams closed before stop_and_persist is called """ stream = io.StringIO() spinner = Halo(text='foo', stream=stream) spinner.start() time.sleep(0.5) # no exception raised after closing the stream means test was successful try: stream.close() time.sleep(0.5) spinner.stop_and_persist('done') except ValueError as e: self.fail('Attempted to write to a closed stream: {}'.format(e)) def test_setting_enabled_property(self): """Test if spinner stops writing when enabled property set to False """ spinner = Halo(text='foo', stream=self._stream) spinner.start() time.sleep(0.5) spinner.enabled = False bytes_written = self._stream.tell() time.sleep(0.5) spinner.stop() total_bytes_written = self._stream.tell() self.assertEqual(total_bytes_written, bytes_written) def test_spinner_interval_default(self): """Test proper assignment of the default interval value. """ spinner = Halo() self.assertEqual(spinner._interval, default_spinner['interval']) def test_spinner_interval_argument(self): """Test proper assignment of the interval value from the constructor argument. """ spinner = Halo(interval=123) self.assertEqual(spinner._interval, 123) def test_spinner_interval_dict(self): """Test proper assignment of the interval value from a dictionary. """ spinner = Halo(spinner={'interval': 321, 'frames': ['+', '-']}) self.assertEqual(spinner._interval, 321) def test_invalid_placement(self): """Test invalid placement of spinner. """ with self.assertRaises(ValueError): Halo(placement='') Halo(placement='foo') Halo(placement=None) spinner = Halo(placement='left') with self.assertRaises(ValueError): spinner.placement = '' spinner.placement = 'foo' spinner.placement = None def test_default_placement(self): """Test default placement of spinner. """ spinner = Halo() self.assertEqual(spinner.placement, 'left') def test_right_placement(self): """Test right placement of spinner. """ spinner = Halo(text='foo', placement='right', stream=self._stream) spinner.start() time.sleep(1) output = self._get_test_output()['text'] (text, _) = output[-1].split(' ') self.assertEqual(text, 'foo') spinner.succeed() output = self._get_test_output()['text'] (text, symbol) = output[-1].split(' ') pattern = re.compile(r"(✔|v)", re.UNICODE) self.assertEqual(text, 'foo') self.assertRegexpMatches(symbol, pattern) spinner.stop() def test_bounce_animation(self): def filler_text(n_chars): return "_" * n_chars terminal_width = get_terminal_columns() text = "{}abc".format(filler_text(terminal_width)) expected_frames_without_appended_spinner = [ "{}".format(filler_text(terminal_width - 2)), "{}".format(filler_text(terminal_width - 2)), "{}".format(filler_text(terminal_width - 2)), "{}a".format(filler_text(terminal_width - 3)), "{}ab".format(filler_text(terminal_width - 4)), "{}abc".format(filler_text(terminal_width - 5)), "{}abc".format(filler_text(terminal_width - 5)), "{}ab".format(filler_text(terminal_width - 4)), "{}a".format(filler_text(terminal_width - 3)), "{}".format(filler_text(terminal_width - 2)), "{}".format(filler_text(terminal_width - 2)), "{}".format(filler_text(terminal_width - 2)), ] # Prepend the actual spinner expected_frames = [ "{} {}".format(frames[idx % frames.__len__()], frame) for idx, frame in enumerate(expected_frames_without_appended_spinner) ] spinner = Halo(text, animation="bounce", stream=self._stream) spinner.start() # Sleep a full bounce cycle time.sleep(1.2) spinner.stop() output = self._get_test_output()['text'] zipped_expected_and_actual_frame = zip(expected_frames, output) for multiple_frames in zipped_expected_and_actual_frame: expected_frame, actual_frame = multiple_frames self.assertEquals(expected_frame, actual_frame) def test_animation_setter(self): spinner = Halo("Asdf") spinner.animation = "bounce" self.assertEquals("bounce", spinner.animation) spinner.animation = "marquee" self.assertEquals("marquee", spinner.animation) def test_spinner_color(self): """Test ANSI escape characters are present """ for color, color_int in COLORS.items(): self._stream = io.open(self._stream_file, 'w+') # reset stream spinner = Halo(color=color, stream=self._stream) spinner.start() spinner.stop() output = self._get_test_output(no_ansi=False) output_merged = [arr for c in output['colors'] for arr in c] self.assertEquals(str(color_int) in output_merged, True) def test_redirect_stdout(self): """Test redirect stdout """ out = self._stream try: self._stream = self._stream_no_tty spinner = Halo(text='foo', spinner='dots', stream=self._stream) spinner.start() time.sleep(1) spinner.stop() output = self._get_test_output()['text'] finally: self._stream = out self.assertIn('foo', output[0]) def tearDown(self): pass if __name__ == '__main__': SUITE = unittest.TestLoader().loadTestsFromTestCase(TestHalo) unittest.TextTestRunner(verbosity=2).run(SUITE) python-halo-0.0.31/tests/test_halo_notebook.py000066400000000000000000000335621375220710100214420ustar00rootroot00000000000000# -*- coding: utf-8 -*- """This module tests HaloNotebook spinners. """ import os import re import sys import time import unittest from spinners.spinners import Spinners from halo import HaloNotebook from halo._utils import get_terminal_columns, is_supported from tests._utils import decode_utf_8_text, encode_utf_8_text, find_colors, strip_ansi from termcolor import COLORS if sys.version_info.major == 2: get_coded_text = encode_utf_8_text else: get_coded_text = decode_utf_8_text if is_supported(): frames = [get_coded_text(frame) for frame in Spinners['dots'].value['frames']] default_spinner = Spinners['dots'].value else: frames = [get_coded_text(frame) for frame in Spinners['line'].value['frames']] default_spinner = Spinners['line'].value class TestHaloNotebook(unittest.TestCase): """Test HaloNotebook enum for attribute values. """ TEST_FOLDER = os.path.dirname(os.path.abspath(__file__)) def setUp(self): """Set up things before beginning of each test. """ pass def _get_test_output(self, spinner, no_ansi=True): """Clean the output from Output widget and return it in list form. Returns ------- list Clean output from Output widget """ output = {} output_text = [] output_colors = [] for line in spinner.output.outputs: if no_ansi: clean_line = strip_ansi(line['text'].strip('\r')) else: clean_line = line['text'].strip('\r') if clean_line != '': output_text.append(get_coded_text(clean_line)) colors_found = find_colors(line['text'].strip('\r')) if colors_found: tmp = [] for color in colors_found: tmp.append(re.sub(r'[^0-9]', '', color, flags=re.I)) output_colors.append(tmp) output['text'] = output_text output['colors'] = output_colors return output def test_basic_spinner(self): """Test the basic of basic spinners. """ spinner = HaloNotebook(text='foo', spinner='dots') spinner.start() time.sleep(1) output = self._get_test_output(spinner)['text'] spinner.stop() self.assertEqual(output[0], '{} foo'.format(frames[0])) self.assertEqual(output[1], '{} foo'.format(frames[1])) self.assertEqual(output[2], '{} foo'.format(frames[2])) self.assertEqual(spinner.output.outputs, spinner._output('')) def test_text_spinner_color(self): """Test basic spinner with available colors color (both spinner and text) """ for color, color_int in COLORS.items(): spinner = HaloNotebook(text='foo', text_color=color, color=color, spinner='dots') spinner.start() time.sleep(1) output = self._get_test_output(spinner)['colors'] spinner.stop() # check if spinner colors match self.assertEqual(color_int, int(output[0][0])) self.assertEqual(color_int, int(output[1][0])) self.assertEqual(color_int, int(output[2][0])) # check if text colors match self.assertEqual(color_int, int(output[0][1])) self.assertEqual(color_int, int(output[1][1])) self.assertEqual(color_int, int(output[2][1])) def test_text_stripping(self): """Test the text being stripped before output. """ spinner = HaloNotebook(text='foo\n', spinner='dots') spinner.start() time.sleep(1) output = self._get_test_output(spinner)['text'] self.assertEqual(output[0], '{} foo'.format(frames[0])) self.assertEqual(output[1], '{} foo'.format(frames[1])) self.assertEqual(output[2], '{} foo'.format(frames[2])) spinner.succeed('foo\n') output = self._get_test_output(spinner)['text'] pattern = re.compile(r'(✔|v) foo', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) def test_text_ellipsing(self): """Test the text gets ellipsed if it's too long """ text = 'This is a text that it is too long. In fact, it exceeds the eighty column standard ' \ 'terminal width, which forces the text frame renderer to add an ellipse at the end of the ' \ 'text. ' * 6 spinner = HaloNotebook(text=text, spinner='dots') spinner.start() time.sleep(1) output = self._get_test_output(spinner)['text'] terminal_width = get_terminal_columns() # -6 of the ' (...)' ellipsis, -2 of the spinner and space self.assertEqual(output[0], '{} {} (...)'.format(frames[0], text[:terminal_width - 6 - 2])) self.assertEqual(output[1], '{} {} (...)'.format(frames[1], text[:terminal_width - 6 - 2])) self.assertEqual(output[2], '{} {} (...)'.format(frames[2], text[:terminal_width - 6 - 2])) spinner.succeed('End!') output = self._get_test_output(spinner)['text'] pattern = re.compile(r'(✔|v) End!', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) def test_text_animation(self): """Test the text gets animated when it is too long """ text = 'This is a text that it is too long. In fact, it exceeds the eighty column standard ' \ 'terminal width, which forces the text frame renderer to add an ellipse at the end of the ' \ 'text. ' * 6 spinner = HaloNotebook(text=text, spinner='dots', animation='marquee') spinner.start() time.sleep(1) output = self._get_test_output(spinner)['text'] terminal_width = get_terminal_columns() self.assertEqual(output[0], '{} {}'.format(frames[0], text[:terminal_width - 2])) self.assertEqual(output[1], '{} {}'.format(frames[1], text[1:terminal_width - 1])) self.assertEqual(output[2], '{} {}'.format(frames[2], text[2:terminal_width])) spinner.succeed('End!') output = self._get_test_output(spinner)['text'] pattern = re.compile(r'(✔|v) End!', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) def test_context_manager(self): """Test the basic of basic spinners used through the with statement. """ with HaloNotebook(text='foo', spinner='dots') as spinner: time.sleep(1) output = self._get_test_output(spinner)['text'] self.assertEqual(output[0], '{} foo'.format(frames[0])) self.assertEqual(output[1], '{} foo'.format(frames[1])) self.assertEqual(output[2], '{} foo'.format(frames[2])) self.assertEqual(spinner.output.outputs, spinner._output('')) def test_decorator_spinner(self): """Test basic usage of spinners with the decorator syntax.""" @HaloNotebook(text="foo", spinner="dots") def decorated_function(): time.sleep(1) spinner = decorated_function.__closure__[1].cell_contents output = self._get_test_output(spinner)['text'] return output output = decorated_function() self.assertEqual(output[0], '{} foo'.format(frames[0])) self.assertEqual(output[1], '{} foo'.format(frames[1])) self.assertEqual(output[2], '{} foo'.format(frames[2])) def test_initial_title_spinner(self): """Test Halo with initial title. """ spinner = HaloNotebook('bar') spinner.start() time.sleep(1) output = self._get_test_output(spinner)['text'] spinner.stop() self.assertEqual(output[0], '{} bar'.format(frames[0])) self.assertEqual(output[1], '{} bar'.format(frames[1])) self.assertEqual(output[2], '{} bar'.format(frames[2])) self.assertEqual(spinner.output.outputs, spinner._output('')) def test_id_not_created_before_start(self): """Test Spinner ID not created before start. """ spinner = HaloNotebook() self.assertEqual(spinner.spinner_id, None) def test_ignore_multiple_start_calls(self): """Test ignoring of multiple start calls. """ spinner = HaloNotebook() spinner.start() spinner_id = spinner.spinner_id spinner.start() self.assertEqual(spinner.spinner_id, spinner_id) spinner.stop() def test_chaining_start(self): """Test chaining start with constructor """ spinner = HaloNotebook().start() spinner_id = spinner.spinner_id self.assertIsNotNone(spinner_id) spinner.stop() def test_succeed(self): """Test succeed method """ spinner = HaloNotebook() spinner.start('foo') spinner.succeed('foo') output = self._get_test_output(spinner)['text'] pattern = re.compile(r'(✔|v) foo', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_succeed_with_new_text(self): """Test succeed method with new text """ spinner = HaloNotebook() spinner.start('foo') spinner.succeed('bar') output = self._get_test_output(spinner)['text'] pattern = re.compile(r'(✔|v) bar', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_info(self): """Test info method """ spinner = HaloNotebook() spinner.start('foo') spinner.info() output = self._get_test_output(spinner)['text'] pattern = re.compile(r'(ℹ|¡) foo', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_fail(self): """Test fail method """ spinner = HaloNotebook() spinner.start('foo') spinner.fail() output = self._get_test_output(spinner)['text'] pattern = re.compile(r'(✖|×) foo', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_warning(self): """Test warn method """ spinner = HaloNotebook() spinner.start('foo') spinner.warn('Warning!') output = self._get_test_output(spinner)['text'] pattern = re.compile(r'(⚠|!!) Warning!', re.UNICODE) self.assertRegexpMatches(output[-1], pattern) spinner.stop() def test_spinner_getters_setters(self): """Test spinner getters and setters. """ spinner = HaloNotebook() self.assertEqual(spinner.text, '') self.assertEqual(spinner.color, 'cyan') self.assertIsNone(spinner.spinner_id) spinner.spinner = 'dots12' spinner.text = 'bar' spinner.color = 'red' self.assertEqual(spinner.text, 'bar') self.assertEqual(spinner.color, 'red') if is_supported(): self.assertEqual(spinner.spinner, Spinners['dots12'].value) else: self.assertEqual(spinner.spinner, default_spinner) spinner.spinner = 'dots11' if is_supported(): self.assertEqual(spinner.spinner, Spinners['dots11'].value) else: self.assertEqual(spinner.spinner, default_spinner) spinner.spinner = 'foo_bar' self.assertEqual(spinner.spinner, default_spinner) # Color is None spinner.color = None spinner.start() spinner.stop() self.assertIsNone(spinner.color) def test_unavailable_spinner_defaults(self): """Test unavailable spinner defaults. """ spinner = HaloNotebook('dot') self.assertEqual(spinner.text, 'dot') self.assertEqual(spinner.spinner, default_spinner) def test_if_enabled(self): """Test if spinner is enabled """ spinner = HaloNotebook(text="foo", enabled=False) spinner.start() time.sleep(1) output = self._get_test_output(spinner)['text'] spinner.clear() spinner.stop() self.assertEqual(len(output), 0) self.assertEqual(output, []) def test_invalid_placement(self): """Test invalid placement of spinner. """ with self.assertRaises(ValueError): HaloNotebook(placement='') HaloNotebook(placement='foo') HaloNotebook(placement=None) spinner = HaloNotebook(placement='left') with self.assertRaises(ValueError): spinner.placement = '' spinner.placement = 'foo' spinner.placement = None def test_default_placement(self): """Test default placement of spinner. """ spinner = HaloNotebook() self.assertEqual(spinner.placement, 'left') def test_right_placement(self): """Test right placement of spinner. """ spinner = HaloNotebook(text="foo", placement="right") spinner.start() time.sleep(1) output = self._get_test_output(spinner)['text'] (text, _) = output[-1].split(" ") self.assertEqual(text, "foo") spinner.succeed() output = self._get_test_output(spinner)['text'] (text, symbol) = output[-1].split(" ") pattern = re.compile(r"(✔|v)", re.UNICODE) self.assertEqual(text, "foo") self.assertRegexpMatches(symbol, pattern) spinner.stop() def test_spinner_color(self): """Test ANSI escape characters are present """ for color, color_int in COLORS.items(): spinner = HaloNotebook(color=color) spinner.start() output = self._get_test_output(spinner, no_ansi=False) spinner.stop() output_merged = [arr for c in output['colors'] for arr in c] self.assertEquals(str(color_int) in output_merged, True) def tearDown(self): """Clean up things after every test. """ pass if __name__ == '__main__': SUITE = unittest.TestLoader().loadTestsFromTestCase(TestHaloNotebook) unittest.TextTestRunner(verbosity=2).run(SUITE) python-halo-0.0.31/tox.ini000066400000000000000000000011101375220710100153370ustar00rootroot00000000000000[tox] envlist = py34, py35, py36, py37, py38, lint skip_missing_interpreters = True [testenv] # See https://github.com/pytest-dev/pytest/pull/5222#issuecomment-492428610 download = True deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt extras=ipython recreate = True setenv = LANG=en_US.UTF-8 commands = nosetests --cover-package=halo --with-coverage --cover-erase --cover-branches --nologcapture [testenv:lint] commands = pylint --errors-only --rcfile={toxinidir}/.pylintrc --output-format=colorized halo