├── 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 |
22 |
--------------------------------------------------------------------------------
/media/15.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/media/45.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/media/65.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/media/80.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/media/93.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/media/97.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/media/na.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/coverage_badge/templates/flat.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 '\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 | \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 | \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 |
--------------------------------------------------------------------------------