├── .coveragerc
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── MANIFEST.in
├── README.rst
├── RELEASING.md
├── coverage_badge
├── __main__.py
└── templates
│ └── flat.svg
├── example.svg
├── media
├── 15.svg
├── 45.svg
├── 65.svg
├── 80.svg
├── 93.svg
├── 97.svg
└── na.svg
├── pytest.ini
├── requirements-dev.txt
├── setup.cfg
├── setup.py
└── tests
├── conftest.py
└── test_output.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | include = coverage_badge/**
3 | omit = venv/*,VENV/*
4 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | pull_request:
6 |
7 | name: CI
8 |
9 | jobs:
10 |
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python:
16 | - '3.9'
17 | - '3.10'
18 | - '3.11'
19 | - '3.12'
20 | - 'pypy3.10'
21 | coverage:
22 | - '6.0'
23 | - '7.0'
24 | - '7.5'
25 | name: Python ${{ matrix.python }} on coverage ${{ matrix.coverage }}
26 | steps:
27 | - uses: actions/checkout@v4
28 | - name: Setup python ${{ matrix.python }}
29 | uses: actions/setup-python@v5
30 | with:
31 | python-version: ${{ matrix.python }}
32 | - name: Install dependencies
33 | run: pip install setuptools coverage==${{ matrix.coverage}} && pip install . && pip install -r requirements-dev.txt
34 | - name: Run tests
35 | run: python -m pytest
36 | - name: Run coverage-badge on own test coverage
37 | run: python -m coverage_badge
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.pyc
3 | __pycache__
4 | build/
5 | dist/
6 | *.egg-info/
7 | .cache/
8 | .coverage
9 | venv/
10 | VENV/
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | This project follows semantic versioning.
4 |
5 | Possible log types:
6 |
7 | - `[added]` for new features.
8 | - `[changed]` for changes in existing functionality.
9 | - `[deprecated]` for once-stable features removed in upcoming releases.
10 | - `[removed]` for features removed in this release.
11 | - `[fixed]` for any bug fixes.
12 | - `[security]` to invite users to upgrade in case of vulnerabilities.
13 | - `[chore]` for maintenance changes
14 |
15 |
16 | ### v1.1.2 (2024-08-03)
17 |
18 | - [changed] Include setuptools as dependency (#31)
19 |
20 | Contributors to this version (thanks!):
21 |
22 | - [@AdrienPensart](https://github.com/AdrienPensart)
23 |
24 | Note: This project is now in maintenance mode. See `README.md` for more details.
25 |
26 |
27 | ### v1.1.1 (2024-04-24)
28 |
29 | - [fixed] Fix compatibility with coverage 7.5 (#28, #29)
30 | - [changed] Drop Python <=3.8 support
31 |
32 | Contributors to this version (thanks!):
33 |
34 | - [@danielpodrazka](https://github.com/danielpodrazka)
35 |
36 | Note: This project is now in maintenance mode. See `README.md` for more details.
37 |
38 |
39 | ### v1.1.0 (2021-11-12)
40 |
41 | - [added] Compatibility with coverage.py 6.x (#17)
42 |
43 | Contributors to this version (thanks!):
44 |
45 | - [@didorothy](https://github.com/didorothy)
46 |
47 |
48 | ### v1.0.2 (2021-10-05)
49 |
50 | - [changed] Drop Python <=3.5 support
51 | - [change] Update install requirements to include coverage (#12)
52 | - [fixed] Lock coverage.py version dependency to 5.x (#14)
53 | - [chore] Switch from TravisCI to GitHub actions
54 | - [chore] Upgrade pytest
55 | - [chore] Improve docs
56 |
57 | Contributors to this version (thanks!):
58 |
59 | - [@jackton1](https://github.com/jackton1)
60 | - [@astariul](https://github.com/astariul)
61 |
62 |
63 | ### v1.0.1 (2019-03-29)
64 |
65 | - [changed] Move Precision class into function scope
66 |
67 |
68 | ### v1.0.0 (2019-03-29)
69 |
70 | - [fixed] Use coverage package for rounding the percentage (#6)
71 |
72 | Contributors to this version (thanks!):
73 |
74 | - [@simonfagerholm](https://github.com/simonfagerholm)
75 |
76 |
77 | ### v0.2.0 (2016-11-29)
78 |
79 | - [fixed] Add XML declaration to generated SVGs (#4)
80 | - [added] Add support for multi-color badges (#3)
81 |
82 | Contributors to this version (thanks!):
83 |
84 | - [@inquire](https://github.com/inquire)
85 | - [@samael500](https://github.com/samael500)
86 |
87 |
88 | ### v0.1.2 (2015-10-13)
89 |
90 | - [fixed] Bugfix in entry point
91 |
92 |
93 | ### v0.1.1 (2015-10-13)
94 |
95 | - [chore] Set up testing
96 |
97 |
98 | ### v0.1.0 (2015-10-13)
99 |
100 | - [chore] Initial release
101 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst LICENSE.txt
2 | include coverage_badge/templates/*.svg
3 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Coverage.py Badge
2 | ==================
3 |
4 | .. |buildstatus| image:: https://github.com/dbrgn/coverage-badge/workflows/CI/badge.svg
5 | :alt: Build status
6 | :target: https://github.com/dbrgn/coverage-badge/actions?query=branch%3Amain
7 | .. |downloads| image:: https://img.shields.io/pypi/dm/coverage-badge.svg
8 | :alt: PyPI Downloads
9 | :target: https://pypi.python.org/pypi/coverage-badge
10 | .. |example| image:: https://cdn.rawgit.com/dbrgn/coverage-badge/main/example.svg
11 | :alt: Example coverage badge
12 |
13 | |buildstatus| |downloads|
14 |
15 | ⚠️ coverage-badge is in maintenance mode. I might still do occasional updates
16 | and fixes from time to time, but there will be no added features. Most
17 | people using coverage-badge might want to use genbadge_ instead, which has
18 | more features (e.g. test badges, flake8 reports, etc).
19 |
20 | A small script to generate coverage badges using Coverage.py.
21 |
22 | Example of a generated badge: |example|
23 |
24 | The badge template has been taken from shields.io_, therefore it should look
25 | mostly good. (The spec is a bit stricter on the margins, but I can't easily do
26 | text width calculations in Python so the margins might not always be 4px.)
27 |
28 | .. _shields.io: http://shields.io/
29 | .. _genbadge: https://smarie.github.io/python-genbadge/
30 |
31 | Installation
32 | ------------
33 | Run:
34 |
35 | .. code-block::
36 |
37 | pip install coverage-badge
38 |
39 |
40 | Usage
41 | -----
42 |
43 | First, run Coverage.py to generate the necessary coverage data. Then you can
44 | either return the badge SVG to stdout::
45 |
46 | $ coverage-badge
47 |
48 | ...or write it to a file::
49 |
50 | $ coverage-badge -o coverage.svg
51 |
52 | It's important that you run ``coverage-badge`` from the directory where the
53 | ``.coverage`` data file is located.
54 |
55 | Different colors for cover ranges:
56 |
57 | .. image:: https://cdn.rawgit.com/dbrgn/coverage-badge/main/media/15.svg
58 | :alt: 15%
59 |
60 | .. image:: https://cdn.rawgit.com/dbrgn/coverage-badge/main/media/45.svg
61 | :alt: 45%
62 |
63 | .. image:: https://cdn.rawgit.com/dbrgn/coverage-badge/main/media/65.svg
64 | :alt: 65%
65 |
66 | .. image:: https://cdn.rawgit.com/dbrgn/coverage-badge/main/media/80.svg
67 | :alt: 80%
68 |
69 | .. image:: https://cdn.rawgit.com/dbrgn/coverage-badge/main/media/93.svg
70 | :alt: 93%
71 |
72 | .. image:: https://cdn.rawgit.com/dbrgn/coverage-badge/main/media/97.svg
73 | :alt: 97%
74 |
75 | ----
76 |
77 | The full usage text::
78 |
79 | usage: __main__.py [-h] [-o FILEPATH] [-p] [-f] [-q] [-v]
80 |
81 | Generate coverage badges for Coverage.py.
82 |
83 | optional arguments:
84 | -h, --help show this help message and exit
85 | -o FILEPATH Save the file to the specified path.
86 | -p Plain color mode. Standard green badge.
87 | -f Force overwrite image, use with -o key.
88 | -q Don't output any non-error messages.
89 | -v Show version.
90 |
91 | License
92 | -------
93 |
94 | MIT License, see `LICENSE.txt` file..
95 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Release process
2 |
3 | Signing key: https://bargen.dev/B993FF98A90C9AB1.txt
4 |
5 | Used variables:
6 |
7 | export VERSION={VERSION}
8 | export GPG=20EE002D778AE197EF7D0D2CB993FF98A90C9AB1
9 |
10 | Update version number:
11 |
12 | vim -p setup.py coverage_badge/__main__.py CHANGELOG.md
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 | Upload package to PyPI:
26 |
27 | twine3 upload dist/coverage[-_]badge-${VERSION}*
28 | git push
29 | git push --tags
30 |
--------------------------------------------------------------------------------
/coverage_badge/__main__.py:
--------------------------------------------------------------------------------
1 | """
2 | Generate coverage badges for Coverage.py.
3 | """
4 | import os
5 | import sys
6 | import argparse
7 | import pkg_resources
8 |
9 | import coverage
10 |
11 |
12 | __version__ = '1.1.2'
13 |
14 |
15 | DEFAULT_COLOR = '#a4a61d'
16 | COLORS = {
17 | 'brightgreen': '#4c1',
18 | 'green': '#97CA00',
19 | 'yellowgreen': '#a4a61d',
20 | 'yellow': '#dfb317',
21 | 'orange': '#fe7d37',
22 | 'red': '#e05d44',
23 | 'lightgrey': '#9f9f9f',
24 | }
25 |
26 | COLOR_RANGES = [
27 | (95, 'brightgreen'),
28 | (90, 'green'),
29 | (75, 'yellowgreen'),
30 | (60, 'yellow'),
31 | (40, 'orange'),
32 | (0, 'red'),
33 | ]
34 |
35 |
36 | class Devnull(object):
37 | """
38 | A file like object that does nothing.
39 | """
40 | def write(self, *args, **kwargs):
41 | pass
42 |
43 |
44 | def get_total():
45 | """
46 | Return the rounded total as properly rounded string.
47 | """
48 | cov = coverage.Coverage()
49 | cov.load()
50 | total = cov.report(file=Devnull())
51 |
52 | if hasattr(coverage.results.Numbers, 'set_precision'): # Coverage <= 5
53 | class Precision(coverage.results.Numbers):
54 | """
55 | A class for using the percentage rounding of the main coverage package,
56 | with any percentage.
57 |
58 | To get the string format of the percentage, use the ``pc_covered_str``
59 | property.
60 |
61 | """
62 | def __init__(self, percent):
63 | self.percent = percent
64 |
65 | @property
66 | def pc_covered(self):
67 | return self.percent
68 |
69 | return Precision(total).pc_covered_str
70 | elif hasattr(coverage.results.Numbers, 'display_covered'): # Coverage 6.x < 7.5
71 | # NOTE: Precision is no longer set globally in the
72 | # `coverage.results.Numbers` class. Instead the precision must be
73 | # passed in as the first argument. We pull the precision from the
74 | # `coverage.Coverage` object because it should pull the correct
75 | # precision from the local .coveragerc file.
76 | return coverage.results.Numbers(precision=cov.config.precision).display_covered(total)
77 | else: # Coverage >= 7.5
78 | return coverage.results.display_covered(total, cov.config.precision)
79 |
80 |
81 |
82 | def get_color(total):
83 | """
84 | Return color for current coverage precent
85 | """
86 | try:
87 | xtotal = int(total)
88 | except ValueError:
89 | return COLORS['lightgrey']
90 | for range_, color in COLOR_RANGES:
91 | if xtotal >= range_:
92 | return COLORS[color]
93 |
94 |
95 | def get_badge(total, color=DEFAULT_COLOR):
96 | """
97 | Read the SVG template from the package, update total, return SVG as a
98 | string.
99 | """
100 | template_path = os.path.join('templates', 'flat.svg')
101 | template = pkg_resources.resource_string(__name__, template_path).decode('utf8')
102 | return template.replace('{{ total }}', total).replace('{{ color }}', color)
103 |
104 |
105 | def parse_args(argv=None):
106 | """
107 | Parse the command line arguments.
108 | """
109 | parser = argparse.ArgumentParser(description=__doc__)
110 | parser.add_argument('-o', dest='filepath',
111 | help='Save the file to the specified path.')
112 | parser.add_argument('-p', dest='plain_color', action='store_true',
113 | help='Plain color mode. Standard green badge.')
114 | parser.add_argument('-f', dest='force', action='store_true',
115 | help='Force overwrite image, use with -o key.')
116 | parser.add_argument('-q', dest='quiet', action='store_true',
117 | help='Don\'t output any non-error messages.')
118 | parser.add_argument('-v', dest='print_version', action='store_true',
119 | help='Show version.')
120 |
121 | # If arguments have been passed in, use them.
122 | if argv:
123 | return parser.parse_args(argv)
124 |
125 | # Otherwise, just use sys.argv directly.
126 | else:
127 | return parser.parse_args()
128 |
129 |
130 | def save_badge(badge, filepath, force=False):
131 | """
132 | Save badge to the specified path.
133 | """
134 | # Validate path (part 1)
135 | if filepath.endswith('/'):
136 | print('Error: Filepath may not be a directory.')
137 | sys.exit(1)
138 |
139 | # Get absolute filepath
140 | path = os.path.abspath(filepath)
141 | if not path.lower().endswith('.svg'):
142 | path += '.svg'
143 |
144 | # Validate path (part 2)
145 | if not force and os.path.exists(path):
146 | print('Error: "{}" already exists.'.format(path))
147 | sys.exit(1)
148 |
149 | # Write file
150 | with open(path, 'w') as f:
151 | f.write(badge)
152 |
153 | return path
154 |
155 |
156 | def main(argv=None):
157 | """
158 | Console scripts entry point.
159 | """
160 | args = parse_args(argv)
161 |
162 | # Print version
163 | if args.print_version:
164 | print('coverage-badge v{}'.format(__version__))
165 | sys.exit(0)
166 |
167 | # Check for coverage
168 | if coverage is None:
169 | print('Error: Python coverage module not installed.')
170 | sys.exit(1)
171 |
172 | # Generate badge
173 | try:
174 | total = get_total()
175 | except coverage.misc.CoverageException as e:
176 | print('Error: {} Did you run coverage first?'.format(e))
177 | sys.exit(1)
178 |
179 | color = DEFAULT_COLOR if args.plain_color else get_color(total)
180 | badge = get_badge(total, color)
181 |
182 | # Show or save output
183 | if args.filepath:
184 | path = save_badge(badge, args.filepath, args.force)
185 | if not args.quiet:
186 | print('Saved badge to {}'.format(path))
187 | else:
188 | print(badge, end='')
189 |
190 |
191 | if __name__ == '__main__':
192 | main()
193 |
--------------------------------------------------------------------------------
/coverage_badge/templates/flat.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = --tb=short --cov
3 | python_files = test_*.py
4 | norecursedirs = *.egg tmp* build .tox venv VENV
5 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pytest==6.2.5
2 | pytest-cache==1.0
3 | pytest-cov==3.0.0
4 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal=1
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | readme = open('README.rst').read()
4 |
5 | setup(name='coverage-badge',
6 | version='1.1.2',
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', 'setuptools'],
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 :: 3',
29 | 'Topic :: Software Development :: Testing',
30 | ],
31 | )
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------