From 59976b5c25db0ca9860c2f91eb9499102a785ca3 Mon Sep 17 00:00:00 2001 From: Jon Grace-Cox Date: Fri, 31 May 2019 23:18:13 -0400 Subject: [PATCH 1/9] Update ignored files * .pytest_cache directory * badge files generated by unittests * htmlcov directory --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 4d8e167..9bce014 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,12 @@ ENV/ .DS_Store demo.svg + +# py.test +.pytest_cache + +# SVG files generated by the unittests +test_badge_*.svg + +# html coverage report +htmlcov/ From 1532cc247e6a55aaabc2c18ff5d16ea489ee2ac2 Mon Sep 17 00:00:00 2001 From: Jon Grace-Cox Date: Fri, 31 May 2019 23:22:56 -0400 Subject: [PATCH 2/9] Add more accurate width calculations for truetype fonts * Use a better estimation technique to guess text widths. * Add unittests for validating badge widths for various size badges. * Add return types to function docstrings. * Update relevant doctests. --- anybadge.py | 228 ++++++++++++++++++++++++++++++++++------- setup.py | 2 +- tests/__init__.py | 0 tests/test_anybadge.py | 86 ++++++++++++++++ 4 files changed, 277 insertions(+), 39 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_anybadge.py diff --git a/anybadge.py b/anybadge.py index 2fd5718..39e4bd8 100755 --- a/anybadge.py +++ b/anybadge.py @@ -8,8 +8,9 @@ simplicity and flexibility. import os import re + # Package information -version = __version__ = "0.1.0.dev2" +version = __version__ = "0.0.0" __version_info__ = tuple(re.split('[.-]', __version__)) __title__ = "anybadge" __summary__ = "A simple, flexible badge generator." @@ -27,7 +28,7 @@ DEFAULT_TEXT_COLOR = '#fff' # supported fonts and font sizes. FONT_WIDTHS = { 'DejaVu Sans,Verdana,Geneva,sans-serif': { - 11: 7 + 11: 10 } } @@ -171,7 +172,10 @@ class Badge(object): @property def value_is_float(self): - """Identify whether the value text is a float.""" + """Identify whether the value text is a float. + + Returns: bool + """ try: _ = float(self.value) except ValueError: @@ -181,7 +185,10 @@ class Badge(object): @property def value_is_int(self): - """Identify whether the value text is an int.""" + """Identify whether the value text is an int. + + Returns: bool + """ try: a = float(self.value) b = int(a) @@ -192,7 +199,10 @@ class Badge(object): @property def value_type(self): - """The Python type associated with the value.""" + """The Python type associated with the value. + + Returns: type + """ if self.value_is_float: return float elif self.value_is_int: @@ -202,60 +212,96 @@ class Badge(object): @property def label_width(self): - """The SVG width of the label text.""" - return self.get_text_width(self.label) + """The SVG width of the label text. + + Returns: int + """ + return int(self.get_text_width(self.label) + (2.0 * self.num_padding_chars * self.font_width)) @property def value_width(self): - """The SVG width of the value text.""" - return self.get_text_width(str(self.value_text)) + """The SVG width of the value text. + + Returns: int + """ + return int(self.get_text_width(str(self.value_text)) + (self.num_padding_chars * self.font_width)) @property def font_width(self): - """Return the badge font width.""" - return self.get_font_width(font_name=self.font_name, font_size=self.font_size) + """Return the width multiplier for a font. + + Returns: + int: Maximum pixel width of badges selected font. + + Example: + + >>> Badge(label='x', value='1').font_width + 10 + """ + return FONT_WIDTHS[self.font_name][self.font_size] @property def color_split_position(self): - """The SVG x position where the color split should occur.""" - return self.get_text_width(' ') + self.label_width + \ - int(float(self.font_width) * float(self.num_padding_chars)) + """The SVG x position where the color split should occur. + + Returns: int + """ + return int(self.font_width + self.label_width + + float(self.font_width) * float(self.num_padding_chars)) @property def label_anchor(self): - """The SVG x position of the middle anchor for the label text.""" + """The SVG x position of the middle anchor for the label text. + + Returns: float + """ return self.color_split_position / 2 @property def value_anchor(self): - """The SVG x position of the middle anchor for the value text.""" + """The SVG x position of the middle anchor for the value text. + + Returns: float + """ return self.color_split_position + ((self.badge_width - self.color_split_position) / 2) @property def label_anchor_shadow(self): - """The SVG x position of the label shadow anchor.""" + """The SVG x position of the label shadow anchor. + + Returns: float + """ return self.label_anchor + 1 @property def value_anchor_shadow(self): - """The SVG x position of the value shadow anchor.""" + """The SVG x position of the value shadow anchor. + + Returns: float + """ return self.value_anchor + 1 @property def badge_width(self): """The total width of badge. - >>> badge = Badge('pylint', '5', font_name='DejaVu Sans,Verdana,Geneva,sans-serif', - ... font_size=11) - >>> badge.badge_width - 91 + Returns: int + + Examples: + + >>> badge = Badge('pylint', '5') + >>> badge.badge_width + 103 """ - return self.get_text_width(' ' + ' ' * int(float(self.num_padding_chars) * 2.0)) \ - + self.label_width + self.value_width + padding = int(self.font_width * (self.num_padding_chars + 3)) + return padding + self.label_width + self.value_width @property def badge_svg_text(self): - """The badge SVG text.""" + """The badge SVG text. + + Returns: str + """ # Identify whether template is a file or the actual template text if len(self.template.split('\n')) == 1: @@ -279,30 +325,29 @@ class Badge(object): .replace('{{ color split x }}', str(self.color_split_position)) \ .replace('{{ value width }}', str(self.badge_width - self.color_split_position)) - @staticmethod - def get_font_width(font_name, font_size): - """Return the width multiplier for a font. - - >>> Badge.get_font_width('DejaVu Sans,Verdana,Geneva,sans-serif', 11) - 7 - """ - return FONT_WIDTHS[font_name][font_size] - def get_text_width(self, text): """Return the width of text. + Args: + text(str): Text to get the pixel width of. + + Returns: + int: Pixel width of the given text based on the badges selected font. + This implementation assumes a fixed font of: font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11" >>> badge = Badge('x', 1, font_name='DejaVu Sans,Verdana,Geneva,sans-serif', font_size=11) >>> badge.get_text_width('pylint') - 42 + 34 """ - return len(text) * self.get_font_width(self.font_name, self.font_size) + return _get_approx_string_width(text, self.font_width) @property def badge_color(self): - """Find the badge color based on the thresholds.""" + """Badge color based on the configured thresholds. + + Returns: str""" # If no thresholds were passed then return the default color if not self.thresholds: return self.default_color @@ -331,7 +376,10 @@ class Badge(object): @property def badge_color_code(self): - """Return the color code for the badge.""" + """Return the color code for the badge. + + Returns: str + """ color = self.badge_color if color[0] == '#': return color @@ -357,6 +405,109 @@ class Badge(object): file_handle.write(self.badge_svg_text) +# Based on the following SO answer: https://stackoverflow.com/a/16008023/6252525 +def _get_approx_string_width(text, font_width, fixed_width=False): + """ + Get the approximate width of a string using a specific average font width. + + Args: + text(str): Text string to calculate width of. + font_width(int): Average width of font characters. + fixed_width(bool): Indicates that the font is fixed width. + + Returns: + int: Width of string in pixels. + + Examples: + + Call the function with a string and the maximum character width of the font you are using: + + >>> int(_get_approx_string_width('hello', 10)) + 29 + + This example shows the comparison of simplistic calculation based on a fixed width. + Given a test string and a fixed font width of 10, we can calculate the width + by multiplying the length and the font character with: + + >>> test_string = 'GOOGLE|ijkl' + >>> _get_approx_string_width(test_string, 10, fixed_width=True) + 110 + + Since some characters in the string are thinner than others we expect that the + apporximate text width will be narrower than the fixed width calculation: + + >>> _get_approx_string_width(test_string, 10) + 77 + + """ + if fixed_width: + return len(text) * font_width + + size = 0.0 + + # A dictionary containing percentages that relate to how wide + # each character will be represented in a variable width font. + # These percentages can be calculated using the ``_get_character_percentage_dict`` function. + char_width_percentages = { + "lij|' ": 40.0, + '![]fI.,:;/\\t': 50.0, + '`-(){}r"': 60.0, + '*^zcsJkvxy': 70.0, + 'aebdhnopqug#$L+<>=?_~FZT0123456789': 70.0, + 'BSPEAKVXY&UwNRCHD': 70.0, + 'QGOMm%W@': 100.0 + } + + for s in text: + percentage = 100.0 + for k in char_width_percentages.keys(): + if s in k: + percentage = char_width_percentages[k] + break + size += (percentage / 100.0) * float(font_width) + + return int(size) + + +def _get_character_percentage_dict(font_path, font_size): + """Get the dictionary used to estimate variable width font text lengths. + + Args: + font_path(str): Path to valid font file. + font_size(int): Font size to use. + + Returns: dict + + This function can be used to calculate the dictionary used in the + ``get_approx_string_width`` function. + + Examples: + >>> _get_character_percentage_dict('/Library/Fonts/Verdana.ttf', 9) # doctest: +ELLIPSIS + {"lij|' ": 40, '![]fI.,:;/\\\\t': 50, '`-(){}r"': 60, '*^zcsJkvxy': 70, ... + """ + from PIL import ImageFont + + # List of groups in size order, smallest to largest + char_width_groups = [ + "lij|' ", + '![]fI.,:;/\\t', + '`-(){}r"', + '*^zcsJkvxy', + 'aebdhnopqug#$L+<>=?_~FZT' + digits, + 'BSPEAKVXY&UwNRCHD', + 'QGOMm%W@', + ] + + def get_largest_in_group(group): + """Get the widest character from the group.""" + return max([ImageFont.truetype(font_path, font_size).getsize(c)[0] for c in group]) + + largest = char_width_groups[-1] + font_width = get_largest_in_group(largest) + return {group: int((get_largest_in_group(group) / font_width) * 100) + for group in char_width_groups} + + def parse_args(): """Parse the command line arguments.""" import argparse @@ -477,5 +628,6 @@ def main(): else: print(badge.badge_svg_text) + if __name__ == '__main__': main() diff --git a/setup.py b/setup.py index a179ce0..33c4b05 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( author_email='jongracecox@gmail.com', py_modules=['anybadge', 'anybadge_server'], setup_requires=['setuptools', 'wheel'], - tests_require=[], + tests_require=['unittest'], install_requires=[], data_files=[], options={ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_anybadge.py b/tests/test_anybadge.py new file mode 100644 index 0000000..cca3aa1 --- /dev/null +++ b/tests/test_anybadge.py @@ -0,0 +1,86 @@ +from unittest import TestCase +from anybadge import Badge + + +class TestAnybadge(TestCase): + """Test case class for anybadge package.""" + + def test_badge_width_with_long_value_text(self): + """Test the width of a badge generated with a long text value.""" + + badge = Badge(label='CppCheck', + value='err: 2 | warn: 9 | info: 99 | style: 365', + default_color='red') + + badge.write_badge('test_badge_1.svg', overwrite=True) + + self.assertLessEqual(badge.badge_width, 326) + + def test_badge_width_with_long_value_text_zero_padding(self): + """Test the width of a badge generated with a long text value.""" + + badge = Badge(label='CppCheck', + value='err: 2 | warn: 9 | info: 99 | style: 365', + default_color='red', + num_padding_chars=0) + + badge.write_badge('test_badge_2.svg', overwrite=True) + + self.assertLessEqual(badge.badge_width, 306) + + def test_badge_width_with_medium_value_text(self): + """Test the width of a badge generated with a medium text value.""" + + badge = Badge(label='medium', + value='89.67%', + default_color='green') + + badge.write_badge('test_badge_3.svg', overwrite=True) + + self.assertLessEqual(badge.badge_width, 138) + + def test_badge_width_with_medium_value_text_zero_pad(self): + """Test the width of a badge generated with a medium text value.""" + + badge = Badge(label='medium', + value='89.67%', + default_color='green', + num_padding_chars=0) + + badge.write_badge('test_badge_4.svg', overwrite=True) + + self.assertLessEqual(badge.badge_width, 118) + + def test_badge_width_with_short_value_text(self): + """Test the width of a badge generated with a short text value.""" + + badge = Badge(label='short', + value='1', + default_color='green') + + badge.write_badge('test_badge_5.svg', overwrite=True) + + self.assertLessEqual(badge.badge_width, 101) + + def test_badge_width_with_short_value_text_zero_pad(self): + """Test the width of a badge generated with a short text value.""" + + badge = Badge(label='short', + value='1', + default_color='green', + num_padding_chars=0) + + badge.write_badge('test_badge_6.svg', overwrite=True) + + self.assertLessEqual(badge.badge_width, 81) + + def test_badge_width_with_tiny_value_text(self): + """Test the width of a badge generated with a short text value.""" + + badge = Badge(label='a', + value='1', + default_color='green') + + badge.write_badge('test_badge_7.svg', overwrite=True) + + self.assertLessEqual(badge.badge_width, 76) From cc8dd5ac96309f92e17cb201ffbd4ba180d80350 Mon Sep 17 00:00:00 2001 From: Jon Grace-Cox Date: Sat, 1 Jun 2019 02:32:11 -0400 Subject: [PATCH 3/9] Add threshold and text color unittests --- tests/test_anybadge.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_anybadge.py b/tests/test_anybadge.py index cca3aa1..9c6ce89 100644 --- a/tests/test_anybadge.py +++ b/tests/test_anybadge.py @@ -84,3 +84,22 @@ class TestAnybadge(TestCase): badge.write_badge('test_badge_7.svg', overwrite=True) self.assertLessEqual(badge.badge_width, 76) + + def test_badge_with_thresholds(self): + """Test generating a badge using thresholds.""" + thresholds = { + 2: 'red', 4: 'orange', 6: 'green', 8: 'brightgreen' + } + + badge = Badge('test', '2.22', value_suffix='%', + thresholds=thresholds) + + badge.write_badge('test_badge_8.svg') + + def test_badge_with_text_color(self): + """Test generating a badge with alternate text_color.""" + + badge = Badge('test', '2.22', value_suffix='%', + text_color='#010101,#101010') + + badge.write_badge('test_badge_9.svg') From 1f2e0307352cb3b175b2b3a4d4bc31635ceaff72 Mon Sep 17 00:00:00 2001 From: Jon Grace-Cox Date: Sat, 1 Jun 2019 02:32:35 -0400 Subject: [PATCH 4/9] Add pytest and pytest-cov to build requirements --- build-requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build-requirements.txt b/build-requirements.txt index 1572f5f..9ad5476 100644 --- a/build-requirements.txt +++ b/build-requirements.txt @@ -2,3 +2,5 @@ mister-bump>=1.0.0 m2r restructuredtext_lint pygments +pytest +pytest-cov \ No newline at end of file From 13eca9ebfde7e2b1229dfd8d31912da976a26e15 Mon Sep 17 00:00:00 2001 From: Jon Grace-Cox Date: Sat, 1 Jun 2019 02:33:41 -0400 Subject: [PATCH 5/9] Run doctests and unittests as part of automated testing * Switch default version to Python 3.7. * Add 3.7 to the supported versions list. * Remove redundant test.py. This test module was basic and is being replaced by doctests and unittests. --- .travis.yml | 7 ++++--- test.py | 15 --------------- 2 files changed, 4 insertions(+), 18 deletions(-) delete mode 100755 test.py diff --git a/.travis.yml b/.travis.yml index fae333d..7cc100b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,11 @@ python: - '2.7' - '3.5' - '3.6' +- '3.7' install: - pip install -U setuptools pip -r build-requirements.txt script: -- python ./test.py +- pytest --doctest-modules --cov=anybadge anybadge.py tests deploy: - provider: pypi user: jongracecox @@ -16,7 +17,7 @@ deploy: on: tags: true all_branches: true - python: '3.6' + python: '3.7' - provider: pypi user: jongracecox password: @@ -24,4 +25,4 @@ deploy: distributions: sdist bdist_wheel on: branch: master - python: '3.6' + python: '3.7' diff --git a/test.py b/test.py deleted file mode 100755 index 2869a0d..0000000 --- a/test.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python -""" -anybadge test module. -""" - -import anybadge - -if __name__ == '__main__': - - thresholds={2: 'red', 4: 'orange', 6: 'green', 8: 'brightgreen'} - - badge = anybadge.Badge('test', '2.22', value_suffix='%', - thresholds=thresholds, text_color='#010101,#101010') - - print(badge.badge_svg_text) From 5405e1920c95dcbeb4c42dd87673de32b474e10c Mon Sep 17 00:00:00 2001 From: Jon Grace-Cox Date: Sat, 1 Jun 2019 03:07:14 -0400 Subject: [PATCH 6/9] Update README.md with latest changes --- README.md | 375 +++++++++++++++++++++++++++++------------------------- 1 file changed, 204 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index aa82d38..67cd9aa 100644 --- a/README.md +++ b/README.md @@ -208,185 +208,218 @@ Here is the output of ``help(anybadge)``:: Help on module anybadge: NAME - anybadge - anybadge - -FILE - /home/jon/Git/anybadge/anybadge.py + anybadge - anybadge DESCRIPTION - A Python module for generating badges for your projects, with a focus on - simplicity and flexibility. + A Python module for generating badges for your projects, with a focus on + simplicity and flexibility. CLASSES - __builtin__.object - Badge - - class Badge(__builtin__.object) - | Badge class used to generate badges. - | - | Examples: - | - | Create a simple green badge: - | - | >>> badge = Badge('label', 123, default_color='green') - | - | Write a badge to file, overwriting any existing file: - | - | >>> badge = Badge('label', 123, default_color='green') - | >>> badge.write_badge('demo.svg', overwrite=True) - | - | Here are a number of examples showing thresholds, since there - | are certain situations that may not be obvious: - | - | >>> badge = Badge('pipeline', 'passing', thresholds={'passing': 'green', 'failing': 'red'}) - | >>> badge.badge_color - | 'green' - | - | 2.32 is not <2 - | 2.32 is < 4, so 2.32 yields orange - | >>> badge = Badge('pylint', 2.32, thresholds={2: 'red', - | ... 4: 'orange', - | ... 8: 'yellow', - | ... 10: 'green'}) - | >>> badge.badge_color - | 'orange' - | - | 8 is not <8 - | 8 is <4, so 8 yields orange - | >>> badge = Badge('pylint', 6, thresholds={2: 'red', - | ... 4: 'orange', - | ... 8: 'yellow', - | ... 10: 'green'}) - | >>> badge.badge_color - | 'green' - | - | 10 is not <8, but use_max_when_value_exceeds defaults to - | True, so 10 yields green - | >>> badge = Badge('pylint', 11, thresholds={2: 'red', - | ... 4: 'orange', - | ... 8: 'yellow', - | ... 10: 'green'}) - | >>> badge.badge_color - | 'green' - | - | 11 is not <10, and use_max_when_value_exceeds is set to - | False, so 11 yields the default color '#a4a61d' - | >>> badge = Badge('pylint', 11, use_max_when_value_exceeds=False, - | ... thresholds={2: 'red', 4: 'orange', 8: 'yellow', - | ... 10: 'green'}) - | >>> badge.badge_color - | '#a4a61d' - | - | Methods defined here: - | - | __init__(self, label, value, font_name='DejaVu Sans,Verdana,Geneva,sans-serif', font_size=11, num_padding_chars=0.5, template='\n{{ value }}\n \n', value_prefix='', value_suffix='', thresholds=None, default_color='#a4a61d', use_max_when_value_exceeds=True, value_format=None, text_color='#fff') - | Constructor for Badge class. - | - | get_text_width(self, text) - | Return the width of text. - | - | This implementation assumes a fixed font of: - | - | font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11" - | >>> badge = Badge('x', 1, font_name='DejaVu Sans,Verdana,Geneva,sans-serif', font_size=11) - | >>> badge.get_text_width('pylint') - | 42 - | - | write_badge(self, file_path, overwrite=False) - | Write badge to file. - | - | ---------------------------------------------------------------------- - | Static methods defined here: - | - | get_font_width(font_name, font_size) - | Return the width multiplier for a font. - | - | >>> Badge.get_font_width('DejaVu Sans,Verdana,Geneva,sans-serif', 11) - | 7 - | - | ---------------------------------------------------------------------- - | Data descriptors defined here: - | - | __dict__ - | dictionary for instance variables (if defined) - | - | __weakref__ - | list of weak references to the object (if defined) - | - | badge_color - | Find the badge color based on the thresholds. - | - | badge_color_code - | Return the color code for the badge. - | - | badge_svg_text - | The badge SVG text. - | - | badge_width - | The total width of badge. - | - | >>> badge = Badge('pylint', '5', font_name='DejaVu Sans,Verdana,Geneva,sans-serif', - | ... font_size=11) - | >>> badge.badge_width - | 91 - | - | color_split_position - | The SVG x position where the color split should occur. - | - | font_width - | Return the badge font width. - | - | label_anchor - | The SVG x position of the middle anchor for the label text. - | - | label_anchor_shadow - | The SVG x position of the label shadow anchor. - | - | label_width - | The SVG width of the label text. - | - | value_anchor - | The SVG x position of the middle anchor for the value text. - | - | value_anchor_shadow - | The SVG x position of the value shadow anchor. - | - | value_is_float - | Identify whether the value text is a float. - | - | value_is_int - | Identify whether the value text is an int. - | - | value_type - | The Python type associated with the value. - | - | value_width - | The SVG width of the value text. + builtins.object + Badge + + class Badge(builtins.object) + | Badge(label, value, font_name='DejaVu Sans,Verdana,Geneva,sans-serif', font_size=11, num_padding_chars=0.5, template='\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n {{ label }}\n {{ label }}\n \n \n {{ value }}\n {{ value }}\n \n', value_prefix='', value_suffix='', thresholds=None, default_color='#4c1', use_max_when_value_exceeds=True, value_format=None, text_color='#fff') + | + | Badge class used to generate badges. + | + | Examples: + | + | Create a simple green badge: + | + | >>> badge = Badge('label', 123, default_color='green') + | + | Write a badge to file, overwriting any existing file: + | + | >>> badge = Badge('label', 123, default_color='green') + | >>> badge.write_badge('demo.svg', overwrite=True) + | + | Here are a number of examples showing thresholds, since there + | are certain situations that may not be obvious: + | + | >>> badge = Badge('pipeline', 'passing', thresholds={'passing': 'green', 'failing': 'red'}) + | >>> badge.badge_color + | 'green' + | + | 2.32 is not <2 + | 2.32 is < 4, so 2.32 yields orange + | >>> badge = Badge('pylint', 2.32, thresholds={2: 'red', + | ... 4: 'orange', + | ... 8: 'yellow', + | ... 10: 'green'}) + | >>> badge.badge_color + | 'orange' + | + | 8 is not <8 + | 8 is <4, so 8 yields orange + | >>> badge = Badge('pylint', 8, thresholds={2: 'red', + | ... 4: 'orange', + | ... 8: 'yellow', + | ... 10: 'green'}) + | >>> badge.badge_color + | 'green' + | + | 10 is not <8, but use_max_when_value_exceeds defaults to + | True, so 10 yields green + | >>> badge = Badge('pylint', 11, thresholds={2: 'red', + | ... 4: 'orange', + | ... 8: 'yellow', + | ... 10: 'green'}) + | >>> badge.badge_color + | 'green' + | + | 11 is not <10, and use_max_when_value_exceeds is set to + | False, so 11 yields the default color '#4c1' + | >>> badge = Badge('pylint', 11, use_max_when_value_exceeds=False, + | ... thresholds={2: 'red', 4: 'orange', 8: 'yellow', + | ... 10: 'green'}) + | >>> badge.badge_color + | '#4c1' + | + | Methods defined here: + | + | __init__(self, label, value, font_name='DejaVu Sans,Verdana,Geneva,sans-serif', font_size=11, num_padding_chars=0.5, template='\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n {{ label }}\n {{ label }}\n \n \n {{ value }}\n {{ value }}\n \n', value_prefix='', value_suffix='', thresholds=None, default_color='#4c1', use_max_when_value_exceeds=True, value_format=None, text_color='#fff') + | Constructor for Badge class. + | + | get_text_width(self, text) + | Return the width of text. + | + | Args: + | text(str): Text to get the pixel width of. + | + | Returns: + | int: Pixel width of the given text based on the badges selected font. + | + | This implementation assumes a fixed font of: + | + | font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11" + | >>> badge = Badge('x', 1, font_name='DejaVu Sans,Verdana,Geneva,sans-serif', font_size=11) + | >>> badge.get_text_width('pylint') + | 34 + | + | write_badge(self, file_path, overwrite=False) + | Write badge to file. + | + | ---------------------------------------------------------------------- + | Data descriptors defined here: + | + | __dict__ + | dictionary for instance variables (if defined) + | + | __weakref__ + | list of weak references to the object (if defined) + | + | badge_color + | Badge color based on the configured thresholds. + | + | Returns: str + | + | badge_color_code + | Return the color code for the badge. + | + | Returns: str + | + | badge_svg_text + | The badge SVG text. + | + | Returns: str + | + | badge_width + | The total width of badge. + | + | Returns: int + | + | Examples: + | + | >>> badge = Badge('pylint', '5') + | >>> badge.badge_width + | 103 + | + | color_split_position + | The SVG x position where the color split should occur. + | + | Returns: int + | + | font_width + | Return the width multiplier for a font. + | + | Returns: + | int: Maximum pixel width of badges selected font. + | + | Example: + | + | >>> Badge(label='x', value='1').font_width + | 10 + | + | label_anchor + | The SVG x position of the middle anchor for the label text. + | + | Returns: float + | + | label_anchor_shadow + | The SVG x position of the label shadow anchor. + | + | Returns: float + | + | label_width + | The SVG width of the label text. + | + | Returns: int + | + | value_anchor + | The SVG x position of the middle anchor for the value text. + | + | Returns: float + | + | value_anchor_shadow + | The SVG x position of the value shadow anchor. + | + | Returns: float + | + | value_is_float + | Identify whether the value text is a float. + | + | Returns: bool + | + | value_is_int + | Identify whether the value text is an int. + | + | Returns: bool + | + | value_type + | The Python type associated with the value. + | + | Returns: type + | + | value_width + | The SVG width of the value text. + | + | Returns: int FUNCTIONS - main() - Generate a badge based on command line arguments. - - parse_args() - Parse the command line arguments. + main() + Generate a badge based on command line arguments. + + parse_args() + Parse the command line arguments. DATA - BADGE_TEMPLATES = {'coverage': {'label': 'coverage', 'suffix': '%', 't... - COLORS = {'green': '#97CA00', 'lightgrey': '#9f9f9f', 'orange': '#fe7d... - DEFAULT_COLOR = '#a4a61d' - DEFAULT_FONT = 'DejaVu Sans,Verdana,Geneva,sans-serif' - DEFAULT_FONT_SIZE = 11 - DEFAULT_TEXT_COLOR = '#fff' - FONT_WIDTHS = {'DejaVu Sans,Verdana,Geneva,sans-serif': {11: 7}} - NUM_PADDING_CHARS = 0.5 - TEMPLATE_SVG = '\n Date: Sat, 1 Jun 2019 03:08:16 -0400 Subject: [PATCH 7/9] Use git tag for pypi package version * Stop using Mr Bump for version handling. * Only deploy to pypi on tags (not master branch). * Pick up version number from Travis CI TRAVIS_TAG environment variable. --- .travis.yml | 12 +++--------- build-requirements.txt | 1 - setup.py | 11 +++++++++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7cc100b..b7fc0a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ install: - pip install -U setuptools pip -r build-requirements.txt script: - pytest --doctest-modules --cov=anybadge anybadge.py tests +before_deploy: +- sed -i "s/^version = .*/version = __version__ = \"$TRAVIS_TAG\"/" anybadge.py deploy: - provider: pypi user: jongracecox @@ -17,12 +19,4 @@ deploy: on: tags: true all_branches: true - python: '3.7' -- provider: pypi - user: jongracecox - password: - secure: "TwKVv2wGesF3zHMtSzC/whgtBfHOG03wYS8bUOeeH4x8g5wQIu9SVyrYSffYE3FxapHbMCzmx7A3IgP4Ma6v4Ik6HqJY7a5DY/na0bcuI40IyCM2J0S0Hbq4E7WXaCCe6t7C5KE7NO3QzIcZboSZBWb78mcKLB2XbuIWMPCXqayGhqh8hynQmhwQ4C5b+jpBXlLtm6+AFH7eJOSdl8iO39RU5TL6FrOjgmks/KvTO0yHaXxmBoRcVudZsv9sAGsUx/UtRA65FPViIgu/dEV8cNtz7HOtL4v9x1mkvsiHF7OJiB1KCzSdUSCI83JSjdh44jjlNx0x2SnPbbwmCR7hi53xszMLbAHqY27hK1O4ntR1Iaui8HhBx8inxwS5z271xEQ9HZ8W9veRaxXxGkzj3LKGzjYflK1UN/ZK2syy5+cF4AEUpqEuQYi2waGhMxxWRo5KA05RLXetvsJYFEHUlATq9aXjMv29yH77KzySgpZe/Emd+Hb7r/7TKDeWXRaWDtUa+lc7G5oHLn6Kmm0iM1lerFKalEO9eoZwO2m1+O47CJ5yTKw1mNsC5Dj6EhG1QeonJWPS5z32vjNvnzRm2psbInt00SE2ClPxY5ASQOdcCMkq4ELGUHVssOGlniVYILs+tiwzrDTaXcckf8lvVvx6CiXfmFY+JYK9q3HYEuE=" - distributions: sdist bdist_wheel - on: - branch: master - python: '3.7' + python: '3.7' \ No newline at end of file diff --git a/build-requirements.txt b/build-requirements.txt index 9ad5476..046b26f 100644 --- a/build-requirements.txt +++ b/build-requirements.txt @@ -1,4 +1,3 @@ -mister-bump>=1.0.0 m2r restructuredtext_lint pygments diff --git a/setup.py b/setup.py index 33c4b05..c250076 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/python +import os +import re from setuptools import setup -from mister_bump import bump from m2r import parse_from_file import restructuredtext_lint @@ -16,11 +17,17 @@ if errors: raise ValueError('README.md contains errors: ', ', '.join([e.message for e in errors])) +# Attempt to get version number from TravisCI environment variable +version = os.environ.get('TRAVIS_TAG', default='0.0.0') + +# Remove leading 'v' +version = re.sub('^v', '', version) + setup( name='anybadge', description='Simple, flexible badge generator for project badges.', long_description=rst_readme, - version=bump(), + version=version, author='Jon Grace-Cox', author_email='jongracecox@gmail.com', py_modules=['anybadge', 'anybadge_server'], From 9f58cf07e4cf1f5129a1be2995027fbf558c163d Mon Sep 17 00:00:00 2001 From: Jon Grace-Cox Date: Sat, 1 Jun 2019 03:16:08 -0400 Subject: [PATCH 8/9] Switch to xenial distribution for TravisCI This is required in order to use Python3. https://docs.travis-ci.com/user/languages/python/#python-37-and-higher --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index b7fc0a1..32bbb14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: xenial language: python python: - '2.7' From 71fb2551f61f661388e61a33f2d79db621950b03 Mon Sep 17 00:00:00 2001 From: Jon Grace-Cox Date: Sat, 1 Jun 2019 03:24:07 -0400 Subject: [PATCH 9/9] Comment out _get_character_percentage_dict function --- anybadge.py | 80 ++++++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/anybadge.py b/anybadge.py index 39e4bd8..e817aeb 100755 --- a/anybadge.py +++ b/anybadge.py @@ -468,44 +468,48 @@ def _get_approx_string_width(text, font_width, fixed_width=False): return int(size) - -def _get_character_percentage_dict(font_path, font_size): - """Get the dictionary used to estimate variable width font text lengths. - - Args: - font_path(str): Path to valid font file. - font_size(int): Font size to use. - - Returns: dict - - This function can be used to calculate the dictionary used in the - ``get_approx_string_width`` function. - - Examples: - >>> _get_character_percentage_dict('/Library/Fonts/Verdana.ttf', 9) # doctest: +ELLIPSIS - {"lij|' ": 40, '![]fI.,:;/\\\\t': 50, '`-(){}r"': 60, '*^zcsJkvxy': 70, ... - """ - from PIL import ImageFont - - # List of groups in size order, smallest to largest - char_width_groups = [ - "lij|' ", - '![]fI.,:;/\\t', - '`-(){}r"', - '*^zcsJkvxy', - 'aebdhnopqug#$L+<>=?_~FZT' + digits, - 'BSPEAKVXY&UwNRCHD', - 'QGOMm%W@', - ] - - def get_largest_in_group(group): - """Get the widest character from the group.""" - return max([ImageFont.truetype(font_path, font_size).getsize(c)[0] for c in group]) - - largest = char_width_groups[-1] - font_width = get_largest_in_group(largest) - return {group: int((get_largest_in_group(group) / font_width) * 100) - for group in char_width_groups} +# This is a helper function that can be used to generate alternate dictionaries +# for the _get_approx_string_width function. The function is not needed for +# normal operation of this package, and since it depends on the PIL package, +# which is not included in the dependencies the function will remain commented out. +# +# def _get_character_percentage_dict(font_path, font_size): +# """Get the dictionary used to estimate variable width font text lengths. +# +# Args: +# font_path(str): Path to valid font file. +# font_size(int): Font size to use. +# +# Returns: dict +# +# This function can be used to calculate the dictionary used in the +# ``get_approx_string_width`` function. +# +# Examples: +# >>> _get_character_percentage_dict('/Library/Fonts/Verdana.ttf', 9) # doctest: +ELLIPSIS +# {"lij|' ": 40, '![]fI.,:;/\\\\t': 50, '`-(){}r"': 60, '*^zcsJkvxy': 70, ... +# """ +# from PIL import ImageFont +# +# # List of groups in size order, smallest to largest +# char_width_groups = [ +# "lij|' ", +# '![]fI.,:;/\\t', +# '`-(){}r"', +# '*^zcsJkvxy', +# 'aebdhnopqug#$L+<>=?_~FZT' + digits, +# 'BSPEAKVXY&UwNRCHD', +# 'QGOMm%W@', +# ] +# +# def get_largest_in_group(group): +# """Get the widest character from the group.""" +# return max([ImageFont.truetype(font_path, font_size).getsize(c)[0] for c in group]) +# +# largest = char_width_groups[-1] +# font_width = get_largest_in_group(largest) +# return {group: int((get_largest_in_group(group) / font_width) * 100) +# for group in char_width_groups} def parse_args():