diff --git a/.gitignore b/.gitignore index f42bedc6b..6ce278987 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ *.py[cod] + +# C extensions *.so -/wiki/ -/docs/ -/feedstock/ # Packages tqdm.egg-info @@ -13,6 +12,7 @@ dist/ .tox/ .coverage __pycache__ +nosetests.xml # Translations *.mo @@ -25,6 +25,14 @@ __pycache__ # PyCharm .idea +# IPython +.ipynb_checkpoints + # asv .asv/ benchmarks/*.py[co] + +# Sumbodules +/wiki/ +/docs/ +/feedstock/ diff --git a/.tqdm.1.md b/.tqdm.1.md index 2f88ad598..69ea3b17d 100644 --- a/.tqdm.1.md +++ b/.tqdm.1.md @@ -1,6 +1,6 @@ % TQDM(1) tqdm User Manuals % tqdm developers -% 2015-2018 +% 2015-2019 # NAME diff --git a/.travis.yml b/.travis.yml index 35037afe3..6afe47609 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -# sudo: required -# dist: trusty language: python matrix: include: @@ -46,10 +44,7 @@ before_install: # - sudo ln -s /run/shm /dev/shm - git fetch --tags install: - # Install tox first, before dependencies (to get per-env deps) - pip install tox - # install this package (tqdm) into the environment - pip install . -# run tests script: - tox diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0d2f12b7..ab2cccdbe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,8 @@ make [] # on UNIX-like environments python setup.py make [] # if make is unavailable ``` +The latter depends on [`py-make>=0.1.0`](https://github.com/tqdm/py-make). + Use the alias `help` (or leave blank) to list all available aliases. @@ -80,9 +82,11 @@ Alternatively, use `nose` to run the tests just for the current Python version: This section is intended for the project's maintainers and describes how to build and upload a new release. Once again, `[python setup.py] make []` will help. +Also consider `pip install`ing development utilities: +`-r requirements-dev.txt` or `tqdm[dev]`. -## SEMANTIC VERSIONING +## Semantic Versioning The tqdm repository managers should: @@ -97,7 +101,7 @@ Note: tools can be used to automate this process, such as [python-semanticversion](https://github.com/rbarrois/python-semanticversion/). -## CHECKING SETUP.PY +## Checking setup.py To check that the `setup.py` file is compliant with PyPi requirements (e.g. version number; reStructuredText in README.rst) use: @@ -114,7 +118,7 @@ to PyPi, use: ``` -## MERGING PULL REQUESTS +## Merging Pull Requests This section describes how to cleanly merge PRs. @@ -164,7 +168,7 @@ git merge --no-ff pr-branch-name ### 5 Version -Modify tqdm/_version.py and amend the last (merge) commit: +Modify `tqdm/_version.py` and amend the last (merge) commit: ``` git add tqdm/_version.py @@ -178,7 +182,7 @@ git push origin master ``` -## BUILDING A RELEASE AND UPLOADING TO PYPI +## Building a Release and Uploading to PyPI Formally publishing requires additional steps: testing and tagging. @@ -199,7 +203,7 @@ display as `v{major}.{minor}.{patch}-{commit_hash}`. ### Upload -Build tqdm into a distributable python package: +Build `tqdm` into a distributable python package: ``` [python setup.py] make build @@ -233,21 +237,20 @@ cannot re-upload another with the same version number updating just the metadata is possible: `[python setup.py] make pypimeta` -## UPDATING GH-PAGES +## Updating Websites -The most important file is README.rst, which sould always be kept up-to-date +The most important file is `README.rst`, which sould always be kept up-to-date and in sync with the in-line source documentation. This will affect all of the following: - The [main repository site](https://github.com/tqdm/tqdm) which automatically - serves the latest README.rst as well as links to all of github's features. - This is the preferred online referral link for tqdm. + serves the latest `README.rst` as well as links to all of github's features. + This is the preferred online referral link for `tqdm`. - The [PyPi mirror](https://pypi.org/project/tqdm) which automatically - serves the latest release built from README.rst as well as links to past + serves the latest release built from `README.rst` as well as links to past releases. - Many external web crawlers. - Additionally (less maintained), there exists: - A [wiki] which is publicly editable. @@ -276,7 +279,7 @@ For experienced devs, once happy with local master: b) `twine upload -s -i $(git config user.signingkey) dist/tqdm-*` 10. create new release on https://github.com/tqdm/tqdm/releases a) add helpful release notes - b) attach dist/tqdm-* binaries (usually only *.whl*) + b) attach `dist/tqdm-*` binaries (usually only `*.whl*`) 11. run `make` in the `wiki` submodule to update release notes 12. run `make deploy` in the `docs` submodule to update website 13. accept the automated PR in the `feedstock` submodule to update conda diff --git a/LICENCE b/LICENCE index 7700869d8..0c4f76d13 100644 --- a/LICENCE +++ b/LICENCE @@ -7,7 +7,7 @@ Exceptions or notable authors are listed below in reverse chronological order: * files: * - MPLv2.0 2015-2018 (c) Casper da Costa-Luis + MPLv2.0 2015-2019 (c) Casper da Costa-Luis [casperdcl](https://github.com/casperdcl). * files: tqdm/_tqdm.py MIT 2016 (c) [PR #96] on behalf of Google Inc. @@ -20,8 +20,10 @@ in reverse chronological order: Mozilla Public Licence (MPL) v. 2.0 - Exhibit A ----------------------------------------------- -This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +This Source Code Form is subject to the terms of the +Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, +You can obtain one at https://mozilla.org/MPL/2.0/. MIT License (MIT) diff --git a/MANIFEST.in b/MANIFEST.in index c77b2ff5e..488c5fae5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,6 +9,7 @@ include tox.ini # Test suite recursive-include tqdm/tests *.py +include requirements-dev.txt # Sub-packages recursive-include tqdm/autonotebook *.py diff --git a/Makefile b/Makefile index f9791de84..4e897f4a4 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,7 @@ # IMPORTANT: for compatibility with `python setup.py make [alias]`, ensure: # 1. Every alias is preceded by @[+]make (eg: @make alias) # 2. A maximum of one @make alias or command per line -# -# Sample makefile compatible with `python setup.py make`: -#``` -#all: -# @make test -# @make install -#test: -# nosetest -#install: -# python setup.py install -#``` +# see: https://github.com/tqdm/py-make/issues/1 .PHONY: alltests @@ -27,14 +17,17 @@ coverclean prebuildclean clean + toxclean installdev install build + buildupload pypi + help none help: - @python setup.py make + @python setup.py make -p alltests: @+make testcoverage @@ -47,7 +40,8 @@ all: @+make build flake8: - @+flake8 --max-line-length=80 --exclude .asv,.tox -j 8 --count --statistics --exit-zero . + @+flake8 --max-line-length=80 --exclude .asv,.tox,.ipynb_checkpoints,build \ + -j 8 --count --statistics --exit-zero . test: tox --skip-missing-interpreters diff --git a/README.rst b/README.rst index f10172839..1f2203fb0 100644 --- a/README.rst +++ b/README.rst @@ -536,6 +536,26 @@ Points to remember when using ``{postfix[...]}`` in the ``bar_format`` string: object. To prevent this behaviour, insert an extra item into the dictionary where the key is not a string. +Additional ``bar_format`` parameters may also be defined by overriding +``format_dict``: + +.. code:: python + + from tqdm import tqdm + class TqdmExtraFormat(tqdm): + """Provides a `total_time` format parameter""" + @property + def format_dict(self): + d = super(TqdmExtraFormat, self).format_dict + total_time = d["elapsed"] * (d["total"] or 0) / max(d["n"], 1) + d.update(total_time=self.format_interval(total_time) + " in total") + return d + + for i in TqdmExtraFormat( + range(10), + bar_format="{total_time}: {percentage:.0f}%|{bar}{r_bar}"): + pass + Nested progress bars ~~~~~~~~~~~~~~~~~~~~ @@ -870,7 +890,7 @@ There are also many |GitHub-Contributions| which we are grateful for. .. |Branch-Coverage-Status| image:: https://codecov.io/gh/tqdm/tqdm/branch/master/graph/badge.svg :target: https://codecov.io/gh/tqdm/tqdm .. |Codacy-Grade| image:: https://api.codacy.com/project/badge/Grade/3f965571598f44549c7818f29cdcf177 - :target: https://www.codacy.com/app/tqdm/tqdm?utm_source=github.com&utm_medium=referral&utm_content=tqdm/tqdm&utm_campaign=Badge_Grade + :target: https://www.codacy.com/app/tqdm/tqdm/dashboard .. |GitHub-Status| image:: https://img.shields.io/github/tag/tqdm/tqdm.svg?maxAge=86400&logo=github&logoColor=white :target: https://github.com/tqdm/tqdm/releases .. |GitHub-Forks| image:: https://img.shields.io/github/forks/tqdm/tqdm.svg?logo=github&logoColor=white @@ -905,8 +925,8 @@ There are also many |GitHub-Contributions| which we are grateful for. :target: https://www.openhub.net/p/tqdm?ref=Thin+badge .. |LICENCE| image:: https://img.shields.io/pypi/l/tqdm.svg :target: https://raw.githubusercontent.com/tqdm/tqdm/master/LICENCE -.. |DOI-URI| image:: https://zenodo.org/badge/21637/tqdm/tqdm.svg - :target: https://zenodo.org/badge/latestdoi/21637/tqdm/tqdm +.. |DOI-URI| image:: https://img.shields.io/badge/DOI-10.5281/zenodo.595120-blue.svg + :target: https://doi.org/10.5281/zenodo.595120 .. |interactive-demo| image:: https://img.shields.io/badge/demo-interactive-orange.svg?logo=jupyter :target: https://notebooks.rmotr.com/demo/gh/tqdm/tqdm .. |Screenshot-Jupyter1| image:: https://raw.githubusercontent.com/tqdm/tqdm/master/images/tqdm-jupyter-1.gif diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..fc129715e --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +py-make>=0.1.0 # setup.py make/pymake +twine # pymake pypi +argopt # cd wiki && pymake +pydoc-markdown # cd docs && pymake diff --git a/setup.py b/setup.py index a026276d3..132669b7a 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - import os try: from setuptools import setup, find_packages @@ -13,20 +12,8 @@ def find_packages(where='.'): for (folder, _, fils) in os.walk(where) if "__init__.py" in fils] import sys -from subprocess import check_call from io import open as io_open -# For Makefile parsing -import shlex -try: # pragma: no cover - import ConfigParser - import StringIO -except ImportError: # pragma: no cover - # Python 3 compatibility - import configparser as ConfigParser - import io as StringIO -import re - # Get version from tqdm/_version.py __version__ = None src_dir = os.path.abspath(os.path.dirname(__file__)) @@ -34,145 +21,30 @@ def find_packages(where='.'): with io_open(version_file, mode='r') as fd: exec(fd.read()) -# Makefile auxiliary functions # - -RE_MAKE_CMD = re.compile('^\t(@\+?)(make)?', flags=re.M) - - -def parse_makefile_aliases(filepath): - """ - Parse a makefile to find commands and substitute variables. Expects a - makefile with only aliases and a line return between each command. - - Returns a dict, with a list of commands for each alias. - """ - - # -- Parsing the Makefile using ConfigParser - # Adding a fake section to make the Makefile a valid Ini file - ini_str = '[root]\n' - with io_open(filepath, mode='r') as fd: - ini_str = ini_str + RE_MAKE_CMD.sub('\t', fd.read()) - ini_fp = StringIO.StringIO(ini_str) - # Parse using ConfigParser - config = ConfigParser.RawConfigParser() - config.readfp(ini_fp) - # Fetch the list of aliases - aliases = config.options('root') - - # -- Extracting commands for each alias - commands = {} - for alias in aliases: - if alias.lower() in ['.phony']: - continue - # strip the first line return, and then split by any line return - commands[alias] = config.get('root', alias).lstrip('\n').split('\n') - - # -- Commands substitution - # Loop until all aliases are substituted by their commands: - # Check each command of each alias, and if there is one command that is to - # be substituted by an alias, try to do it right away. If this is not - # possible because this alias itself points to other aliases , then stop - # and put the current alias back in the queue to be processed again later. - - # Create the queue of aliases to process - aliases_todo = list(commands.keys()) - # Create the dict that will hold the full commands - commands_new = {} - # Loop until we have processed all aliases - while aliases_todo: - # Pick the first alias in the queue - alias = aliases_todo.pop(0) - # Create a new entry in the resulting dict - commands_new[alias] = [] - # For each command of this alias - for cmd in commands[alias]: - # Ignore self-referencing (alias points to itself) - if cmd == alias: - pass - # Substitute full command - elif cmd in aliases and cmd in commands_new: - # Append all the commands referenced by the alias - commands_new[alias].extend(commands_new[cmd]) - # Delay substituting another alias, waiting for the other alias to - # be substituted first - elif cmd in aliases and cmd not in commands_new: - # Delete the current entry to avoid other aliases - # to reference this one wrongly (as it is empty) - del commands_new[alias] - aliases_todo.append(alias) - break - # Full command (no aliases) - else: - commands_new[alias].append(cmd) - commands = commands_new - del commands_new - - # -- Prepending prefix to avoid conflicts with standard setup.py commands - # for alias in commands.keys(): - # commands['make_'+alias] = commands[alias] - # del commands[alias] - - return commands - - -def execute_makefile_commands(commands, alias, verbose=False): - cmds = commands[alias] - for cmd in cmds: - # Parse string in a shell-like fashion - # (incl quoted strings and comments) - parsed_cmd = shlex.split(cmd, comments=True) - # Execute command if not empty (ie, not just a comment) - if parsed_cmd: - if verbose: - print("Running command: " + cmd) - # Launch the command and wait to finish (synchronized call) - check_call(parsed_cmd, cwd=src_dir) - - -# Main setup.py config # - - # Executing makefile commands if specified if sys.argv[1].lower().strip() == 'make': + import pymake # Filename of the makefile fpath = os.path.join(src_dir, 'Makefile') - # Parse the makefile, substitute the aliases and extract the commands - commands = parse_makefile_aliases(fpath) - - # If no alias (only `python setup.py make`), print the list of aliases - if len(sys.argv) < 3 or sys.argv[-1] == '--help': - print("Shortcut to use commands via aliases. List of aliases:") - print('\n'.join(alias for alias in sorted(commands.keys()))) - - # Else process the commands for this alias - else: - arg = sys.argv[-1] - # if unit testing, we do nothing (we just checked the makefile parsing) - if arg == 'none': - sys.exit(0) - # else if the alias exists, we execute its commands - elif arg in commands.keys(): - execute_makefile_commands(commands, arg, verbose=True) - # else the alias cannot be found - else: - raise Exception("Provided alias cannot be found: make " + arg) - # Stop the processing of setup.py here: - # It's important to avoid setup.py raising an error because of the command - # not being standard + pymake.main(['-f', fpath] + sys.argv[2:]) + # Stop to avoid setup.py raising non-standard command error sys.exit(0) - -# Python package config # +extras_require = {} +requirements_dev = os.path.join(src_dir, 'requirements-dev.txt') +with io_open(requirements_dev, mode='r') as fd: + extras_require['dev'] = [i.strip().split('#', 1)[0].strip() + for i in fd.read().strip().split('\n')] README_rst = '' fndoc = os.path.join(src_dir, 'README.rst') with io_open(fndoc, mode='r', encoding='utf-8') as fd: README_rst = fd.read() - setup( name='tqdm', version=__version__, description='Fast, Extensible Progress Meter', + long_description=README_rst, license='MPLv2.0, MIT Licences', author='Noam Yorav-Raphael', author_email='noamraph@gmail.com', @@ -181,10 +53,11 @@ def execute_makefile_commands(commands, alias, verbose=False): maintainer_email='python.tqdm@gmail.com', platforms=['any'], packages=['tqdm'] + ['tqdm.' + i for i in find_packages('tqdm')], + provides=['tqdm'], + extras_require=extras_require, entry_points={'console_scripts': ['tqdm=tqdm._main:main'], }, package_data={'tqdm': ['CONTRIBUTING.md', 'LICENCE', 'examples/*.py', - 'tqdm.1']}, - long_description=README_rst, + 'tqdm.1', 'requirements-dev.txt']}, python_requires='>=2.6, !=3.0.*, !=3.1.*', classifiers=[ # Trove classifiers @@ -225,12 +98,16 @@ def execute_makefile_commands(commands, alias, verbose=False): 'Programming Language :: Python :: Implementation :: IronPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Desktop Environment', + 'Topic :: Education :: Computer Aided Instruction (CAI)', 'Topic :: Education :: Testing', 'Topic :: Office/Business', 'Topic :: Other/Nonlisted Topic', + 'Topic :: Software Development :: Build Tools', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Software Development :: Pre-processors', 'Topic :: Software Development :: User Interfaces', + 'Topic :: System :: Installation/Setup', 'Topic :: System :: Logging', 'Topic :: System :: Monitoring', 'Topic :: System :: Shells', diff --git a/tox.ini b/tox.ini index f4e4e4c8a..fef7be2e3 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ deps = coverage coveralls commands = - nosetests --with-coverage --cover-package=tqdm --ignore-files="tests_p(erf|andas)\.py" -d -v tqdm/ + nosetests --with-coverage --cover-package=tqdm --ignore-files="tests_perf\.py" -d -v tqdm/ - coveralls [extra] @@ -22,22 +22,18 @@ deps = nose-timer codecov commands = - nosetests --with-coverage --with-timer --cover-package=tqdm --ignore-files="tests_p(erf|andas)\.py" -d -v tqdm/ + nosetests --with-coverage --with-timer --cover-package=tqdm --ignore-files="tests_perf\.py" -d -v tqdm/ - coveralls codecov [testenv] -# default tests (most things) -passenv = CI TRAVIS TRAVIS_* +passenv = CI TRAVIS TRAVIS_* TOXENV CODECOV_* deps = {[extra]deps} cython numpy pandas -commands = - nosetests --with-coverage --with-timer --cover-package=tqdm --ignore-files="tests_perf\.py" -d -v tqdm/ - - coveralls - codecov +commands = {[extra]commands} # no cython/numpy/pandas for py{py,py3,26,33,34} @@ -47,9 +43,12 @@ deps = nose coverage coveralls==1.2.0 + codecov pycparser==2.18 idna==2.7 -commands = {[coverage]commands} +commands = + {[coverage]commands} + codecov [testenv:pypy] deps = {[extra]deps} diff --git a/tqdm/_main.py b/tqdm/_main.py index 67f6d3e92..0a6117487 100644 --- a/tqdm/_main.py +++ b/tqdm/_main.py @@ -195,6 +195,8 @@ def main(fp=sys.stderr, argv=None): delim = tqdm_args.pop('delim', '\n') delim_per_char = tqdm_args.pop('bytes', False) manpath = tqdm_args.pop('manpath', None) + stdin = getattr(sys.stdin, 'buffer', sys.stdin) + stdout = getattr(sys.stdout, 'buffer', sys.stdout) if manpath is not None: from os import path from shutil import copyfile @@ -210,14 +212,12 @@ def main(fp=sys.stderr, argv=None): tqdm_args.setdefault('unit_divisor', 1024) log.debug(tqdm_args) with tqdm(**tqdm_args) as t: - posix_pipe(sys.stdin, sys.stdout, - '', buf_size, t.update) + posix_pipe(stdin, stdout, '', buf_size, t.update) elif delim == '\n': log.debug(tqdm_args) - for i in tqdm(sys.stdin, **tqdm_args): - sys.stdout.write(i) + for i in tqdm(stdin, **tqdm_args): + stdout.write(i) else: log.debug(tqdm_args) with tqdm(**tqdm_args) as t: - posix_pipe(sys.stdin, sys.stdout, - delim, buf_size, t.update) + posix_pipe(stdin, stdout, delim, buf_size, t.update) diff --git a/tqdm/_tqdm.py b/tqdm/_tqdm.py index b378a1a33..8eac3285d 100755 --- a/tqdm/_tqdm.py +++ b/tqdm/_tqdm.py @@ -79,8 +79,8 @@ class TqdmDefaultWriteLock(object): an argument to joblib or the parallelism lib you use. """ def __init__(self): - # Create global parallelism locks to avoid racing issues with parallel bars - # works only if fork available (Linux/MacOSX, but not Windows) + # Create global parallelism locks to avoid racing issues with parallel + # bars works only if fork available (Linux/MacOSX, but not Windows) self.create_mp_lock() self.create_th_lock() cls = type(self) @@ -210,8 +210,8 @@ def format_num(n): @staticmethod def ema(x, mu=None, alpha=0.3): """ - Exponential moving average: smoothing to give progressively lower - weights to older values. + Exponential moving average: smoothing to give progressively lower + weights to older values. Parameters ---------- @@ -222,7 +222,7 @@ def ema(x, mu=None, alpha=0.3): alpha : float, optional Smoothing factor in range [0, 1], [default: 0.3]. Increase to give more weight to recent values. - Ranges from 0 (yields mu) to 1 (yields x). + Ranges from 0 (yields mu) to 1 (yields x). """ return x if mu is None else (alpha * x) + (1 - alpha) * mu @@ -252,7 +252,7 @@ def print_status(s): @staticmethod def format_meter(n, total, elapsed, ncols=None, prefix='', ascii=False, unit='it', unit_scale=False, rate=None, bar_format=None, - postfix=None, unit_divisor=1000): + postfix=None, unit_divisor=1000, **extra_kwargs): """ Return a string-based progress bar given some parameters @@ -381,26 +381,17 @@ def format_meter(n, total, elapsed, ncols=None, prefix='', ascii=False, if bar_format: # Custom bar formatting # Populate a dict with all available progress indicators - bar_args = {'n': n, - 'n_fmt': n_fmt, - 'total': total, - 'total_fmt': total_fmt, - 'percentage': percentage, - 'rate': inv_rate if inv_rate and inv_rate > 1 - else rate, - 'rate_fmt': rate_fmt, - 'rate_noinv': rate, - 'rate_noinv_fmt': rate_noinv_fmt, - 'rate_inv': inv_rate, - 'rate_inv_fmt': rate_inv_fmt, - 'elapsed': elapsed_str, - 'remaining': remaining_str, - 'l_bar': l_bar, - 'r_bar': r_bar, - 'desc': prefix or '', - 'postfix': postfix, - # 'bar': full_bar # replaced by procedure below - } + format_dict = dict( + n=n, n_fmt=n_fmt, total=total, total_fmt=total_fmt, + percentage=percentage, + rate=inv_rate if inv_rate and inv_rate > 1 else rate, + rate_fmt=rate_fmt, rate_noinv=rate, + rate_noinv_fmt=rate_noinv_fmt, rate_inv=inv_rate, + rate_inv_fmt=rate_inv_fmt, elapsed=elapsed_str, + remaining=remaining_str, l_bar=l_bar, r_bar=r_bar, + desc=prefix or '', postfix=postfix, + # bar=full_bar, # replaced by procedure below + **extra_kwargs) # auto-remove colon for empty `desc` if not prefix: @@ -411,11 +402,11 @@ def format_meter(n, total, elapsed, ncols=None, prefix='', ascii=False, # Format left/right sides of the bar, and format the bar # later in the remaining space (avoid breaking display) l_bar_user, r_bar_user = bar_format.split('{bar}') - l_bar = l_bar_user.format(**bar_args) - r_bar = r_bar_user.format(**bar_args) + l_bar = l_bar_user.format(**format_dict) + r_bar = r_bar_user.format(**format_dict) else: # Else no progress bar, we can just format and return - return bar_format.format(**bar_args) + return bar_format.format(**format_dict) # Formatting progress bar space available for bar's display if ncols: @@ -838,10 +829,13 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True, self.disable = True self.pos = self._get_free_pos(self) self._instances.remove(self) - raise (TqdmDeprecationWarning("""\ -`nested` is deprecated and automated. Use position instead for manual control. -""", fp_write=getattr(file, 'write', sys.stderr.write)) if "nested" in kwargs - else TqdmKeyError("Unknown argument(s): " + str(kwargs))) + from textwrap import dedent + raise (TqdmDeprecationWarning(dedent("""\ + `nested` is deprecated and automated. + Use `position` instead for manual control. + """), fp_write=getattr(file, 'write', sys.stderr.write)) + if "nested" in kwargs else + TqdmKeyError("Unknown argument(s): " + str(kwargs))) # Preprocess the arguments if ((ncols is None) and (file in (sys.stderr, sys.stdout))) or \ @@ -926,11 +920,7 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True, # Initialize the screen printer self.sp = self.status_printer(self.fp) with self._lock: - if self.pos: - self.moveto(abs(self.pos)) - self.sp(self.__repr__(elapsed=0)) - if self.pos: - self.moveto(-abs(self.pos)) + self.display() # Init the time counter self.last_print_t = self._time() @@ -953,14 +943,23 @@ def __exit__(self, *exc): def __del__(self): self.close() - def __repr__(self, elapsed=None): - return self.format_meter( - self.n, self.total, - elapsed if elapsed is not None else self._time() - self.start_t, - self.dynamic_ncols(self.fp) if self.dynamic_ncols else self.ncols, - self.desc, self.ascii, self.unit, - self.unit_scale, 1 / self.avg_time if self.avg_time else None, - self.bar_format, self.postfix, self.unit_divisor) + @property + def format_dict(self): + """Public API for read-only member access""" + return dict( + n=self.n, total=self.total, + elapsed=self._time() - self.start_t + if hasattr(self, 'start_t') else 0, + ncols=self.dynamic_ncols(self.fp) + if self.dynamic_ncols else self.ncols, + prefix=self.desc, ascii=self.ascii, unit=self.unit, + unit_scale=self.unit_scale, + rate=1 / self.avg_time if self.avg_time else None, + bar_format=self.bar_format, postfix=self.postfix, + unit_divisor=self.unit_divisor) + + def __repr__(self): + return self.format_meter(**self.format_dict) @property def _comparable(self): @@ -992,12 +991,11 @@ def __iter__(self): avg_time = self.avg_time _time = self._time - try: - sp = self.sp - except AttributeError: - raise TqdmDeprecationWarning("""\ -Please use `tqdm_gui(...)` instead of `tqdm(..., gui=True)` -""", fp_write=getattr(self.fp, 'write', sys.stderr.write)) + if not hasattr(self, 'sp'): + from textwrap import dedent + raise TqdmDeprecationWarning(dedent("""\ + Please use `tqdm_gui(...)` instead of `tqdm(..., gui=True)` + """), fp_write=getattr(self.fp, 'write', sys.stderr.write)) for obj in iterable: yield obj @@ -1019,12 +1017,7 @@ def __iter__(self): self.n = n with self._lock: - if self.pos: - self.moveto(abs(self.pos)) - # Print bar update - sp(self.__repr__()) - if self.pos: - self.moveto(-abs(self.pos)) + self.display() # If no `miniters` was specified, adjust automatically # to the max iteration rate seen so far between 2 prints @@ -1098,22 +1091,17 @@ def update(self, n=1): # EMA (not just overall average) if self.smoothing and delta_t and delta_it: rate = delta_t / delta_it - self.avg_time = self.ema(rate, self.avg_time, self.smoothing) + self.avg_time = self.ema( + rate, self.avg_time, self.smoothing) if not hasattr(self, "sp"): - raise TqdmDeprecationWarning("""\ -Please use `tqdm_gui(...)` instead of `tqdm(..., gui=True)` -""", fp_write=getattr(self.fp, 'write', sys.stderr.write)) + from textwrap import dedent + raise TqdmDeprecationWarning(dedent("""\ + Please use `tqdm_gui(...)` instead of `tqdm(..., gui=True)` + """), fp_write=getattr(self.fp, 'write', sys.stderr.write)) with self._lock: - if self.pos: - self.moveto(abs(self.pos)) - - # Print bar update - self.sp(self.__repr__()) - - if self.pos: - self.moveto(-abs(self.pos)) + self.display() # If no `miniters` was specified, adjust automatically to the # maximum iteration rate seen so far between two prints. @@ -1171,25 +1159,18 @@ def fp_write(s): raise # pragma: no cover with self._lock: - if pos: - self.moveto(pos) - if self.leave: if self.last_print_n < self.n: # stats for overall rate (no weighted average) self.avg_time = None - self.sp(self.__repr__()) - if pos: - self.moveto(-pos) - elif not max([abs(getattr(i, "pos", 0)) - for i in self._instances] + [0]): + self.display(pos=pos) + if not max([abs(getattr(i, "pos", 0)) + for i in self._instances] + [pos]): # only if not nested (#477) fp_write('\n') else: - self.sp('') # clear up last bar - if pos: - self.moveto(-pos) - else: + self.display(msg='', pos=pos) + if not pos: fp_write('\r') def unpause(self): @@ -1262,6 +1243,7 @@ def set_postfix_str(self, s='', refresh=True): self.refresh() def moveto(self, n): + # TODO: private method self.fp.write(_unicode('\n' * n + _term_move_up() * -n)) self.fp.flush() @@ -1290,12 +1272,28 @@ def refresh(self, nolock=False): if not nolock: self._lock.acquire() - self.moveto(abs(self.pos)) - self.sp(self.__repr__()) - self.moveto(-abs(self.pos)) + self.display() if not nolock: self._lock.release() + def display(self, msg=None, pos=None): + """ + Use `self.sp` and to display `msg` in the specified `pos`. + + Parameters + ---------- + msg : what to display (default: repr(self)) + pos : position to display in. (default: abs(self.pos)) + """ + if pos is None: + pos = abs(self.pos) + + if pos: + self.moveto(pos) + self.sp(self.__repr__() if msg is None else msg) + if pos: + self.moveto(-pos) + def trange(*args, **kwargs): """ diff --git a/tqdm/_tqdm_gui.py b/tqdm/_tqdm_gui.py index aadd016cb..ad46d8bdd 100644 --- a/tqdm/_tqdm_gui.py +++ b/tqdm/_tqdm_gui.py @@ -243,7 +243,8 @@ def update(self, n=1): # EMA (not just overall average) if self.smoothing and delta_t and delta_it: rate = delta_t / delta_it - self.avg_time = self.ema(rate, self.avg_time, self.smoothing) + self.avg_time = self.ema( + rate, self.avg_time, self.smoothing) # Inline due to multiple calls total = self.total diff --git a/tqdm/_utils.py b/tqdm/_utils.py index 63f9b3112..8aebc0ef7 100644 --- a/tqdm/_utils.py +++ b/tqdm/_utils.py @@ -6,7 +6,8 @@ IS_WIN = CUR_OS in ['Windows', 'cli'] IS_NIX = (not IS_WIN) and any( CUR_OS.startswith(i) for i in - ['CYGWIN', 'MSYS', 'Linux', 'Darwin', 'SunOS', 'FreeBSD', 'NetBSD', 'OpenBSD']) + ['CYGWIN', 'MSYS', 'Linux', 'Darwin', 'SunOS', + 'FreeBSD', 'NetBSD', 'OpenBSD']) RE_ANSI = re.compile(r"\x1b\[[;\d]*[A-Za-z]") diff --git a/tqdm/_version.py b/tqdm/_version.py index 5257f9efc..ced3af41a 100644 --- a/tqdm/_version.py +++ b/tqdm/_version.py @@ -5,7 +5,7 @@ __all__ = ["__version__"] # major, minor, patch, -extra -version_info = 4, 29, 1 +version_info = 4, 30, 0 # Nice string for the version __version__ = '.'.join(map(str, version_info)) diff --git a/tqdm/tests/tests_main.py b/tqdm/tests/tests_main.py index 58fbbd35c..f22db089b 100644 --- a/tqdm/tests/tests_main.py +++ b/tqdm/tests/tests_main.py @@ -14,6 +14,18 @@ def _sh(*cmd, **kwargs): **kwargs).communicate()[0].decode('utf-8') +class Null(object): + def __call__(self, *_, **__): + return self + + def __getattr__(self, _): + return self + + +IN_DATA_LIST = map(str, _range(int(123))) +NULL = Null() + + # WARNING: this should be the last test as it messes with sys.stdin, argv @with_setup(pretest, posttest) def test_main(): @@ -33,35 +45,64 @@ def test_main(): _SYS = sys.stdin, sys.argv with closing(StringIO()) as sys.stdin: - sys.argv = ['', '--desc', 'Test CLI-delims', + sys.argv = ['', '--desc', 'Test CLI --delim', '--ascii', 'True', '--delim', r'\0', '--buf_size', '64'] - sys.stdin.write('\0'.join(map(str, _range(int(1e3))))) + sys.stdin.write('\0'.join(map(str, _range(int(123))))) + #sys.stdin.write(b'\xff') # TODO sys.stdin.seek(0) main() - - IN_DATA_LIST = map(str, _range(int(1e3))) sys.stdin = IN_DATA_LIST + sys.argv = ['', '--desc', 'Test CLI pipes', '--ascii', 'True', '--unit_scale', 'True'] import tqdm.__main__ # NOQA - IN_DATA = '\0'.join(IN_DATA_LIST) with closing(StringIO()) as sys.stdin: + IN_DATA = '\0'.join(IN_DATA_LIST) sys.stdin.write(IN_DATA) sys.stdin.seek(0) sys.argv = ['', '--ascii', '--bytes', '--unit_scale', 'False'] with closing(UnicodeIO()) as fp: main(fp=fp) assert str(len(IN_DATA)) in fp.getvalue() + sys.stdin = IN_DATA_LIST + + # test --log + with closing(StringIO()) as sys.stdin: + sys.stdin.write('\0'.join(map(str, _range(int(123))))) + sys.stdin.seek(0) + # with closing(UnicodeIO()) as fp: + main(argv=['--log', 'DEBUG'], fp=NULL) + # assert "DEBUG:" in sys.stdout.getvalue() + sys.stdin = IN_DATA_LIST + + # clean up + sys.stdin, sys.argv = _SYS + + +def test_manpath(): + """Test CLI --manpath""" + tmp = mkdtemp() + man = path.join(tmp, "tqdm.1") + assert not path.exists(man) + try: + main(argv=['--manpath', tmp], fp=NULL) + except SystemExit: + pass + else: + raise SystemExit("Expected system exit") + assert path.exists(man) + rmtree(tmp, True) + +def test_exceptions(): + """Test CLI Exceptions""" + _SYS = sys.stdin, sys.argv sys.stdin = IN_DATA_LIST - sys.argv = ['', '-ascii', '--unit_scale', 'False', - '--desc', 'Test CLI errors'] - main() sys.argv = ['', '-ascii', '-unit_scale', '--bad_arg_u_ment', 'foo'] try: - main() + main(fp=NULL) except TqdmKeyError as e: if 'bad_arg_u_ment' not in str(e): raise @@ -70,7 +111,7 @@ def test_main(): sys.argv = ['', '-ascii', '-unit_scale', 'invalid_bool_value'] try: - main() + main(fp=NULL) except TqdmTypeError as e: if 'invalid_bool_value' not in str(e): raise @@ -79,7 +120,7 @@ def test_main(): sys.argv = ['', '-ascii', '--total', 'invalid_int_value'] try: - main() + main(fp=NULL) except TqdmTypeError as e: if 'invalid_int_value' not in str(e): raise @@ -90,30 +131,11 @@ def test_main(): for i in ('-h', '--help', '-v', '--version'): sys.argv = ['', i] try: - main() + main(fp=NULL) except SystemExit: pass - - # test --manpath - tmp = mkdtemp() - man = path.join(tmp, "tqdm.1") - assert not path.exists(man) - try: - main(argv=['--manpath', tmp]) - except SystemExit: - pass - else: - raise SystemExit("Expected system exit") - assert path.exists(man) - rmtree(tmp, True) - - # test --log - with closing(StringIO()) as sys.stdin: - sys.stdin.write('\0'.join(map(str, _range(int(1e3))))) - sys.stdin.seek(0) - # with closing(UnicodeIO()) as fp: - main(argv=['--log', 'DEBUG']) - # assert "DEBUG:" in sys.stdout.getvalue() + else: + raise ValueError('expected SystemExit') # clean up sys.stdin, sys.argv = _SYS diff --git a/tqdm/tests/tests_tqdm.py b/tqdm/tests/tests_tqdm.py index f3bdf78b5..998b00a08 100644 --- a/tqdm/tests/tests_tqdm.py +++ b/tqdm/tests/tests_tqdm.py @@ -61,7 +61,29 @@ def closing(arg): RE_ctrlchr = re.compile("(%s)" % '|'.join(CTRLCHR)) # Match control chars RE_ctrlchr_excl = re.compile('|'.join(CTRLCHR)) # Match and exclude ctrl chars RE_pos = re.compile( - r'((\x1b\[A|\r|\n)+((pos\d+) bar:\s+\d+%|\s{3,6})?)') # NOQA + r'([\r\n]+((pos\d+) bar:\s+\d+%|\s{3,6})?[^\r\n]*)') + + +def pos_line_diff(res_list, expected_list, raise_nonempty=True): + """ + Return differences between two bar output lists. + To be used with `RE_pos` + """ + l = len(res_list) + if l < len(expected_list): + res = [(None, e) for e in expected_list[l:]] + elif l > len(expected_list): + res = [(r, None) for r in res_list[l:]] + res = [(r, e) for r, e in zip(res_list, expected_list) + for pos in [len(e)-len(e.lstrip('\n'))] # bar position + if not r.startswith(e) # start matches + or not (r.endswith('\x1b[A' * pos) # move up at end + or r=='\n') # final bar + or r[(-1-pos) * len('\x1b[A'):] == '\x1b[A'] # extra move up + if res and raise_nonempty: + raise AssertionError( + "Got => Expected\n" + '\n'.join('"%r" => "%r"' % i for i in res)) + return res class DiscreteTimer(object): @@ -157,15 +179,12 @@ def progressbar_rate(bar_str): def squash_ctrlchars(s): """Apply control characters in a string just like a terminal display""" - # List of supported control codes - ctrlcodes = [r'\r', r'\n', r'\x1b\[A'] - # Init variables curline = 0 # current line in our fake terminal lines = [''] # state of our fake terminal # Split input string by control codes - RE_ctrl = re.compile("(%s)" % ("|".join(ctrlcodes)), flags=re.DOTALL) + RE_ctrl = re.compile("(%s)" % ("|".join(CTRLCHR)), flags=re.DOTALL) s_split = RE_ctrl.split(s) s_split = filter(None, s_split) # filter out empty splits @@ -355,16 +374,14 @@ def test_leave_option(): with closing(StringIO()) as our_file: for _ in tqdm(_range(3), file=our_file, leave=True): pass - our_file.seek(0) - assert '| 3/3 ' in our_file.read() - our_file.seek(0) - assert '\n' == our_file.read()[-1] # not '\r' + res = our_file.getvalue() + assert '| 3/3 ' in res + assert '\n' == res[-1] # not '\r' with closing(StringIO()) as our_file2: for _ in tqdm(_range(3), file=our_file2, leave=False): pass - our_file2.seek(0) - assert '| 3/3 ' not in our_file2.read() + assert '| 3/3 ' not in our_file2.getvalue() @with_setup(pretest, posttest) @@ -373,14 +390,12 @@ def test_trange(): with closing(StringIO()) as our_file: for _ in trange(3, file=our_file, leave=True): pass - our_file.seek(0) - assert '| 3/3 ' in our_file.read() + assert '| 3/3 ' in our_file.getvalue() with closing(StringIO()) as our_file2: for _ in trange(3, file=our_file2, leave=False): pass - our_file2.seek(0) - assert '| 3/3 ' not in our_file2.read() + assert '| 3/3 ' not in our_file2.getvalue() @with_setup(pretest, posttest) @@ -389,8 +404,7 @@ def test_min_interval(): with closing(StringIO()) as our_file: for _ in tqdm(_range(3), file=our_file, mininterval=1e-10): pass - our_file.seek(0) - assert " 0%| | 0/3 [00:00<" in our_file.read() + assert " 0%| | 0/3 [00:00<" in our_file.getvalue() @with_setup(pretest, posttest) @@ -428,10 +442,8 @@ def test_max_interval(): t.close() # because PyPy doesn't gc immediately t2.close() # as above - our_file2.seek(0) - assert "25%" not in our_file2.read() - our_file.seek(0) - assert "25%" not in our_file.read() + assert "25%" not in our_file2.getvalue() + assert "25%" not in our_file.getvalue() # Test with maxinterval effect timer = DiscreteTimer() @@ -447,8 +459,7 @@ def test_max_interval(): t.update(smallstep) timer.sleep(1e-2) - our_file.seek(0) - assert "25%" in our_file.read() + assert "25%" in our_file.getvalue() # Test iteration based tqdm with maxinterval effect timer = DiscreteTimer() @@ -464,8 +475,7 @@ def test_max_interval(): if i >= 3 * bigstep: break - our_file.seek(0) - assert "15%" in our_file.read() + assert "15%" in our_file.getvalue() # Test different behavior with and without mininterval timer = DiscreteTimer() @@ -535,15 +545,13 @@ def test_min_iters(): with closing(StringIO()) as our_file: for _ in tqdm(_range(3), file=our_file, leave=True, miniters=4): our_file.write('blank\n') - our_file.seek(0) - assert '\nblank\nblank\n' in our_file.read() + assert '\nblank\nblank\n' in our_file.getvalue() with closing(StringIO()) as our_file: for _ in tqdm(_range(3), file=our_file, leave=True, miniters=1): our_file.write('blank\n') - our_file.seek(0) # assume automatic mininterval = 0 means intermediate output - assert '| 3/3 ' in our_file.read() + assert '| 3/3 ' in our_file.getvalue() @with_setup(pretest, posttest) @@ -563,8 +571,7 @@ def test_dynamic_min_iters(): # The third iteration should be displayed t.update() - our_file.seek(0) - out = our_file.read() + out = our_file.getvalue() assert t.dynamic_miniters t.__del__() # simulate immediate del gc @@ -585,8 +592,7 @@ def test_dynamic_min_iters(): t.update(5) # this should be stored as miniters t.update(1) - our_file.seek(0) - out = our_file.read() + out = our_file.getvalue() assert all(i in out for i in ("0/10", "1/10", "3/10")) assert "2/10" not in out assert t.dynamic_miniters and not t.smoothing @@ -623,15 +629,13 @@ def test_big_min_interval(): with closing(StringIO()) as our_file: for _ in tqdm(_range(2), file=our_file, mininterval=1E10): pass - our_file.seek(0) - assert '50%' not in our_file.read() + assert '50%' not in our_file.getvalue() with closing(StringIO()) as our_file: with tqdm(_range(2), file=our_file, mininterval=1E10) as t: t.update() t.update() - our_file.seek(0) - assert '50%' not in our_file.read() + assert '50%' not in our_file.getvalue() @with_setup(pretest, posttest) @@ -652,8 +656,7 @@ def test_smoothed_dynamic_min_iters(): for _ in _range(20): t.update() - our_file.seek(0) - out = our_file.read() + out = our_file.getvalue() assert t.dynamic_miniters assert ' 0%| | 0/100 [00:00<' in out assert '10%' in out @@ -684,8 +687,7 @@ def test_smoothed_dynamic_min_iters_with_min_interval(): for _ in _range(4): t.update() timer.sleep(1e-2) - our_file.seek(0) - out = our_file.read() + out = our_file.getvalue() assert t.dynamic_miniters with closing(StringIO()) as our_file: @@ -699,8 +701,7 @@ def test_smoothed_dynamic_min_iters_with_min_interval(): timer.sleep(0.1) if i >= 14: break - our_file.seek(0) - out2 = our_file.read() + out2 = our_file.getvalue() assert t.dynamic_miniters assert ' 0%| | 0/100 [00:00<' in out @@ -738,12 +739,12 @@ def _rlock_creation_target(): assert rlock_mock.call_count == 0 # Creating a progress bar should initialize the lock with closing(StringIO()) as our_file: - with tqdm(file=our_file) as t: + with tqdm(file=our_file) as _: # NOQA pass assert rlock_mock.call_count == 1 # Creating a progress bar again should reuse the lock with closing(StringIO()) as our_file: - with tqdm(file=our_file) as t: + with tqdm(file=our_file) as _: # NOQA pass assert rlock_mock.call_count == 1 @@ -754,15 +755,13 @@ def test_disable(): with closing(StringIO()) as our_file: for _ in tqdm(_range(3), file=our_file, disable=True): pass - our_file.seek(0) - assert our_file.read() == '' + assert our_file.getvalue() == '' with closing(StringIO()) as our_file: progressbar = tqdm(total=3, file=our_file, miniters=1, disable=True) progressbar.update(3) progressbar.close() - our_file.seek(0) - assert our_file.read() == '' + assert our_file.getvalue() == '' @with_setup(pretest, posttest) @@ -779,8 +778,7 @@ def test_unit(): with closing(StringIO()) as our_file: for _ in tqdm(_range(3), file=our_file, miniters=1, unit="bytes"): pass - our_file.seek(0) - assert 'bytes/s' in our_file.read() + assert 'bytes/s' in our_file.getvalue() @with_setup(pretest, posttest) @@ -796,8 +794,7 @@ def test_ascii(): for _ in tqdm(_range(3), total=15, file=our_file, miniters=1, mininterval=0, ascii=True): pass - our_file.seek(0) - res = our_file.read().strip("\r").split("\r") + res = our_file.getvalue().strip("\r").split("\r") assert '7%|6' in res[1] assert '13%|#3' in res[2] assert '20%|##' in res[3] @@ -807,8 +804,7 @@ def test_ascii(): with tqdm(total=15, file=our_file, ascii=False, mininterval=0) as t: for _ in _range(3): t.update() - our_file.seek(0) - res = our_file.read().strip("\r").split("\r") + res = our_file.getvalue().strip("\r").split("\r") assert "7%|\u258b" in res[1] assert "13%|\u2588\u258e" in res[2] assert "20%|\u2588\u2588" in res[3] @@ -823,8 +819,7 @@ def test_update(): as progressbar: assert len(progressbar) == 2 progressbar.update(2) - our_file.seek(0) - assert '| 2/2' in our_file.read() + assert '| 2/2' in our_file.getvalue() progressbar.desc = 'dynamically notify of 4 increments in total' progressbar.total = 4 try: @@ -835,8 +830,7 @@ def test_update(): progressbar.update() # should default to +1 else: raise ValueError("Should not support negative updates") - our_file.seek(0) - res = our_file.read() + res = our_file.getvalue() assert '| 3/4 ' in res assert 'dynamically notify of 4 increments in total' in res @@ -873,13 +867,11 @@ def test_close(): assert '| 3/3 ' in res # Should be blank # close() called assert len(tqdm._instances) == 0 - our_file.seek(0) exres = res + '\n' - if exres != our_file.read(): - our_file.seek(0) - raise AssertionError( - "\nExpected:\n{0}\nGot:{1}\n".format(exres, our_file.read())) + if exres != our_file.getvalue(): + raise AssertionError("\nExpected:\n{0}\nGot:{1}\n".format( + exres, our_file.getvalue())) # Closing after the output stream has closed with closing(StringIO()) as our_file: @@ -901,8 +893,7 @@ def test_smoothing(): for _ in t: pass - our_file.seek(0) - assert '| 3/3 ' in our_file.read() + assert '| 3/3 ' in our_file.getvalue() # -- Test smoothing # Compile the regex to find the rate @@ -1001,8 +992,8 @@ def test_deprecated_nested(): try: tqdm(total=2, file=our_file, nested=True) except TqdmDeprecationWarning: - if """`nested` is deprecated and automated.\ - Use position instead for manual control.""" not in our_file.getvalue(): + if """`nested` is deprecated and automated. +Use `position` instead for manual control.""" not in our_file.getvalue(): raise else: raise DeprecationError("Should not allow nested kwarg") @@ -1025,6 +1016,26 @@ def test_bar_format(): assert isinstance(t.bar_format, _unicode) +@with_setup(pretest, posttest) +def test_custom_format(): + """Test adding additional derived format arguments""" + class TqdmExtraFormat(tqdm): + """Provides a `total_time` format parameter""" + @property + def format_dict(self): + d = super(TqdmExtraFormat, self).format_dict + total_time = d["elapsed"] * (d["total"] or 0) / max(d["n"], 1) + d.update(total_time=self.format_interval(total_time) + " in total") + return d + + with closing(StringIO()) as our_file: + for i in TqdmExtraFormat( + range(10), file=our_file, + bar_format="{total_time}: {percentage:.0f}%|{bar}{r_bar}"): + pass + assert "00:00 in total" in our_file.getvalue() + + @with_setup(pretest, posttest) def test_unpause(): """Test unpause""" @@ -1061,16 +1072,13 @@ def test_position(): t = tqdm(total=2, desc='pos2 bar', leave=False, position=2, **kwargs) t.update() t.close() - our_file.seek(0) - out = our_file.read() + out = our_file.getvalue() res = [m[0] for m in RE_pos.findall(out)] exres = ['\n\n\rpos2 bar: 0%', - '\x1b[A\x1b[A\n\n\rpos2 bar: 50%', - '\x1b[A\x1b[A\n\n\r ', - '\x1b[A\x1b[A'] - if res != exres: - raise AssertionError("\nExpected:\n{0}\nGot:\n{1}\nRaw:\n{2}\n".format( - str(exres), str(res), str([out]))) + '\n\n\rpos2 bar: 50%', + '\n\n\r '] + + pos_line_diff(res, exres) # Test iteration-based tqdm positioning our_file = StringIO() @@ -1079,34 +1087,31 @@ def test_position(): for _ in trange(2, desc='pos1 bar', position=1, **kwargs): for _ in trange(2, desc='pos2 bar', position=2, **kwargs): pass - our_file.seek(0) - out = our_file.read() + out = our_file.getvalue() res = [m[0] for m in RE_pos.findall(out)] exres = ['\rpos0 bar: 0%', '\n\rpos1 bar: 0%', - '\x1b[A\n\n\rpos2 bar: 0%', - '\x1b[A\x1b[A\n\n\rpos2 bar: 50%', - '\x1b[A\x1b[A\n\n\rpos2 bar: 100%', - '\x1b[A\x1b[A\n\n\x1b[A\x1b[A\n\rpos1 bar: 50%', - '\x1b[A\n\n\rpos2 bar: 0%', - '\x1b[A\x1b[A\n\n\rpos2 bar: 50%', - '\x1b[A\x1b[A\n\n\rpos2 bar: 100%', - '\x1b[A\x1b[A\n\n\x1b[A\x1b[A\n\rpos1 bar: 100%', - '\x1b[A\n\x1b[A\rpos0 bar: 50%', + '\n\n\rpos2 bar: 0%', + '\n\n\rpos2 bar: 50%', + '\n\n\rpos2 bar: 100%', + '\n\rpos1 bar: 50%', + '\n\n\rpos2 bar: 0%', + '\n\n\rpos2 bar: 50%', + '\n\n\rpos2 bar: 100%', + '\n\rpos1 bar: 100%', + '\rpos0 bar: 50%', '\n\rpos1 bar: 0%', - '\x1b[A\n\n\rpos2 bar: 0%', - '\x1b[A\x1b[A\n\n\rpos2 bar: 50%', - '\x1b[A\x1b[A\n\n\rpos2 bar: 100%', - '\x1b[A\x1b[A\n\n\x1b[A\x1b[A\n\rpos1 bar: 50%', - '\x1b[A\n\n\rpos2 bar: 0%', - '\x1b[A\x1b[A\n\n\rpos2 bar: 50%', - '\x1b[A\x1b[A\n\n\rpos2 bar: 100%', - '\x1b[A\x1b[A\n\n\x1b[A\x1b[A\n\rpos1 bar: 100%', - '\x1b[A\n\x1b[A\rpos0 bar: 100%', + '\n\n\rpos2 bar: 0%', + '\n\n\rpos2 bar: 50%', + '\n\n\rpos2 bar: 100%', + '\n\rpos1 bar: 50%', + '\n\n\rpos2 bar: 0%', + '\n\n\rpos2 bar: 50%', + '\n\n\rpos2 bar: 100%', + '\n\rpos1 bar: 100%', + '\rpos0 bar: 100%', '\n'] - if res != exres: - raise AssertionError("\nExpected:\n{0}\nGot:\n{1}\nRaw:\n{2}\n".format( - str(exres), str(res), str([out]))) + pos_line_diff(res, exres) # Test manual tqdm positioning our_file = StringIO() @@ -1119,22 +1124,18 @@ def test_position(): t1.update() t3.update() t2.update() - our_file.seek(0) - out = our_file.read() + out = our_file.getvalue() res = [m[0] for m in RE_pos.findall(out)] exres = ['\rpos0 bar: 0%', '\n\rpos1 bar: 0%', - '\x1b[A\n\n\rpos2 bar: 0%', - '\x1b[A\x1b[A\rpos0 bar: 50%', + '\n\n\rpos2 bar: 0%', + '\rpos0 bar: 50%', '\n\n\rpos2 bar: 50%', - '\x1b[A\x1b[A\n\rpos1 bar: 50%', - '\x1b[A\rpos0 bar: 100%', + '\n\rpos1 bar: 50%', + '\rpos0 bar: 100%', '\n\n\rpos2 bar: 100%', - '\x1b[A\x1b[A\n\rpos1 bar: 100%', - '\x1b[A'] - if res != exres: - raise AssertionError("\nExpected:\n{0}\nGot:\n{1}\nRaw:\n{2}\n".format( - str(exres), str(res), str([out]))) + '\n\rpos1 bar: 100%'] + pos_line_diff(res, exres) t1.close() t2.close() t3.close() @@ -1148,11 +1149,9 @@ def test_position(): res = [m[0] for m in RE_pos.findall(our_file.getvalue())] exres = ['\rpos0 bar: 0%', '\n\rpos1 bar: 0%', - '\x1b[A\n\n\rpos2 bar: 0%', - '\x1b[A\x1b[A'] - if res != exres: - raise AssertionError( - "\nExpected:\n{0}\nGot:\n{1}\n".format(str(exres), str(res))) + '\n\n\rpos2 bar: 0%'] + pos_line_diff(res, exres) + t2.close() t4 = tqdm(total=10, file=our_file, desc='pos3 bar', mininterval=0) @@ -1162,15 +1161,12 @@ def test_position(): res = [m[0] for m in RE_pos.findall(our_file.getvalue())] exres = ['\rpos0 bar: 0%', '\n\rpos1 bar: 0%', - '\x1b[A\n\n\rpos2 bar: 0%', - '\x1b[A\x1b[A\n\x1b[A\n\n\rpos3 bar: 0%', - '\x1b[A\x1b[A\rpos0 bar: 10%', + '\n\n\rpos2 bar: 0%', + '\n\n\rpos3 bar: 0%', + '\rpos0 bar: 10%', '\n\rpos2 bar: 10%', - '\x1b[A\n\n\rpos3 bar: 10%', - '\x1b[A\x1b[A'] - if res != exres: - raise AssertionError( - "\nExpected:\n{0}\nGot:\n{1}\n".format(str(exres), str(res))) + '\n\n\rpos3 bar: 10%'] + pos_line_diff(res, exres) t4.close() t3.close() t1.close() @@ -1412,10 +1408,11 @@ def test_write(): assert before_err == '\rpos0 bar: 0%|\rpos0 bar: 10%|' assert before_out == '' after_err_res = [m[0] for m in RE_pos.findall(after_err)] - assert after_err_res == [u'\rpos0 bar: 0%', - u'\rpos0 bar: 10%', - u'\r ', - u'\r\rpos0 bar: 10%'] + exres = [u'\rpos0 bar: 0%', + u'\rpos0 bar: 10%', + u'\r ', + u'\r\rpos0 bar: 10%'] + pos_line_diff(after_err_res, exres) assert after_out == s + '\n' # Restore stdout and stderr sys.stderr = stde