├── coverage_badge ├── __init__.py ├── templates │ └── flat.svg └── __main__.py ├── setup.cfg ├── MANIFEST.in ├── requirements-dev.txt ├── .gitignore ├── .travis.yml ├── pytest.ini ├── tests ├── conftest.py └── test_output.py ├── RELEASING.md ├── example.svg ├── media ├── 15.svg ├── 45.svg ├── 65.svg ├── 80.svg ├── 93.svg ├── 97.svg └── na.svg ├── LICENSE.txt ├── setup.py └── README.rst /coverage_badge/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE.txt 2 | include coverage_badge/templates/*.svg 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==2.8.2 2 | pytest-cache==1.0 3 | pytest-cov==2.2.0 4 | pytest-pep8==1.0.6 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | __pycache__ 4 | build/ 5 | dist/ 6 | *.egg-info/ 7 | .cache/ 8 | .coverage 9 | venv/ 10 | VENV/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.4 5 | - 3.5 6 | - 3.6 7 | - 3.7 8 | - 3.8 9 | - 3.9 10 | install: 11 | - pip install -r requirements-dev.txt 12 | script: 13 | - py.test 14 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --pep8 --tb=short --cov 3 | python_files = test_*.py 4 | norecursedirs = *.egg tmp* build .tox 5 | pep8ignore = 6 | *.py E126 E127 E128 7 | setup.py ALL 8 | */tests/* ALL 9 | */docs/* ALL 10 | */build/* ALL 11 | */TOXENV/* ALL 12 | VENV/* ALL 13 | pep8maxlinelength = 99 14 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import, unicode_literals 3 | 4 | import sys 5 | import os 6 | 7 | current_dir = os.path.dirname(os.path.abspath(__file__)) 8 | parent_dir = os.path.normpath(os.path.join(current_dir, '..')) 9 | sys.path.insert(0, parent_dir) 10 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | Signing key: 3578F667F2F3A5FA (https://keybase.io/dbrgn) 4 | 5 | Used variables: 6 | 7 | export VERSION={VERSION} 8 | export GPG=3578F667F2F3A5FA 9 | 10 | Update version number: 11 | 12 | vim -p setup.py coverage_badge/__main__.py 13 | 14 | Do a signed commit and signed tag of the release: 15 | 16 | git add setup.py coverage_badge/__main__.py 17 | git commit -S${GPG} -m "Release v${VERSION}" 18 | git tag -u ${GPG} -m "Release v${VERSION}" v${VERSION} 19 | 20 | Build source and binary distributions: 21 | 22 | python3 setup.py sdist 23 | python3 setup.py bdist_wheel 24 | 25 | Sign files: 26 | 27 | gpg --detach-sign -u ${GPG} -a dist/coverage-badge-${VERSION}.tar.gz 28 | gpg --detach-sign -u ${GPG} -a dist/coverage_badge-${VERSION}-py2.py3-none-any.whl 29 | 30 | Upload package to PyPI: 31 | 32 | twine3 upload dist/coverage[-_]badge-${VERSION}* 33 | git push 34 | git push --tags 35 | -------------------------------------------------------------------------------- /example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 24% 19 | 24% 20 | 21 | 22 | -------------------------------------------------------------------------------- /media/15.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 15% 19 | 15% 20 | 21 | 22 | -------------------------------------------------------------------------------- /media/45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 45% 19 | 45% 20 | 21 | 22 | -------------------------------------------------------------------------------- /media/65.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 65% 19 | 65% 20 | 21 | 22 | -------------------------------------------------------------------------------- /media/80.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 80% 19 | 80% 20 | 21 | 22 | -------------------------------------------------------------------------------- /media/93.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 93% 19 | 93% 20 | 21 | 22 | -------------------------------------------------------------------------------- /media/97.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 97% 19 | 97% 20 | 21 | 22 | -------------------------------------------------------------------------------- /media/na.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | na% 19 | na% 20 | 21 | 22 | -------------------------------------------------------------------------------- /coverage_badge/templates/flat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | {{ total }}% 19 | {{ total }}% 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2020 Danilo Bargen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | readme = open('README.rst').read() 4 | 5 | setup(name='coverage-badge', 6 | version='1.0.1', 7 | description='Generate coverage badges for Coverage.py.', 8 | author='Danilo Bargen', 9 | author_email='mail@dbrgn.ch', 10 | url='https://github.com/dbrgn/coverage-badge', 11 | install_requires=['coverage'], 12 | packages=['coverage_badge'], 13 | zip_safe=True, 14 | include_package_data=True, 15 | license='MIT', 16 | keywords='coverage badge shield', 17 | long_description=readme, 18 | entry_points={ 19 | 'console_scripts': [ 20 | 'coverage-badge = coverage_badge.__main__:main', 21 | ] 22 | }, 23 | classifiers=[ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Environment :: Console', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python :: 2', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3', 31 | 'Topic :: Software Development :: Testing', 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /tests/test_output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import, unicode_literals 3 | 4 | import sys 5 | from textwrap import dedent 6 | 7 | import pytest 8 | 9 | from coverage_badge import __main__ 10 | 11 | 12 | @pytest.fixture 13 | def cb(monkeypatch): 14 | """ 15 | Return a monkey patched coverage_badge module that always returns a percentage of 79. 16 | """ 17 | def get_fake_total(): 18 | return '79' 19 | monkeypatch.setattr(__main__, 'get_total', get_fake_total) 20 | return __main__ 21 | 22 | 23 | def test_version(cb, capsys): 24 | """ 25 | Test the version output. 26 | """ 27 | with pytest.raises(SystemExit) as se: 28 | cb.main(['-v']) 29 | out, _ = capsys.readouterr() 30 | assert out == 'coverage-badge v%s\n' % __main__.__version__ 31 | 32 | 33 | def test_svg_output(cb, capsys): 34 | """ 35 | Test the SVG output. 36 | """ 37 | cb.main([]) 38 | out, _ = capsys.readouterr() 39 | assert out.startswith('') 40 | assert '' in out 41 | assert '79%' in out 42 | assert out.endswith('\n') 43 | 44 | 45 | def test_color_ranges(cb, capsys): 46 | """ 47 | Test color total value 48 | """ 49 | for total, color in (('97', '#4c1'), ('93', '#97CA00'), ('80', '#a4a61d'), ('65', '#dfb317'), 50 | ('45', '#fe7d37'), ('15', '#e05d44'), ('n/a', '#9f9f9f')): 51 | __main__.get_total = lambda: total 52 | cb.main([]) 53 | out, _ = capsys.readouterr() 54 | row = '' % color 55 | assert out.startswith(dedent('''\ 56 | 57 | ''')) 58 | assert row in out 59 | assert out.endswith('\n') 60 | 61 | 62 | def test_plain_color_mode(cb, capsys): 63 | """ 64 | Should get always one color in badge 65 | """ 66 | assert __main__.DEFAULT_COLOR == '#a4a61d' 67 | for total in ('97', '93', '80', '65', '45', '15', 'n/a'): 68 | __main__.get_total = lambda: total 69 | cb.main(['-p']) 70 | out, _ = capsys.readouterr() 71 | row = '' 72 | assert out.startswith(dedent('''\ 73 | 74 | ''')) 75 | assert row in out 76 | assert out.endswith('\n') 77 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Coverage.py Badge 2 | ================== 3 | 4 | .. image:: https://img.shields.io/pypi/dm/coverage-badge.svg 5 | :alt: PyPI Downloads 6 | :target: https://pypi.python.org/pypi/coverage-badge 7 | 8 | A small script to generate coverage badges using Coverage.py. Example of a generated badge: 9 | 10 | .. image:: https://cdn.rawgit.com/dbrgn/coverage-badge/master/example.svg 11 | :alt: Example coverage badge 12 | 13 | The badge template has been taken from shields.io_, therefore it should look 14 | mostly good. (The spec is a bit stricter on the margins, but I can't easily do 15 | text width calculations in Python so the margins might not always be 4px.) 16 | 17 | **:arrow_right: Note:** If you need a script with a few more features 18 | (e.g. test badges, flake8 reports, etc), check out genbadge_. 19 | 20 | .. _shields.io: http://shields.io/ 21 | .. _genbadge: https://smarie.github.io/python-genbadge/ 22 | 23 | Installation 24 | ------------ 25 | Run: 26 | 27 | .. code-block:: 28 | 29 | pip install coverage-badge 30 | 31 | 32 | Usage 33 | ----- 34 | 35 | First, run Coverage.py to generate the necessary coverage data. Then you can 36 | either return the badge SVG to stdout:: 37 | 38 | $ coverage-badge 39 | 40 | ...or write it to a file:: 41 | 42 | $ coverage-badge -o coverage.svg 43 | 44 | It's important that you run ``coverage-badge`` from the directory where the 45 | ``.coverage`` data file is located. 46 | 47 | Different colors for cover ranges: 48 | 49 | .. image:: https://cdn.rawgit.com/samael500/coverage-badge/master/media/15.svg 50 | :alt: 15% 51 | 52 | .. image:: https://cdn.rawgit.com/samael500/coverage-badge/master/media/45.svg 53 | :alt: 45% 54 | 55 | .. image:: https://cdn.rawgit.com/samael500/coverage-badge/master/media/65.svg 56 | :alt: 65% 57 | 58 | .. image:: https://cdn.rawgit.com/samael500/coverage-badge/master/media/80.svg 59 | :alt: 80% 60 | 61 | .. image:: https://cdn.rawgit.com/samael500/coverage-badge/master/media/93.svg 62 | :alt: 93% 63 | 64 | .. image:: https://cdn.rawgit.com/samael500/coverage-badge/master/media/97.svg 65 | :alt: 97% 66 | 67 | ---- 68 | 69 | The full usage text:: 70 | 71 | usage: __main__.py [-h] [-o FILEPATH] [-p] [-f] [-q] [-v] 72 | 73 | Generate coverage badges for Coverage.py. 74 | 75 | optional arguments: 76 | -h, --help show this help message and exit 77 | -o FILEPATH Save the file to the specified path. 78 | -p Plain color mode. Standard green badge. 79 | -f Force overwrite image, use with -o key. 80 | -q Don't output any non-error messages. 81 | -v Show version. 82 | 83 | License 84 | ------- 85 | 86 | MIT License, see `LICENSE.txt` file.. 87 | -------------------------------------------------------------------------------- /coverage_badge/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generate coverage badges for Coverage.py. 3 | """ 4 | # -*- coding: utf-8 -*- 5 | from __future__ import print_function, division, absolute_import, unicode_literals 6 | 7 | import os 8 | import sys 9 | import argparse 10 | import pkg_resources 11 | 12 | import coverage 13 | 14 | 15 | __version__ = '1.0.1' 16 | 17 | 18 | DEFAULT_COLOR = '#a4a61d' 19 | COLORS = { 20 | 'brightgreen': '#4c1', 21 | 'green': '#97CA00', 22 | 'yellowgreen': '#a4a61d', 23 | 'yellow': '#dfb317', 24 | 'orange': '#fe7d37', 25 | 'red': '#e05d44', 26 | 'lightgrey': '#9f9f9f', 27 | } 28 | 29 | COLOR_RANGES = [ 30 | (95, 'brightgreen'), 31 | (90, 'green'), 32 | (75, 'yellowgreen'), 33 | (60, 'yellow'), 34 | (40, 'orange'), 35 | (0, 'red'), 36 | ] 37 | 38 | 39 | class Devnull(object): 40 | """ 41 | A file like object that does nothing. 42 | """ 43 | def write(self, *args, **kwargs): 44 | pass 45 | 46 | 47 | def get_total(): 48 | """ 49 | Return the rounded total as properly rounded string. 50 | """ 51 | cov = coverage.Coverage() 52 | cov.load() 53 | total = cov.report(file=Devnull()) 54 | 55 | class Precision(coverage.results.Numbers): 56 | """ 57 | A class for using the percentage rounding of the main coverage package, 58 | with any percentage. 59 | 60 | To get the string format of the percentage, use the ``pc_covered_str`` 61 | property. 62 | 63 | """ 64 | def __init__(self, percent): 65 | self.percent = percent 66 | 67 | @property 68 | def pc_covered(self): 69 | return self.percent 70 | 71 | return Precision(total).pc_covered_str 72 | 73 | 74 | def get_color(total): 75 | """ 76 | Return color for current coverage precent 77 | """ 78 | try: 79 | xtotal = int(total) 80 | except ValueError: 81 | return COLORS['lightgrey'] 82 | for range_, color in COLOR_RANGES: 83 | if xtotal >= range_: 84 | return COLORS[color] 85 | 86 | 87 | def get_badge(total, color=DEFAULT_COLOR): 88 | """ 89 | Read the SVG template from the package, update total, return SVG as a 90 | string. 91 | """ 92 | template_path = os.path.join('templates', 'flat.svg') 93 | template = pkg_resources.resource_string(__name__, template_path).decode('utf8') 94 | return template.replace('{{ total }}', total).replace('{{ color }}', color) 95 | 96 | 97 | def parse_args(argv=None): 98 | """ 99 | Parse the command line arguments. 100 | """ 101 | parser = argparse.ArgumentParser(description=__doc__) 102 | parser.add_argument('-o', dest='filepath', 103 | help='Save the file to the specified path.') 104 | parser.add_argument('-p', dest='plain_color', action='store_true', 105 | help='Plain color mode. Standard green badge.') 106 | parser.add_argument('-f', dest='force', action='store_true', 107 | help='Force overwrite image, use with -o key.') 108 | parser.add_argument('-q', dest='quiet', action='store_true', 109 | help='Don\'t output any non-error messages.') 110 | parser.add_argument('-v', dest='print_version', action='store_true', 111 | help='Show version.') 112 | 113 | # If arguments have been passed in, use them. 114 | if argv: 115 | return parser.parse_args(argv) 116 | 117 | # Otherwise, just use sys.argv directly. 118 | else: 119 | return parser.parse_args() 120 | 121 | 122 | def save_badge(badge, filepath, force=False): 123 | """ 124 | Save badge to the specified path. 125 | """ 126 | # Validate path (part 1) 127 | if filepath.endswith('/'): 128 | print('Error: Filepath may not be a directory.') 129 | sys.exit(1) 130 | 131 | # Get absolute filepath 132 | path = os.path.abspath(filepath) 133 | if not path.lower().endswith('.svg'): 134 | path += '.svg' 135 | 136 | # Validate path (part 2) 137 | if not force and os.path.exists(path): 138 | print('Error: "{}" already exists.'.format(path)) 139 | sys.exit(1) 140 | 141 | # Write file 142 | with open(path, 'w') as f: 143 | f.write(badge) 144 | 145 | return path 146 | 147 | 148 | def main(argv=None): 149 | """ 150 | Console scripts entry point. 151 | """ 152 | args = parse_args(argv) 153 | 154 | # Print version 155 | if args.print_version: 156 | print('coverage-badge v{}'.format(__version__)) 157 | sys.exit(0) 158 | 159 | # Check for coverage 160 | if coverage is None: 161 | print('Error: Python coverage module not installed.') 162 | sys.exit(1) 163 | 164 | # Generate badge 165 | try: 166 | total = get_total() 167 | except coverage.misc.CoverageException as e: 168 | print('Error: {} Did you run coverage first?'.format(e)) 169 | sys.exit(1) 170 | 171 | color = DEFAULT_COLOR if args.plain_color else get_color(total) 172 | badge = get_badge(total, color) 173 | 174 | # Show or save output 175 | if args.filepath: 176 | path = save_badge(badge, args.filepath, args.force) 177 | if not args.quiet: 178 | print('Saved badge to {}'.format(path)) 179 | else: 180 | print(badge, end='') 181 | 182 | 183 | if __name__ == '__main__': 184 | main() 185 | --------------------------------------------------------------------------------