diff --git a/.travis.yml b/.travis.yml index 75733fc8..584ddd24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ sudo: false language: python python: 3.4 +branches: # remove travis double-check on pull requests in main repo + only: + - master + - /^\d\.\d+$/ env: - TOXENV=py26 - TOXENV=py27 diff --git a/CONTRIBUTE b/CONTRIBUTE index 503166e4..76f61e6f 100644 --- a/CONTRIBUTE +++ b/CONTRIBUTE @@ -4,19 +4,25 @@ HOW TO CONTRIBUTE TO TQDM This file describes how to contribute changes to the project, and how to upload to the pypi repository. +Most of the management commands have been directly placed inside the +Makefile: `python setup.py make [alias]`, (or simply `make [alias]` in +UNIX-like environments). + +Note: to use the Makefile on Windows, you need to install make.exe, +for example by installing [MinGW MSYS](http://www.mingw.org/wiki/msys). + HOW TO COMMIT YOUR CONTRIBUTIONS -------------------------------- -Contributions to the project is made using the "Fork & Pull" model. Here's a -quickstart on how to do that (your mileage may vary depending on your git -config): +Contributions to the project are made using the "Fork & Pull" model. The +typical steps would be: - create an account on [github](https://github.com) - fork [tqdm](https://github.com/tqdm/tqdm) - make a local clone: `git clone https://github.com/your_account/tqdm.git` - make your changes on your local copy -- commit your changes `git commit -m "my message"` +- commit your changes `git commit -a -m "my message"` - TEST YOUR CHANGES (see below) - `push` to your github account: `git push origin` - finally, create a Pull Request (PR) from your github fork @@ -30,22 +36,57 @@ TESTING To test functionality on your machine (such as before submitting a Pull Request), there are a number of unit tests. -To run the tests, + +Standard unit testing +~~~~~~~~~~~~~~~~~~~~~ + +The standard way to run the tests: - install `tox` - `cd` to the root of the `tqdm` directory (in the same folder as this file) -- run the following commands: +- run the following command: + +``` +python setup.py make test +``` + +or ``` make test -make coverage -make flake8 ``` -Alternatively (if you can't use `make`) +or -- install `nosetest` -- run the following commands: +``` +tox --skip-missing-interpreters +``` + +This will build the module and run the tests in a virtual environment. +Errors and coverage rates will be output to the console/log. (Ignore missing +interpreters errors - these are due to the local machine missing certain +versions of Python.) + +Note: to install all versions of the Python interpreter that are specified +in [tox.ini](https://raw.githubusercontent.com/tqdm/tqdm/master/tox.ini), +you can use `MiniConda` to install a minimal setup. You must also make sure +that each distribution has an alias to call the Python interpreter: +`python27` for Python 2.7's interpreter, `python32` for Python 3.2's, etc. + + +Alternative unit testing with Nose +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Alternatively, use `nose` to run the tests just for your Python version: + +- install `nose` and `flake8` +- run the following command: + +``` +python setup.py make alltests +``` + +or ``` nosetests --with-coverage --cover-package=tqdm -v tqdm/ diff --git a/Makefile b/Makefile index ce7a23a6..773f694c 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,76 @@ -.PHONY: all flake8 test coverage +# IMPORTANT: for compatibility with `python setup.py make [alias]`, ensure: +# 1. A line return after every alias +# 2. One command per line +# E.g.: +# ``` +# all: +# @make test +# @make install +# test: +# nosetest +# install: +# python setup.py install +# ``` + +.PHONY: + alltests + all + flake8 + test + testnose + testsetup + testcoverage + installdev + install + build + pypimeta + pypi + none + +alltests: + @make testcoverage + @make flake8 + @make testsetup + +all: + @make alltests + @make build -all: flake8 coverage flake8: flake8 --max-line-length=80 --count --statistics --exit-zero tqdm/ flake8 --max-line-length=80 --count --statistics --exit-zero examples/ + flake8 --max-line-length=80 --count --statistics --exit-zero . test: + tox --skip-missing-interpreters + +testnose: nosetests tqdm -v -coverage: +testsetup: + python setup.py check --restructuredtext --strict + python setup.py make none + +testcoverage: rm -f .coverage # coverage erase nosetests tqdm --with-coverage --cover-package=tqdm -v + +installdev: + python setup.py develop --uninstall + python setup.py develop + +install: + python setup.py install + +build: + python setup.py sdist --formats=gztar,zip bdist_wininst + python setup.py sdist bdist_wheel + +pypimeta: + python setup.py register + +pypi: + twine upload dist/* + +none: + # used for unit testing diff --git a/README.rst b/README.rst index aa1108fc..8f39b428 100644 --- a/README.rst +++ b/README.rst @@ -225,13 +225,14 @@ with the following command: $ make test $ make coverage -See the `CONTRIBUTE `__ file for more information. +See the `CONTRIBUTE `__ +file for more information. License ------- -`MIT LICENSE `__. +`MIT LICENSE `__. Authors diff --git a/RELEASE b/RELEASE index 9084d7d3..6d61370c 100644 --- a/RELEASE +++ b/RELEASE @@ -4,13 +4,20 @@ HOW TO MANAGE A NEW RELEASE This file is intended for the project's maintainers and it describes how to update, build and upload a new release. +Most of the management commands have been directly placed inside the +Makefile: `python setup.py make [alias]`, (or simply `make [alias]` in +UNIX-like environments). + +Note: to use the Makefile on Windows, you need to install make.exe, +for example by installing [MinGW MSYS](http://www.mingw.org/wiki/msys). + SEMANTIC VERSIONING ------------------- The tqdm repository managers should regularly bump the version number in the -VERSION file to follow the [Semantic Versioning](http://semver.org/) -convention. +[_version.py](https://raw.githubusercontent.com/tqdm/tqdm/master/tqdm/_version.py) +file to follow the [Semantic Versioning](http://semver.org/) convention. Tools can be used to automate this process, such as [bumpversion](https://github.com/peritus/bumpversion) or @@ -18,23 +25,22 @@ Tools can be used to automate this process, such as to automate this task. The managers should take care of this instead of users to avoid PR conflicts -solely due to the VERSION file bumping. +solely due to the version file bumping. CHECKING SETUP.PY ----------------- -To check that the setup.py is OK for PyPi (eg, version number, -reStructuredText in README.rst intelligible for PyPi, which uses a -awkward RST lib, etc.) you can use the following command to make -sure everything's ok: +To check that the `setup.py` file is compliant with PyPi requirements (e.g. +version number; reStructuredText in README.rst) use the following command: ``` python setup.py check --restructuredtext --strict ``` -Also, if you happen to mistakenly upload a broken release to PyPi, -you can fix the metadata by using: `python setup.py register`. +If you happen to mistakenly upload a broken release to PyPi, +you can fix the metadata by using: `python setup.py make pypimeta` +or `python setup.py register`. BUILDING A RELEASE AND UPLOADING TO PYPI @@ -46,24 +52,22 @@ process and info that will be uploaded to [pypi](pypi.python.org). Check the result by using the following commands: ``` -python setup.py develop --uninstall -python setup.py develop +python setup.py make installdev ``` Secondly, build tqdm into a distributable python package: ``` -python setup.py sdist --formats=gztar,zip bdist_wininst --plat-name=win32 -python setup.py sdist bdist_wheel --plat-name=win32 +python setup.py make build ``` This will generate several builds in the `dist/` folder. -Finally, we can upload everything to pypi. Uploading to pypi can be done -easily using the [twine](https://github.com/pypa/twine) module: +Finally, upload everything to pypi. This can be done easily using the +[twine](https://github.com/pypa/twine) module: ``` -twine upload dist/* +python setup.py make pypi ``` NOTE: @@ -74,7 +78,7 @@ before the real deployment cannot re-upload another with the same version number! - in case of a mistake in the metadata on pypi (like the long description README getting garbled because of a silent error), you can use the following -command to update the metadata: `python setup.py register` +command to update the metadata: `make pypimeta` or `python setup.py register` -Also, the new release can be added to the Github by creating a new release +Also, the new release can be added to github by creating a new release from the web interface. diff --git a/examples/simple_examples.py b/examples/simple_examples.py index 03e6dfbd..96e5cefe 100644 --- a/examples/simple_examples.py +++ b/examples/simple_examples.py @@ -26,5 +26,5 @@ stmts = ( ' [i for i in ProgressBar()(xrange(int(1e8)))]') for s in stmts: - print s - print timeit(stmt=s, number=1), 'seconds' + print(s) + print(timeit(stmt=s, number=1), 'seconds') diff --git a/setup.py b/setup.py index 6d372580..b1bea3d2 100755 --- a/setup.py +++ b/setup.py @@ -1,14 +1,161 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- + import os from setuptools import setup +import sys +import subprocess +# For Makefile parsing +try: # pragma: no cover + import ConfigParser + import StringIO +except ImportError: # pragma: no cover + # Python 3 compatibility + import configparser as ConfigParser + import io as StringIO + + +""" Makefile auxiliary functions """ + + +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, i.e.: + + ``` + all: + test + install + test: + nosetest + install: + python setup.py install + ``` + + 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 open(filepath, 'r') as fd: + ini_str = ini_str + fd.read().replace('@make ', '') + 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: + # 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 = 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: + if verbose: + print("Running command: " + cmd) + subprocess.check_call(cmd.split()) + + +""" Main setup.py config """ + # Get version from tqdm/_version.py __version__ = None version_file = os.path.join(os.path.dirname(__file__), 'tqdm', '_version.py') -for line in open(version_file).readlines(): - if (line.startswith('version_info') or line.startswith('__version__')): - exec(line.strip()) +with open(version_file) as fd: + exec(fd.read()) + +# Executing makefile commands if specified +if sys.argv[1].lower().strip() == 'make': + # Filename of the makefile + fpath = '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:") + for alias in sorted(commands.keys()): + print("- " + alias) + + # 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 + sys.exit(0) + + +""" Python package config """ + + +README_rst = '' +with open('README.rst', 'r') as fd: + README_rst = fd.read() setup( name='tqdm', @@ -20,10 +167,12 @@ setup( url='https://github.com/tqdm/tqdm', maintainer='tqdm developers', maintainer_email='python.tqdm@gmail.com', - platforms = ["any"], + platforms=['any'], packages=['tqdm'], - long_description = open("README.rst", "r").read(), - classifiers=[ # Trove classifiers, see https://pypi.python.org/pypi?%3Aaction=list_classifiers + long_description=README_rst, + classifiers=[ + # Trove classifiers + # (https://pypi.python.org/pypi?%3Aaction=list_classifiers) 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: MIT License', 'Environment :: Console', @@ -48,5 +197,8 @@ setup( 'Topic :: Utilities', 'Intended Audience :: Developers', ], - keywords = 'progressbar progressmeter progress bar meter rate eta console terminal time', + keywords='progressbar progressmeter progress bar meter' + ' rate eta console terminal time', + test_suite='nose.collector', + tests_require=['nose', 'flake8', 'coverage'], ) diff --git a/tox.ini b/tox.ini index 90ac0c26..16b54349 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py32, py33, py34, pypy, pypy3, flake8 +envlist = py26, py27, py32, py33, py34, pypy, pypy3, flake8, setup.py [testenv] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH @@ -17,8 +17,16 @@ commands = coveralls [testenv:flake8] -basepython = python2.7 deps = flake8 commands = - flake8 --count --statistics tqdm/ - flake8 --count --statistics examples/ + flake8 --max-line-length=80 --count --statistics --exit-zero tqdm/ + flake8 --max-line-length=80 --count --statistics --exit-zero examples/ + flake8 --max-line-length=80 --count --statistics --exit-zero . + +[testenv:setup.py] +deps = + docutils + pygments +commands = + python setup.py check --restructuredtext --metadata --strict + python setup.py make none diff --git a/tqdm/_version.py b/tqdm/_version.py index 21c179bc..2a3823f4 100644 --- a/tqdm/_version.py +++ b/tqdm/_version.py @@ -1,5 +1,5 @@ # Definition of the version number -version_info = 2, 1, 0 # major, minor, patch, -extra +version_info = 2, 2, 0 # major, minor, patch, -extra # Nice string for the version __version__ = '.'.join(map(str, version_info)).replace('.-', '-').strip('.-')