Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,28 @@ assignees: jongracecox

---

**Describe the bug**
## Describe the bug
A clear and concise description of what the bug is.

**To Reproduce**
## To Reproduce
Provide the simplest example of code or commands to reproduce the behavior.

**Expected behavior**
## Expected behavior
A clear and concise description of what you expected to happen.

**Screenshots**
## Screenshots
If applicable, add screenshots to help explain your problem.

** Python version: (please complete the following information)**
## Python version: (please complete the following information)
- 2.7
- 3.6
- 3.7
- ...

** Operating system: (please complete the following information)**
## Operating system: (please complete the following information)
- Linux
- Windows
- Mac OS

**Additional context**
## Additional context
Add any other context about the problem here.
8 changes: 4 additions & 4 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ assignees: jongracecox

---

**Is your feature request related to a problem? Please describe.**
## Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
## Describe the solution you'd like
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
## Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
## Additional context
Add any other context or screenshots about the feature request here.
5 changes: 4 additions & 1 deletion .github/workflows/pr_check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch the entire history including tags

- name: Set up Python
uses: actions/setup-python@v4
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,19 @@ Each threshold entry is used to define the upper bounds of the threshold. If you
upper bound for your version number threshold you will need to provide an extreme upper bound -
in this example it is `999.0.0`.

### Escaping

Badges are generated as .svg files, which utilize an XML-based markup language. Consequently,
any HTML characters present in badge labels or values must be escaped to ensure proper
representation. If you need to disable escaping, the following options are available:

- Python API
- `escape_label=False`
- `escape_value=False`
- CLI
- `--no-escape-label`
- `--no-escape-value`

### Examples

#### Pylint using template
Expand Down
24 changes: 22 additions & 2 deletions anybadge/badge.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections import OrderedDict
from pathlib import Path
from typing import Dict, Type, Optional, Union
import html

