From 6de2da8d585546c42b28c48e3a1c8896c31dec79 Mon Sep 17 00:00:00 2001 From: Matt Calvert Date: Tue, 22 Feb 2022 13:52:24 +0000 Subject: [PATCH] Add style support and add GitLab Scoped labels (#50) - Add new `--style` argument - Add new **GitLab Scoped** label style - Update documentation to reflect new argument --- README.md | 314 +++++++++++++++++++++++++++---------- anybadge.py | 96 ++++++++++-- examples/gitlab_scoped.svg | 22 +++ 3 files changed, 336 insertions(+), 96 deletions(-) create mode 100644 examples/gitlab_scoped.svg diff --git a/README.md b/README.md index c91830c..76bb923 100644 --- a/README.md +++ b/README.md @@ -178,9 +178,9 @@ badges based on version numbering. Here are some examples: ```python badge = Badge( - label='Version', - value='3.0.0', - thresholds={'3.0.0': 'red', '3.2.0': 'orange', '999.0.0': 'green'}, + label='Version', + value='3.0.0', + thresholds={'3.0.0': 'red', '3.2.0': 'orange', '999.0.0': 'green'}, semver=True ) ``` @@ -192,7 +192,7 @@ In the above example the thresholds equate to the following: * If value is < 999.0.0 then badge will be green. Each threshold entry is used to define the upper bounds of the threshold. If you don't know the -upper bound for your version number threshold you will need to provide an extreme upper bound - +upper bound for your version number threshold you will need to provide an extreme upper bound - in this example it is `999.0.0`. ### Examples @@ -201,37 +201,66 @@ in this example it is `999.0.0`. ```bash anybadge --value=2.22 --file=pylint.svg pylint ``` -![pylint](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/pylint.svg) +![pylint](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/pylint.svg) #### Pylint using arguments ```bash anybadge -l pylint -v 2.22 -f pylint.svg 2=red 4=orange 8=yellow 10=green ``` -![pylint](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/pylint.svg) +![pylint](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/pylint.svg) #### Coverage using template ```bash anybadge --value=65 --file=coverage.svg coverage -``` -![pylint](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/coverage.svg) +``` +![coverage](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/coverage.svg) #### Pipeline, using labeled colors ```bash anybadge --label=pipeline --value=passing --file=pipeline.svg passing=green failing=red ``` -![pylint](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/pipeline.svg) +![pipeline](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/pipeline.svg) #### Badge with fixed color ```bash -anybadge --label=awesomeness --value="110%" --file=awesomeness.svg --color=#97CA00 +anybadge --label=awesomeness --value="110%" --file=awesomeness.svg --color='#97CA00' ``` -![pylint](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/awesomeness.svg) +![awesomeness](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/awesomeness.svg) + +#### GitLab Scoped style badge +```bash +anybadge --style=gitlab-scoped --label=Project --value=Archimedes --file=gitlab_scoped.svg --color='#c1115d' +``` +![gitlab_scoped](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/gitlab_scoped.svg) #### Thresholds based on semantic versions ```bash anybadge --label=Version --value=2.4.5 --file=version.svg 1.0.0=red 2.4.6=orange 2.9.1=yellow 999.0.0=green ``` +#### Importing in your own app +```python +from anybadge import Badge + +test1 = 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' +) + +test1.write_badge('test1.svg') +``` + ### Options These are the command line options: @@ -273,6 +302,10 @@ optional arguments: Font size. -t TEMPLATE, --template TEMPLATE Location of alternative template .svg file. + -s STYLE, --style STYLE + Alternative style of badge to create. Valid values are + "gitlab-scoped", "default". This overrides any templates + passed using --template. -u, --use-max Use the maximum threshold color when the value exceeds the maximum threshold. -f FILE, --file FILE Output file location. @@ -281,6 +314,8 @@ optional arguments: Text color. Single value affects both labeland value colors. A comma separated pair affects label and value text respectively. +``` + Examples -------- @@ -289,22 +324,29 @@ thresholds. Pylint:: +``` anybadge.py --value=2.22 --file=pylint.svg pylint anybadge.py --label=pylint --value=2.22 --file=pylint.svg 2=red 4=orange 8=yellow 10=green +``` Coverage:: +``` anybadge.py --value=65 --file=coverage.svg coverage anybadge.py --label=coverage --value=65 --suffix='%%' --file=coverage.svg 50=red 60=orange 80=yellow 100=green +``` CI Pipeline:: +``` anybadge.py --label=pipeline --value=passing --file=pipeline.svg passing=green failing=red +``` Python usage ============ Here is the output of ``help(anybadge)``:: +``` Help on module anybadge: NAME @@ -317,30 +359,56 @@ DESCRIPTION CLASSES 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(label, value, font_name=None, font_size=None, num_padding_chars=None, num_label_padding_chars=None, num_value_padding_chars=None, template=None, style=None, value_prefix='', value_suffix='', thresholds=None, default_color=None, use_max_when_value_exceeds=True, value_format=None, text_color=None, semver=False) + | | Badge class used to generate badges. - | + | + | Args: + | label(str): Badge label text. + | value(str): Badge value text. + | font_name(str, optional): Name of font to use. + | font_size(int, optional): Font size. + | num_padding_chars(float, optional): Number of padding characters to use to give extra + | space around text. + | num_label_padding_chars(float, optional): Number of padding characters to use to give extra + | space around label text. + | num_value_padding_chars(float, optional): Number of padding characters to use to give extra + | space around value text. + | template(str, optional): String containing the SVG template. This should be valid SVG + | file content with place holders for variables to be populated during rendering. + | style(str, optional): Style of badge to create. This will make anybadge render a badge in a + | different style. Valid values are "gitlab-scoped", "default". Default is "default". + | value_prefix(str, optional): Prefix to be placed before value. + | value_suffix(str, optional): Suffix to be placed after value. + | thresholds(dict, optional): A dictionary containing thresholds used to select badge + | color based on the badge value. + | default_color(str, optional): Badge color as a name or as an HTML color code. + | use_max_when_value_exceeds(bool, optional): Choose whether to use the maximum threshold + | value when the badge value exceeds the top threshold. Default is True. + | value_format(str, optional) String with formatting to be used to format the value text. + | text_color(str, optional): Text color as a name or as an HTML color code. + | semver(bool, optional): Used to indicate that the value is a semantic version number. + | | 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', @@ -349,7 +417,7 @@ CLASSES | ... 10: 'green'}) | >>> badge.badge_color | 'orange' - | + | | 8 is not <8 | 8 is <4, so 8 yields orange | >>> badge = Badge('pylint', 8, thresholds={2: 'red', @@ -358,7 +426,7 @@ CLASSES | ... 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', @@ -367,7 +435,7 @@ CLASSES | ... 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, @@ -375,149 +443,229 @@ CLASSES | ... 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') + | + | __init__(self, label, value, font_name=None, font_size=None, num_padding_chars=None, num_label_padding_chars=None, num_value_padding_chars=None, template=None, style=None, value_prefix='', value_suffix='', thresholds=None, default_color=None, use_max_when_value_exceeds=True, value_format=None, text_color=None, semver=False) | Constructor for Badge class. - | + | + | __repr__(self) + | Return a representation of the Badge object instance. + | + | The output of the __repr__ function could be used to recreate the current object. + | + | Examples: + | + | >>> badge = Badge('example', '123.456') + | >>> repr(badge) + | "Badge('example', '123.456')" + | + | >>> badge = Badge('example', '123.456', value_suffix='TB') + | >>> repr(badge) + | "Badge('example', '123.456', value_suffix='TB')" + | + | >>> badge = Badge('example', '123.456', text_color='#111111', value_suffix='TB') + | >>> repr(badge) + | "Badge('example', '123.456', value_suffix='TB', text_color='#111111')" + | + | >>> badge = Badge('example', '123', num_padding_chars=5) + | >>> repr(badge) + | "Badge('example', '123', num_padding_chars=5)" + | + | >>> badge = Badge('example', '123', num_label_padding_chars=5) + | >>> repr(badge) + | "Badge('example', '123', num_label_padding_chars=5)" + | + | >>> badge = Badge('example', '123', num_label_padding_chars=5, num_value_padding_chars=6, + | ... template='template.svg', value_prefix='$', thresholds={10: 'green', 30: 'red'}, + | ... default_color='red', use_max_when_value_exceeds=False, value_format="%s m/s") + | >>> repr(badge) + | "Badge('example', '123', num_label_padding_chars=5, num_value_padding_chars=6, template='template.svg', value_prefix='$', thresholds={10: 'green', 30: 'red'}, default_color='red', use_max_when_value_exceeds=False, value_format='%s m/s')" + | + | __str__(self) + | Return string representation of badge. + | + | This will return the badge SVG text. + | + | Returns: str + | + | Examples: + | + | >>> print(Badge('example', '123')) # doctest: +ELLIPSIS + | + | ... + | | 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) - | + | Readonly properties defined here: + | + | arc_start + | The position where the arc on the gitlab-scoped should start. + | + | Returns: int + | + | Examples: + | + | >>> badge = Badge('pylint', '5') + | >>> badge.arc_start + | 58 + | | badge_color | Badge color based on the configured thresholds. - | + | | Returns: str - | + | | badge_color_code | Return the color code for the badge. - | + | | Returns: str - | + | + | Raises: ValueError when an invalid badge color is set. + | | 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 - | + | 61 + | | color_split_position | The SVG x position where the color split should occur. - | + | | Returns: int - | + | + | float_thresholds + | Thresholds as a dict using floats as keys. + | | 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 - | + | + | semver_thresholds + | Thresholds as a dict using LooseVersion as keys. + | + | semver_version + | The semantic version represented by the value string. + | + | Returns: LooseVersion + | | 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_box_width + | The SVG width of the value text box. + | + | Returns: int + | | 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 + | + | ---------------------------------------------------------------------- + | Data descriptors defined here: + | + | __dict__ + | dictionary for instance variables (if defined) + | + | __weakref__ + | list of weak references to the object (if defined) FUNCTIONS - main() + main(args=None) Generate a badge based on command line arguments. - - parse_args() + + parse_args(args) Parse the command line arguments. DATA BADGE_TEMPLATES = {'coverage': {'label': 'coverage', 'suffix': '%', 't... - COLORS = {'green': '#4c1', 'lightgrey': '#9f9f9f', 'orange': '#fe7d37'... + COLORS = {'aqua': '#00FFFF', 'black': '#000000', 'blue': '#0000FF', 'b... DEFAULT_COLOR = '#4c1' 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: 10}} + FONT_WIDTHS = {'Arial, Helvetica, sans-serif': {11: 8}, 'DejaVu Sans,V... + MASK_ID_PREFIX = 'anybadge_' NUM_PADDING_CHARS = 0.5 + TEMPLATE_GITLAB_SCOPED_SVG = '\n... TEMPLATE_SVG = '\n """ +# Template SVG for GitLab Scoped label and value badges with placeholders for +# various items that will be added during final creation. +TEMPLATE_GITLAB_SCOPED_SVG = """ + + + + + + + + + + + + + + + + {{ label }} + + + {{ value }} + +""" + # Define some templates that can be used for common badge types, saving # from having to provide thresholds and labels each time. BADGE_TEMPLATES = { @@ -133,6 +158,8 @@ class Badge(object): space around value text. template(str, optional): String containing the SVG template. This should be valid SVG file content with place holders for variables to be populated during rendering. + style(str, optional): Style of badge to create. This will make anybadge render a badge in a + different style. Valid values are "gitlab-scoped", "default". Default is "default". value_prefix(str, optional): Prefix to be placed before value. value_suffix(str, optional): Suffix to be placed after value. thresholds(dict, optional): A dictionary containing thresholds used to select badge @@ -200,7 +227,7 @@ class Badge(object): def __init__(self, label, value, font_name=None, font_size=None, num_padding_chars=None, num_label_padding_chars=None, - num_value_padding_chars=None, template=None, + num_value_padding_chars=None, template=None, style=None, value_prefix='', value_suffix='', thresholds=None, default_color=None, use_max_when_value_exceeds=True, value_format=None, text_color=None, semver=False): @@ -222,6 +249,8 @@ class Badge(object): num_value_padding_chars = num_padding_chars if not template: template = TEMPLATE_SVG + if style not in ['gitlab-scoped']: + style = "default" if not default_color: default_color = DEFAULT_COLOR if not text_color: @@ -250,6 +279,7 @@ class Badge(object): self.num_label_padding_chars = num_label_padding_chars self.num_value_padding_chars = num_value_padding_chars self.template = template + self.style = style self.thresholds = thresholds self.default_color = default_color @@ -313,6 +343,8 @@ class Badge(object): optional_args += ", num_value_padding_chars=%s" % repr(self.num_value_padding_chars) if self.template != TEMPLATE_SVG: optional_args += ", template=%s" % repr(self.template) + if self.style != 'default': + optional_args += ", style=%s" % repr(self.style) if self.value_prefix != '': optional_args += ", value_prefix=%s" % repr(self.value_prefix) if self.value_suffix != '': @@ -337,13 +369,13 @@ class Badge(object): def _repr_svg_(self): """Return SVG representation when used inside Jupyter notebook cells. - + This will render the SVG immediately inside a notebook cell when creating a Badge instance without assigning it to an identifier. """ return self.badge_svg_text - - + + @classmethod def _get_next_mask_id(cls): """Return a new mask ID from a singleton sequence maintained on the class. @@ -357,6 +389,22 @@ class Badge(object): return MASK_ID_PREFIX + str(cls.mask_id) + def _get_svg_template(self): + """Return the correct SVG template to render, based on the style and template + that have been set + + Returns: str + """ + if self.style == "gitlab-scoped": + return TEMPLATE_GITLAB_SCOPED_SVG + + # Identify whether template is a file or the actual template text + if len(self.template.split('\n')) == 1: + with open(self.template, mode='r') as file_handle: + return file_handle.read() + else: + return self.template + @property def semver_version(self): """The semantic version represented by the value string. @@ -443,6 +491,14 @@ class Badge(object): """ return int(self.get_text_width(str(self.value_text)) + (2.0 * self.num_value_padding_chars * self.font_width)) + @property + def value_box_width(self): + """The SVG width of the value text box. + + Returns: int + """ + return self.value_width - 9 + @property def font_width(self): """Return the width multiplier for a font. @@ -511,6 +567,20 @@ class Badge(object): """ return self.label_width + self.value_width + @property + def arc_start(self): + """The position where the arc on the gitlab-scoped should start. + + Returns: int + + Examples: + + >>> badge = Badge('pylint', '5') + >>> badge.arc_start + 51 + """ + return self.badge_width - 10 + @property def badge_svg_text(self): """The badge SVG text. @@ -518,12 +588,7 @@ class Badge(object): Returns: str """ - # Identify whether template is a file or the actual template text - if len(self.template.split('\n')) == 1: - with open(self.template, mode='r') as file_handle: - badge_text = file_handle.read() - else: - badge_text = self.template + badge_text = self._get_svg_template() return badge_text.replace('{{ badge width }}', str(self.badge_width)) \ .replace('{{ font name }}', self.font_name) \ @@ -538,8 +603,10 @@ class Badge(object): .replace('{{ label text color }}', self.label_text_color) \ .replace('{{ value text color }}', self.value_text_color) \ .replace('{{ color split x }}', str(self.color_split_position)) \ - .replace('{{ value width }}', str(self.value_width))\ - .replace('{{ mask id }}', self.mask_id) + .replace('{{ value width }}', str(self.value_width)) \ + .replace('{{ mask id }}', self.mask_id) \ + .replace('{{ value box width }}', str(self.value_box_width)) \ + .replace('{{ arc start }}', str(self.arc_start)) def __str__(self): """Return string representation of badge. @@ -824,6 +891,9 @@ examples: parser.add_argument('-t', '--template', type=str, help='Location of alternative ' 'template .svg file.', default=TEMPLATE_SVG) + parser.add_argument('-st', '--style', type=str, help='Alternative style of badge to create. Valid ' + 'values are "gitlab-scoped", "default". This ' + 'overrides any templates passed using --template.') parser.add_argument('-u', '--use-max', action='store_true', help='Use the maximum threshold color when the value exceeds the ' 'maximum threshold.') @@ -878,7 +948,7 @@ def main(args=None): badge = Badge(label, args.value, value_prefix=args.prefix, value_suffix=suffix, default_color=args.color, num_padding_chars=args.padding, num_label_padding_chars=args.label_padding, num_value_padding_chars=args.value_padding, - font_name=args.font, font_size=args.font_size, template=args.template, + font_name=args.font, font_size=args.font_size, template=args.template, style=args.style, use_max_when_value_exceeds=args.use_max, thresholds=threshold_dict, value_format=args.value_format, text_color=args.text_color, semver=args.semver) diff --git a/examples/gitlab_scoped.svg b/examples/gitlab_scoped.svg new file mode 100644 index 0000000..b4835ec --- /dev/null +++ b/examples/gitlab_scoped.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + Project + + + Archimedes + + \ No newline at end of file