diff --git a/.travis.yml b/.travis.yml index 78dbe90..dde6545 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,53 +1,46 @@ -sudo: required -dist: trusty language: python -python: 3.5 - -# use cache for big builds +matrix: + include: + - python: 2.6 + env: TOXENV=py26 + - python: 2.7 + env: TOXENV=py27 + - python: 3.4 + env: TOXENV=py34 + - python: 3.5 + env: TOXENV=py35 + - python: 3.6 + env: TOXENV=py36 + - python: 3.7 + dist: xenial + sudo: true + env: TOXENV=py37 + - python: pypy2.7-5.10.0 + env: TOXENV=pypy + - python: pypy3.5-5.10.0 + env: TOXENV=pypy3 + - python: 3.6 + env: TOXENV=flake8 +# use cache for big builds like pandas (to minimise build time). +# If issues, clear cache +# https://docs.travis-ci.com/user/caching/#Clearing-Caches cache: pip: true directories: - $HOME/.cache/pip before_cache: - rm -f $HOME/.cache/pip/log/debug.log - notifications: email: false # branches: # remove travis double-check on pull requests in main repo # only: # - master # - /^\d\.\d+$/ - -env: - - TOXENV=py26 - - TOXENV=py27 - - TOXENV=py33 - - TOXENV=py34 - - TOXENV=py35 - - TOXENV=pypy - - TOXENV=pypy3 - - TOXENV=flake8 - -before_install: - # fix a crash with multiprocessing on Travis - - sudo rm -rf /dev/shm - - sudo ln -s /run/shm /dev/shm - # install codecov - - pip install codecov - install: - # install big packages (they are cached to minimize build time) - # if issues, clear cache - # https://docs.travis-ci.com/user/caching/#Clearing-Caches - # Coverage install - - pip install tox 'coverage<4' - # install this package (pymake) into the environment - - python setup.py install - + # Install tox first, before dependencies (to get per-env deps) + - pip install tox + # install this package (py-make) into the environment + - pip install . # run tests script: - tox -# submit coverage - -after_success: - - codecov diff --git a/LICENCE b/LICENCE index 855c32b..48700d6 100644 --- a/LICENCE +++ b/LICENCE @@ -1,38 +1,19 @@ `pymake` is a product of collaborative work. Unless otherwise stated, all authors (see commit logs) retain copyright -for their respective work, and release the work under the MIT licence -(text below). +for their respective work, and release the work under the MPLv2.0 licence. Exceptions or notable authors are listed below in reverse chronological order: -* MPLv2.0 (text below) 2016 (c) Casper da Costa-Luis +* files * + MPLv2.0 2016-2019 (c) Casper da Costa-Luis [casperdcl](https://github.com/casperdcl). 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/. - - -MIT License (MIT) ------------------ - -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. +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/. diff --git a/MANIFEST.in b/MANIFEST.in index 82557c3..6c46c8e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,11 +3,11 @@ include .coveragerc include CONTRIBUTE include LICENCE include Makefile -include README.rst include tox.ini # Test suite recursive-include pymake/tests *.py # Examples/Documentation -recursive-include examples * +recursive-include examples *.py Makefile* +include README.rst diff --git a/Makefile b/Makefile index 60a329a..8bce987 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,8 @@ #test: # nosetest #install: -# python setup.py install +# python setup.py \ +# install #``` .PHONY: @@ -26,15 +27,17 @@ coverclean prebuildclean clean + toxclean installdev install build - pypimeta + buildupload pypi + help none help: - @python setup.py make + @python setup.py make -p alltests: @+make testcoverage @@ -46,10 +49,7 @@ all: @+make build flake8: - @+flake8 --max-line-length=80 --count --statistics --exit-zero pymake/ - @+flake8 --max-line-length=80 --count --statistics --exit-zero examples/ - @+flake8 --max-line-length=80 --count --statistics --exit-zero . - @+flake8 --max-line-length=80 --count --statistics --exit-zero pymake/tests/ + @+flake8 --max-line-length=80 --exclude .tox,build -j 8 --count --statistics --exit-zero . test: tox --skip-missing-interpreters @@ -78,10 +78,16 @@ prebuildclean: @+python -c "import shutil; shutil.rmtree('pymake.egg-info', True)" coverclean: @+python -c "import os; os.remove('.coverage') if os.path.exists('.coverage') else None" + @+python -c "import shutil; shutil.rmtree('pymake/__pycache__', True)" + @+python -c "import shutil; shutil.rmtree('pymake/tests/__pycache__', True)" clean: - @+python -c "import os; import glob; [os.remove(i) for i in glob.glob('*.py[co]')]" - @+python -c "import os; import glob; [os.remove(i) for i in glob.glob('pymake/*.py[co]')]" - @+python -c "import os; import glob; [os.remove(i) for i in glob.glob('pymake/tests/*.py[co]')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('*.py[co]')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('pymake/*.py[co]')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('pymake/tests/*.py[co]')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('pymake/examples/*.py[co]')]" +toxclean: + @+python -c "import shutil; shutil.rmtree('.tox', True)" + installdev: python setup.py develop --uninstall @@ -92,11 +98,8 @@ install: build: @make prebuildclean - python setup.py sdist --formats=gztar,zip bdist_wheel - python setup.py bdist_wininst - -pypimeta: - python setup.py register + python setup.py sdist bdist_wheel + # python setup.py bdist_wininst pypi: twine upload dist/* @@ -104,7 +107,6 @@ pypi: buildupload: @make testsetup @make build - @make pypimeta @make pypi none: diff --git a/README.rst b/README.rst index 218397a..624425e 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,11 @@ -|Logo| - py-make ======= |PyPI-Status| |PyPI-Versions| -|Build-Status| |Coverage-Status| |Branch-Coverage-Status| |Codacy-Grade| +|Build-Status| |Coverage-Status| |Branch-Coverage-Status| |Codacy-Grade| |Libraries-Rank| -|LICENCE| +|DOI-URI| |LICENCE| |OpenHub-Status| Bring basic ``Makefile`` support to any system with Python. @@ -16,7 +14,7 @@ Inspired by work in `tqdm `__. Simply install then execute ``pymake`` in a directory containing a ``Makefile``. -``pyamke`` works on any platform (Linux, Windows, Mac, FreeBSD, Solaris/SunOS). +``pymake`` works on any platform (Linux, Windows, Mac, FreeBSD, Solaris/SunOS). ``pymake`` does not require any library to run, just a vanilla Python interpreter will do. @@ -34,16 +32,16 @@ Installation Latest PyPI stable release ~~~~~~~~~~~~~~~~~~~~~~~~~~ -|PyPI-Status| +|PyPI-Status| |PyPI-Downloads| |Libraries-Dependents| .. code:: sh pip install py-make -Latest development release on github +Latest development release on GitHub ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -|GitHub-Status| |GitHub-Stars| |GitHub-Forks| +|GitHub-Status| |GitHub-Stars| |GitHub-Commits| |GitHub-Forks| |GitHub-Updated| Pull and install in the current directory: @@ -93,7 +91,8 @@ Sample makefile compatible with ``pymake``: test: nosetest install: - python setup.py install + python setup.py\ + install compile: $(PY) test.py circle: @@ -106,7 +105,7 @@ Sample makefile compatible with ``pymake``: Documentation ------------- -|PyPI-Versions| |README-Hits| (Since 19 May 2016) +|PyPI-Versions| |README-Hits| (Since 28 Oct 2016) .. code:: sh @@ -116,6 +115,8 @@ Documentation Contributions ------------- +|GitHub-Commits| |GitHub-Issues| |GitHub-PRs| |OpenHub-Status| + All source code is hosted on `GitHub `__. Contributions are welcome. @@ -135,35 +136,57 @@ Citation information: |DOI-URI| Authors ------- -- Casper da Costa-Luis (casperdcl) -- Stephen Larroque (lrq3000) +The main developers, ranked by surviving lines of code +(`git fame -wMC `__), are: + +- Casper da Costa-Luis (`casperdcl `__, ~99.5%, |Gift-Casper|) +- Stephen Larroque (`lrq3000 `__, ~0.5%) + +We are grateful for all |GitHub-Contributions|. |README-Hits| (Since 28 Oct 2016) -.. |Logo| image:: https://raw.githubusercontent.com/tqdm/py-make/master/logo.png -.. |Screenshot| image:: https://raw.githubusercontent.com/tqdm/py-make/master/images/py-make.gif -.. |Build-Status| image:: https://travis-ci.org/tqdm/py-make.svg?branch=master +.. |Build-Status| image:: https://img.shields.io/travis/tqdm/py-make/master.svg?logo=travis :target: https://travis-ci.org/tqdm/py-make -.. |Coverage-Status| image:: https://coveralls.io/repos/tqdm/py-make/badge.svg - :target: https://coveralls.io/r/tqdm/py-make -.. |Branch-Coverage-Status| image:: https://codecov.io/github/tqdm/py-make/coverage.svg?branch=master - :target: https://codecov.io/github/tqdm/py-make?branch=master +.. |Coverage-Status| image:: https://coveralls.io/repos/tqdm/py-make/badge.svg?branch=master + :target: https://coveralls.io/github/tqdm/py-make +.. |Branch-Coverage-Status| image:: https://codecov.io/gh/tqdm/py-make/branch/master/graph/badge.svg + :target: https://codecov.io/gh/tqdm/py-make .. |Codacy-Grade| image:: https://api.codacy.com/project/badge/Grade/3f965571598f44549c7818f29cdcf177 :target: https://www.codacy.com/app/tqdm/py-make?utm_source=github.com&utm_medium=referral&utm_content=tqdm/py-make&utm_campaign=Badge_Grade -.. |GitHub-Status| image:: https://img.shields.io/github/tag/tqdm/py-make.svg?maxAge=2592000 +.. |GitHub-Status| image:: https://img.shields.io/github/tag/tqdm/py-make.svg?maxAge=86400&logo=github&logoColor=white :target: https://github.com/tqdm/py-make/releases -.. |GitHub-Forks| image:: https://img.shields.io/github/forks/tqdm/py-make.svg +.. |GitHub-Forks| image:: https://img.shields.io/github/forks/tqdm/py-make.svg?logo=github&logoColor=white :target: https://github.com/tqdm/py-make/network -.. |GitHub-Stars| image:: https://img.shields.io/github/stars/tqdm/py-make.svg +.. |GitHub-Stars| image:: https://img.shields.io/github/stars/tqdm/py-make.svg?logo=github&logoColor=white :target: https://github.com/tqdm/py-make/stargazers +.. |GitHub-Commits| image:: https://img.shields.io/github/commit-activity/y/tqdm/py-make.svg?logo=git&logoColor=white + :target: https://github.com/tqdm/py-make/graphs/commit-activity +.. |GitHub-Issues| image:: https://img.shields.io/github/issues-closed/tqdm/py-make.svg?logo=github&logoColor=white + :target: https://github.com/tqdm/py-make/issues +.. |GitHub-PRs| image:: https://img.shields.io/github/issues-pr-closed/tqdm/py-make.svg?logo=github&logoColor=white + :target: https://github.com/tqdm/py-make/pulls +.. |GitHub-Contributions| image:: https://img.shields.io/github/contributors/tqdm/py-make.svg?logo=github&logoColor=white + :target: https://github.com/tqdm/py-make/graphs/contributors +.. |GitHub-Updated| image:: https://img.shields.io/github/last-commit/tqdm/py-make/master.svg?logo=github&logoColor=white&label=pushed + :target: https://github.com/tqdm/py-make/pulse +.. |Gift-Casper| image:: https://img.shields.io/badge/gift-donate-ff69b4.svg + :target: https://caspersci.uk.to/donate.html .. |PyPI-Status| image:: https://img.shields.io/pypi/v/py-make.svg - :target: https://pypi.python.org/pypi/py-make -.. |PyPI-Downloads| image:: https://img.shields.io/pypi/dm/py-make.svg - :target: https://pypi.python.org/pypi/py-make -.. |PyPI-Versions| image:: https://img.shields.io/pypi/pyversions/py-make.svg - :target: https://pypi.python.org/pypi/py-make + :target: https://pypi.org/project/py-make +.. |PyPI-Downloads| image:: https://img.shields.io/pypi/dm/py-make.svg?label=pypi%20downloads&logo=python&logoColor=white + :target: https://pypi.org/project/py-make +.. |PyPI-Versions| image:: https://img.shields.io/pypi/pyversions/py-make.svg?logo=python&logoColor=white + :target: https://pypi.org/project/py-make +.. |Libraries-Rank| image:: https://img.shields.io/librariesio/sourcerank/pypi/py-make.svg?logo=koding&logoColor=white + :target: https://libraries.io/pypi/py-make +.. |Libraries-Dependents| image:: https://img.shields.io/librariesio/dependent-repos/pypi/py-make.svg?logo=koding&logoColor=white + :target: https://github.com/tqdm/py-make/network/dependents +.. |OpenHub-Status| image:: https://www.openhub.net/p/py-make/widgets/project_thin_badge?format=gif + :target: https://www.openhub.net/p/py-make?ref=Thin+badge .. |LICENCE| image:: https://img.shields.io/pypi/l/py-make.svg :target: https://raw.githubusercontent.com/tqdm/py-make/master/LICENCE .. |DOI-URI| image:: https://zenodo.org/badge/21637/tqdm/py-make.svg :target: https://zenodo.org/badge/latestdoi/21637/tqdm/py-make -.. |README-Hits| image:: http://hitt.herokuapp.com/pymake/pymake.svg +.. |README-Hits| image:: https://caspersci.uk.to/cgi-bin/hits.cgi?q=py-make&style=social&r=https://github.com/tqdm/py-make + :target: https://caspersci.uk.to/cgi-bin/hits.cgi?q=py-make&a=plot&r=https://github.com/tqdm/tqdm&style=social diff --git a/examples/Makefile b/examples/Makefile index d5c01c6..172c554 100755 --- a/examples/Makefile +++ b/examples/Makefile @@ -4,8 +4,9 @@ IPY=python -c err hello: - # this is a comment - $(IPY) "print('hello world')" + # this is a comment followed by a multi-line command + $(IPY) \ + "print('hello world')" err: keyboardmashitalltogetherthisshouldnotrunotherwiseunittestswillfail diff --git a/pymake/_main.py b/pymake/_main.py index b48593d..dc0a22e 100644 --- a/pymake/_main.py +++ b/pymake/_main.py @@ -31,12 +31,11 @@ import sys import logging as log - __all__ = ["main"] -def main(): - opts = docopt(__doc__, version=__version__) +def main(argv=None): + opts = docopt(__doc__, version=__version__, argv=argv) if opts.pop('--debug-trace', False): opts['--debug'] = "NOTSET" log.basicConfig(level=getattr(log, opts['--debug'], log.INFO), @@ -66,4 +65,6 @@ def main(): raise PymakeKeyError(sys.argv[0] + ": *** No rule to make target `" + target + "'. Stop.") + + main.__doc__ = __doc__ diff --git a/pymake/_pymake.py b/pymake/_pymake.py index cd7bf09..fdd21c8 100644 --- a/pymake/_pymake.py +++ b/pymake/_pymake.py @@ -13,7 +13,7 @@ 'parse_makefile_aliases', 'execute_makefile_commands'] -RE_MAKE_CMD = re.compile('^\t(@\+?)(make)?') +RE_MAKE_CMD = re.compile(r'^\t(@\+?)(make)?') RE_MACRO_DEF = re.compile(r"^(\S+)\s*\:?\=\s*(.*?)$") RE_MACRO = re.compile(r"\$\(\s*\S+\s*\)") @@ -38,11 +38,11 @@ def parse_makefile_aliases(filepath): default_alias : str ''' - # -- Parsing the Makefile using ConfigParser - # Adding a fake section to make the Makefile a valid Ini file - ini_lines = ['[root]'] with io.open(filepath, mode='r') as fd: - ini_lines.extend(RE_MAKE_CMD.sub('\t', i) for i in fd.readlines()) + ini_lines = fd.read().replace('\r\n', '\n').replace('\\\n', '') + ini_lines = (RE_MAKE_CMD.sub('\t', i) for i in ini_lines.split('\n')) + # fake section to resemble valid *.ini + ini_lines = ['[root]'] + list(ini_lines) # Substitute macros macros = dict(found for l in ini_lines @@ -51,7 +51,7 @@ def parse_makefile_aliases(filepath): # allow finite amount of nesting for _ in range(99): for (m, expr) in getattr(macros, 'iteritems', macros.items)(): - ini_str = re.sub(r"\$\(" + m + "\)", expr, ini_str) + ini_str = re.sub(r"\$\(%s\)" % m, expr, ini_str) if not RE_MACRO.search(ini_str): # Strip macro definitions for rest of parsing ini_str = '\n'.join(l for l in ini_str.splitlines() @@ -62,13 +62,11 @@ def parse_makefile_aliases(filepath): str(set(RE_MACRO.findall(ini_str)))) 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 + # Extract commands for each alias commands = {} default_alias = '' for alias in aliases: @@ -76,54 +74,38 @@ def parse_makefile_aliases(filepath): continue if not default_alias: default_alias = alias - # 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. + # Command substitution (depth-first). + # If this is not possible because an alias points to another alias, + # then stop and put the current alias back in the queue to be + # processed again later (bottom-up). - # 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 + alias = aliases_todo.pop() 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 + elif cmd in aliases: + # Append substituted full commands + if cmd in commands_new: + commands_new[alias].extend(commands_new[cmd]) + # Delay substituting another alias until it is substituted + else: + del commands_new[alias] + aliases_todo.insert(0, 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] - + # Prepending prefix to avoid conflicts with standard setup.py commands + # for alias in list(commands.keys()): + # commands['make_'+alias] = commands.pop(alias) return commands, default_alias @@ -152,7 +134,7 @@ def execute_makefile_commands( # 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) + # Execute command if not empty/comment if parsed_cmd: if not silent: print(cmd) diff --git a/pymake/_version.py b/pymake/_version.py index f575a36..b64e273 100644 --- a/pymake/_version.py +++ b/pymake/_version.py @@ -9,7 +9,7 @@ __all__ = ["__version__"] # major, minor, patch, -extra -version_info = 0, 0, 0 +version_info = 0, 1, 0 # Nice string for the version __version__ = '.'.join(map(str, version_info)) diff --git a/pymake/tests/tests_main.py b/pymake/tests/tests_main.py index 13df609..800fd17 100644 --- a/pymake/tests/tests_main.py +++ b/pymake/tests/tests_main.py @@ -1,83 +1,81 @@ import sys import subprocess -import os +from os import path from pymake import main, PymakeKeyError, PymakeTypeError +dn = path.dirname +fname = path.join(dn(dn(dn(path.abspath(__file__)))), + "examples", "Makefile").replace('\\', '/') + def _sh(*cmd, **kwargs): return subprocess.Popen(cmd, stdout=subprocess.PIPE, **kwargs).communicate()[0].decode('utf-8') -def repeat(fn, n, arg): - a = arg - for _ in range(n): - a = fn(a) - return a - - -# WARNING: this should be the last test as it messes with sys.stdin, argv def test_main(): - """ Test execution """ - - fname = os.path.join(os.path.abspath(repeat(os.path.dirname, 3, __file__)), - "examples", "Makefile").replace('\\', '/') - res = _sh(sys.executable, '-c', - 'from pymake import main; import sys; ' + - 'sys.argv = ["", "-f", "' + fname + '"]; main()', + """Test execution""" + res = _sh(sys.executable, '-c', ('\ + import pymake; pymake.main(["-f", "%s"])' % fname).strip(), stderr=subprocess.STDOUT) # actual test: - assert ("hello world" in res) + try: + assert ("hello world" in res) + except AssertionError: + if sys.version_info[:2] > (2, 6): + raise # semi-fake test which gets coverage: - _SYS = sys.stdin, sys.argv - + _SYS = sys.argv sys.argv = ['', '-f', fname] main() + sys.argv = _SYS - """ Test invalid alias """ - sys.argv = ['', '-f', fname, 'foo'] + +def test_invalid_alias(): + """Test invalid alias""" try: - main() + main(['-f', fname, 'foo']) except PymakeKeyError as e: if 'foo' not in str(e): raise else: raise PymakeKeyError('foo') - """ Test various targets """ + +def test_multi_target(): + """Test various targets""" for trg in ['circle', 'empty', 'one']: - sys.argv = ['', '-s', '-f', fname, trg] - main() + main(['-s', '-f', fname, trg]) - """ Test --print-data-base with errors """ - sys.argv = ['', '-s', '-p', '-f', fname, 'err'] - main() - """ Test --just-print with errors """ - sys.argv = ['', '-s', '-n', '-f', fname, 'err'] - main() +def test_print_data_base(): + """Test --print-data-base with errors""" + main(['-s', '-p', '-f', fname, 'err']) + + +def test_just_print(): + """Test --just-print with errors""" + main(['-s', '-n', '-f', fname, 'err']) - """ Test --ignore-errors """ - sys.argv = ['', '-s', '-f', fname, 'err'] + +def test_ignore_errors(): + """Test --ignore-errors""" try: - main() + main(['-s', '-f', fname, 'err']) except OSError: pass # test passed if file not found else: raise PymakeTypeError('err') - sys.argv = ['', '-s', '-i', '-f', fname, 'err'] - main() + main(['-s', '-i', '-f', fname, 'err']) - """ Test help and version """ + +def test_help_version(): + """Test help and version""" for i in ('-h', '--help', '-v', '--version'): - sys.argv = ['', i] try: - main() + main([i]) except SystemExit: pass - - # clean up - sys.stdin, sys.argv = _SYS diff --git a/pymake/tests/tests_pymake.py b/pymake/tests/tests_pymake.py index a100a7f..76c9b4a 100644 --- a/pymake/tests/tests_pymake.py +++ b/pymake/tests/tests_pymake.py @@ -1,5 +1,5 @@ def test_exceptions(): - """ Test Exceptions """ + """Test Exceptions""" from pymake import PymakeTypeError # NOQA from pymake import PymakeKeyError # NOQA diff --git a/pymake/tests/tests_version.py b/pymake/tests/tests_version.py index 8d7b360..a78db74 100644 --- a/pymake/tests/tests_version.py +++ b/pymake/tests/tests_version.py @@ -2,7 +2,7 @@ def test_version(): - """ Test version string """ + """Test version string""" from pymake import __version__ Mmpe = re.split('[.-]', __version__) assert 3 <= len(Mmpe) <= 4 diff --git a/setup.py b/setup.py index cdc61d2..de507db 100755 --- a/setup.py +++ b/setup.py @@ -1,168 +1,32 @@ #!python # -*- coding: utf-8 -*- -""" -The funny thing is we can't very well import this module to install itself. -So we reimplement Makefile parsing all over again here! -""" - - import os try: from setuptools import setup except ImportError: from distutils.core import setup 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 - import configparser as ConfigParser - import io as StringIO -import re - - -# 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) - - -# Main setup.py config # - # Get version from pymake/_version.py __version__ = None -version_file = os.path.join(os.path.dirname(__file__), 'pymake', '_version.py') +src_dir = os.path.abspath(os.path.dirname(__file__)) +version_file = os.path.join(src_dir, 'pymake', '_version.py') with io_open(version_file, mode='r') as fd: exec(fd.read()) # Executing makefile commands if specified if sys.argv[1].lower().strip() == 'make': + import pymake # requires package to be installed already # 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:") - 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 + fpath = os.path.join(src_dir, 'Makefile') + pymake.main(['-f', fpath] + sys.argv[2:]) + # Stop to avoid setup.py raising non-standard command error sys.exit(0) - -# Python package config # - README_rst = '' -with io_open('README.rst', mode='r', encoding='utf-8') as fd: +fndoc = os.path.join(src_dir, 'README.rst') +with io_open(fndoc, mode='r', encoding='utf-8') as fd: README_rst = fd.read() setup( @@ -179,21 +43,32 @@ def execute_makefile_commands(commands, alias, verbose=False): packages=['pymake'], install_requires=['docopt>=0.6.0'], entry_points={'console_scripts': ['pymake=pymake._main:main'], }, + package_data={'py-make': ['CONTRIBUTE', 'LICENCE', + 'examples/*.py', 'examples/Makefile*']}, long_description=README_rst, + python_requires='>=2.6, !=3.0.*, !=3.1.*', classifiers=[ # Trove classifiers - # (https://pypi.python.org/pypi?%3Aaction=list_classifiers) - 'Development Status :: 3 - Alpha', - 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)', - 'License :: OSI Approved :: MIT License', + # (https://pypi.org/pypi?%3Aaction=list_classifiers) + 'Development Status :: 4 - Beta', 'Environment :: Console', + 'Environment :: MacOS X', + 'Environment :: Other Environment', + 'Environment :: Win32 (MS Windows)', + 'Environment :: X11 Applications', 'Framework :: IPython', - 'Operating System :: Microsoft :: Windows', + 'Intended Audience :: Developers', + 'Intended Audience :: Education', + 'Intended Audience :: End Users/Desktop', + 'Intended Audience :: Other Audience', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)', 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', - 'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: BSD', 'Operating System :: POSIX :: BSD :: FreeBSD', + 'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: SunOS/Solaris', 'Programming Language :: Python', 'Programming Language :: Python :: 2', @@ -204,15 +79,22 @@ def execute_makefile_commands(commands, alias, verbose=False): 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: Implementation :: PyPy', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: IronPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Desktop Environment', + 'Topic :: Education :: Testing', + 'Topic :: Office/Business', + 'Topic :: Other/Nonlisted Topic', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: User Interfaces', 'Topic :: System :: Monitoring', + 'Topic :: System :: Shells', 'Topic :: Terminals', - 'Topic :: Utilities', - 'Intended Audience :: Developers', + 'Topic :: Utilities' ], keywords='make makefile gnumake gnu console terminal cli', test_suite='nose.collector', diff --git a/tox.ini b/tox.ini index 2e51984..1e84359 100644 --- a/tox.ini +++ b/tox.ini @@ -1,29 +1,42 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests +# Tox (https://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] -envlist = py26, py27, py32, py33, py34, py35, pypy, pypy3, flake8, setup.py +# deprecation warning: py{26,32,33,34} +envlist = py{26,27,33,34,35,36,37,py,py3}, flake8, setup.py [testenv] -passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH +passenv = CI TRAVIS TRAVIS_* TOXENV CODECOV_* deps = nose nose-timer - coverage<4 + coverage coveralls + codecov commands = nosetests --with-coverage --with-timer --cover-package=pymake -d -v pymake/ - coveralls + codecov + +[testenv:py26] +deps = + nose + coverage + coveralls==1.2.0 + codecov + pycparser==2.18 + idna==2.7 +commands = + nosetests --with-coverage --cover-package=pymake -d -v pymake/ + - coveralls + codecov [testenv:flake8] deps = flake8 commands = - flake8 --max-line-length=80 --count --statistics --exit-zero pymake/ - flake8 --max-line-length=80 --count --statistics --exit-zero examples/ - flake8 --max-line-length=80 --count --statistics --exit-zero . - flake8 --max-line-length=80 --count --statistics --exit-zero pymake/tests/ + flake8 --max-line-length=80 -j 8 --count --statistics --exit-zero . [testenv:setup.py] deps =