Skip to content

Commit f63ce4a

Browse files
committed
fix: issues with encoding in all the outputs
1 parent 0abd016 commit f63ce4a

File tree

9 files changed

+432
-197
lines changed

9 files changed

+432
-197
lines changed

safety/auth/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ def handle_timeout(self) -> None:
247247
headless = kwargs.get("headless", False)
248248
initial_state = kwargs.get("initial_state", None)
249249
ctx = kwargs.get("ctx", None)
250-
message = "Copy and paste this URL into your browser:\n⚠️ Ensure there are no extra spaces, especially at line breaks, as they may break the link."
250+
message = "Copy and paste this URL into your browser:\n:icon_warning: Ensure there are no extra spaces, especially at line breaks, as they may break the link."
251251

252252
if not headless:
253253
# Start a threaded HTTP server to handle the callback

safety/console.py

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,111 @@
1+
from functools import lru_cache
12
import logging
23
import os
4+
import sys
5+
import re
6+
from typing import TYPE_CHECKING, List, Match, Dict, Any, Optional, Union
7+
from rich import emoji
38
from rich.console import Console
49
from rich.theme import Theme
10+
from safety.emoji import load_emoji
11+
12+
13+
if TYPE_CHECKING:
14+
from rich.console import HighlighterType, JustifyMethod, OverflowMethod
15+
from rich.style import Style
16+
from rich.text import Text
17+
518

619
LOG = logging.getLogger(__name__)
720

21+
22+
@lru_cache()
23+
def should_use_ascii():
24+
"""
25+
Check if we should use ASCII alternatives for emojis
26+
"""
27+
encoding = getattr(sys.stdout, "encoding", "").lower()
28+
29+
if encoding in {"utf-8", "utf8", "cp65001", "utf-8-sig"}:
30+
return False
31+
32+
return True
33+
34+
35+
def get_spinner_animation() -> List[str]:
36+
"""
37+
Get the spinner animation based on the encoding
38+
"""
39+
if should_use_ascii():
40+
spinner = ["[ ]", "[= ]", "[== ]", "[=== ]", "[====]", "[ ===]", "[ ==]", "[ =]"]
41+
else:
42+
spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
43+
return spinner
44+
45+
46+
def replace_non_ascii_chars(text: str):
47+
"""
48+
Replace non-ascii characters with ascii alternatives
49+
"""
50+
CHARS_MAP = {
51+
"━": "-",
52+
"’": "'",
53+
}
54+
55+
for char, replacement in CHARS_MAP.items():
56+
text = text.replace(char, replacement)
57+
58+
try:
59+
text.encode("ascii")
60+
except UnicodeEncodeError:
61+
LOG.warning("No handled non-ascii characters detected, encoding with replace")
62+
text = text.encode("ascii", "replace").decode("ascii")
63+
64+
return text
65+
66+
67+
class SafeConsole(Console):
68+
"""
69+
Console subclass that handles emoji encoding issues by detecting
70+
problematic encoding environments and replacing emojis with ASCII alternatives.
71+
Uses string replacement for custom emoji namespace to avoid private API usage.
72+
"""
73+
74+
def render_str(
75+
self,
76+
text: str,
77+
*,
78+
style: Union[str, "Style"] = "",
79+
justify: Optional["JustifyMethod"] = None,
80+
overflow: Optional["OverflowMethod"] = None,
81+
emoji: Optional[bool] = None,
82+
markup: Optional[bool] = None,
83+
highlight: Optional[bool] = None,
84+
highlighter: Optional["HighlighterType"] = None,
85+
) -> "Text":
86+
"""
87+
Override render_str to pre-process our custom emojis before Rich handles the text.
88+
"""
89+
90+
use_ascii = should_use_ascii()
91+
text = load_emoji(text, use_ascii=use_ascii)
92+
93+
if use_ascii:
94+
text = replace_non_ascii_chars(text)
95+
96+
# Let Rich handle everything else normally
97+
return super().render_str(
98+
text,
99+
style=style,
100+
justify=justify,
101+
overflow=overflow,
102+
emoji=emoji,
103+
markup=markup,
104+
highlight=highlight,
105+
highlighter=highlighter,
106+
)
107+
108+
8109
SAFETY_THEME = {
9110
"file_title": "bold default on default",
10111
"dep_name": "bold yellow on default",
@@ -23,13 +124,19 @@
23124
"vulns_found_number": "red on default",
24125
}
25126

26-
non_interactive = os.getenv('NON_INTERACTIVE') == '1'
27127

28-
console_kwargs = {"theme": Theme(SAFETY_THEME, inherit=False)}
128+
non_interactive = os.getenv("NON_INTERACTIVE") == "1"
29129

30-
if non_interactive:
31-
LOG.info("NON_INTERACTIVE environment variable is set, forcing non-interactive mode")
32-
console_kwargs.update({"force_terminal": True, "force_interactive": False})
130+
console_kwargs: Dict[str, Any] = {
131+
"theme": Theme(SAFETY_THEME, inherit=False),
132+
"emoji": not should_use_ascii()
133+
}
33134

135+
if non_interactive:
136+
LOG.info(
137+
"NON_INTERACTIVE environment variable is set, forcing non-interactive mode"
138+
)
139+
console_kwargs["force_terminal"] = True
140+
console_kwargs["force_interactive"] = False
34141

35-
main_console = Console(**console_kwargs)
142+
main_console = SafeConsole(**console_kwargs)

0 commit comments

Comments
 (0)