from . import config
from .colors import Color
Expand Down Expand Up @@ -122,6 +123,8 @@ def __init__(
value_format: Optional[str] = None,
text_color: Optional[str] = None,
semver: Optional[bool] = False,
escape_label: Optional[bool] = True,
escape_value: Optional[bool] = True,
):
"""Constructor for Badge class."""
# Set defaults if values were not passed
Expand Down Expand Up @@ -209,6 +212,9 @@ def __init__(
self.use_max_when_value_exceeds = use_max_when_value_exceeds
self.mask_str = self.__class__._get_next_mask_str()

self.escape_label = escape_label
self.escape_value = escape_value

def __repr__(self) -> str:
"""Return a representation of the Badge object instance.

Expand Down Expand Up @@ -333,6 +339,20 @@ def _get_svg_template(self) -> str:
else:
return self.template

@property
def encoded_label(self) -> str:
if self.escape_label:
return html.escape(self.label)
else:
return self.label

@property
def encoded_value(self) -> str:
if self.escape_value:
return html.escape(self.value_text)
else:
return self.value_text

@property
def semver_version(self) -> Version:
"""The semantic version represented by the value string.
Expand Down Expand Up @@ -638,8 +658,8 @@ def badge_svg_text(self) -> str:
badge_text.replace("{{ badge width }}", str(self.badge_width))
.replace("{{ font name }}", self.font_name)
.replace("{{ font size }}", str(self.font_size))
.replace("{{ label }}", self.label)
.replace("{{ value }}", self.value_text)
.replace("{{ label }}", self.encoded_label)
.replace("{{ value }}", self.encoded_value)
.replace("{{ label anchor }}", str(self.label_anchor))
.replace("{{ label anchor shadow }}", str(self.label_anchor_shadow))
.replace("{{ value anchor }}", str(self.value_anchor))
Expand Down
38 changes: 32 additions & 6 deletions anybadge/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ def parse_args(args):
default=False,
help="Treat value and thresholds as semantic versions.",
)
parser.add_argument(
"--no-escape-label",
action="store_true",
default=False,
help="Do not escape the label text.",
)
parser.add_argument(
"--no-escape-value",
action="store_true",
default=False,
help="Do not escape the value text.",
)
parser.add_argument(
"args",
nargs=argparse.REMAINDER,
Expand All @@ -165,16 +177,20 @@ def parse_args(args):
return parser.parse_args(args)


def main(args=None):
"""Generate a badge based on command line arguments."""
def main(args=None) -> int:
"""Generate a badge based on command line arguments.

Returns:
int: 0 if successful, 1 otherwise.
"""

# Args may be sent from command line of as args directly.
if not args:
args = sys.argv[1:]

if args == ["--version"]:
print(anybadge_version)
return
return 0

# Parse command line arguments
args = parse_args(args)
Expand All @@ -195,8 +211,13 @@ def main(args=None):
suffix = style.suffix

# Create threshold list from args
threshold_list = [x.split("=") for x in threshold_text]
threshold_dict = {x[0]: x[1] for x in threshold_list}
try:
threshold_list = [x.split("=") for x in threshold_text]
threshold_dict = {x[0]: x[1] for x in threshold_list}
except Exception as e:
print(f"ERROR: Failed to parse threshold values: '{' '.join(threshold_text)}'")
print(f"ERROR: Thresholds should be in the form '<value>=color'")
return 1

# Create badge object
badge = Badge(
Expand All @@ -217,6 +238,8 @@ def main(args=None):
value_format=args.value_format,
text_color=args.text_color,
semver=args.semver,
escape_label=not args.no_escape_label,
escape_value=not args.no_escape_value,
)

if args.file:
Expand All @@ -225,6 +248,9 @@ def main(args=None):
else:
print(badge.badge_svg_text)

return 0


if __name__ == "__main__":
main()
retval = main()
sys.exit(retval)
44 changes: 39 additions & 5 deletions anybadge/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
import re


EMOJI_REGEX = re.compile(
"["
"\U0001F600-\U0001F64F" # emoticons
"\U0001F300-\U0001F5FF" # symbols & pictographs
"\U0001F680-\U0001F6FF" # transport & map symbols
"\U0001F1E0-\U0001F1FF" # flags (iOS)
"\U00002702-\U000027B0" # Dingbats
"\U000024C2-\U0001F251"
"]+",
flags=re.UNICODE,
)


def is_emoji(character):
"""Return True if character is an emoji.

Examples:

>>> is_emoji('👍')
True

>>> is_emoji('a')
False

"""
return bool(EMOJI_REGEX.match(character))


# Based on the following SO answer: https://stackoverflow.com/a/16008023/6252525
def _get_approx_string_width(text, font_width, fixed_width=False) -> int:
"""
Expand Down Expand Up @@ -52,11 +83,14 @@ def _get_approx_string_width(text, font_width, fixed_width=False) -> int:
}

for s in text:
percentage = 100.0
for k in char_width_percentages.keys():
if s in k:
percentage = char_width_percentages[k]
break
percentage = 50.0
if is_emoji(s):
percentage = 75.0
else:
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)
1 change: 1 addition & 0 deletions build-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pytest
pytest-cov
requests
setuptools
sh
tox
types-requests
wheel
4 changes: 2 additions & 2 deletions tasks/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
from invoke import task
from bs4 import BeautifulSoup

from anybadge.colors import Color


@task
def update(c):
"""Generate colors Enum from Mozilla color keywords."""
from anybadge.colors import Color

url = "https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color_keywords"
response = requests.get(url)

Expand Down
39 changes: 38 additions & 1 deletion tasks/test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import subprocess
from pathlib import Path
from time import sleep
import sys

from invoke import task

Expand All @@ -12,10 +13,46 @@
def local(c):
"""Run local tests."""
print("Running local tests...")

print("Ensuring pip is installed")
subprocess.run(
f"{sys.executable} -m ensurepip",
shell=True,
)

print("Ensuring anybagde command is not already installed")
result = subprocess.run(
"which anybadge",
shell=True,
)
if result.returncode == 0:
raise RuntimeError("anybadge command is already installed. Uninstall it first.")

print("Installing local package to current virtual environment")
subprocess.run(
f"{sys.executable} -m pip install .",
cwd=str(PROJECT_DIR),
shell=True,
)

retval = 0
try:
subprocess.run(
f"{sys.executable} -m pytest --doctest-modules "
"--cov=anybadge --cov-report term --cov-report html:htmlcov --cov-report xml:coverage.xml anybadge tests",
shell=True,
)
except Exception as e:
print(f"Error running tests: {e}")
retval = 1

print("Uninstalling local package from current virtual environment")
subprocess.run(
"pytest --doctest-modules --cov=anybadge --cov-report html:htmlcov anybadge tests",
f"{sys.executable} -m pip uninstall anybadge -y",
cwd=str(PROJECT_DIR),
shell=True,
)
sys.exit(retval)


def build_test_docker_image():
Expand Down
Loading
Loading