diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index ecfe3de10c829..2f5428f78edfc 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -50,6 +50,8 @@ jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.") from markupsafe import escape as escape_html # markupsafe is jinja2 dependency +_DOLLAR_PATTERN = re.compile(r"\$.*?\$") + BaseFormatter = Union[str, Callable] ExtFormatter = Union[BaseFormatter, dict[Any, Optional[BaseFormatter]]] CSSPair = tuple[str, Union[str, float]] @@ -71,7 +73,11 @@ class StylerRenderer: Base class to process rendering a Styler with a specified jinja2 template. """ - loader = jinja2.PackageLoader("pandas", "io/formats/templates") + import os + + loader = jinja2.FileSystemLoader( + os.path.join(os.path.dirname(__file__), "templates") + ) env = jinja2.Environment(loader=loader, trim_blocks=True) template_html = env.get_template("html.tpl") template_html_table = env.get_template("html_table.tpl") @@ -834,10 +840,7 @@ def _generate_body_row( data_element = _element( "td", - ( - f"{self.css['data']} {self.css['row']}{r} " - f"{self.css['col']}{c}{cls}" - ), + (f"{self.css['data']} {self.css['row']}{r} {self.css['col']}{c}{cls}"), value, data_element_visible, attributes="", @@ -956,7 +959,7 @@ def concatenated_visible_rows(obj): idx_len = d["index_lengths"].get((lvl, r), None) if idx_len is not None: # i.e. not a sparsified entry d["clines"][rn + idx_len].append( - f"\\cline{{{lvln+1}-{len(visible_index_levels)+data_len}}}" + f"\\cline{{{lvln + 1}-{len(visible_index_levels) + data_len}}}" ) def format( @@ -1211,7 +1214,7 @@ def format( data = self.data.loc[subset] if not isinstance(formatter, dict): - formatter = {col: formatter for col in data.columns} + formatter = dict.fromkeys(data.columns, formatter) cis = self.columns.get_indexer_for(data.columns) ris = self.index.get_indexer_for(data.index) @@ -1397,7 +1400,7 @@ def format_index( return self # clear the formatter / revert to default and avoid looping if not isinstance(formatter, dict): - formatter = {level: formatter for level in levels_} + formatter = dict.fromkeys(levels_, formatter) else: formatter = { obj._get_level_number(level): formatter_ @@ -1540,7 +1543,7 @@ def relabel_index( >>> df = pd.DataFrame({"samples": np.random.rand(10)}) >>> styler = df.loc[np.random.randint(0, 10, 3)].style - >>> styler.relabel_index([f"sample{i+1} ({{}})" for i in range(3)]) + >>> styler.relabel_index([f"sample{i + 1} ({{}})" for i in range(3)]) ... # doctest: +SKIP samples sample1 (5) 0.315811 @@ -1694,7 +1697,7 @@ def format_index_names( return self # clear the formatter / revert to default and avoid looping if not isinstance(formatter, dict): - formatter = {level: formatter for level in levels_} + formatter = dict.fromkeys(levels_, formatter) else: formatter = { obj._get_level_number(level): formatter_ @@ -2503,7 +2506,7 @@ def color(value, user_arg, command, comm_arg): if value[0] == "#" and len(value) == 7: # color is hex code return command, f"[HTML]{{{value[1:].upper()}}}{arg}" if value[0] == "#" and len(value) == 4: # color is short hex code - val = f"{value[1].upper()*2}{value[2].upper()*2}{value[3].upper()*2}" + val = f"{value[1].upper() * 2}{value[2].upper() * 2}{value[3].upper() * 2}" return command, f"[HTML]{{{val}}}{arg}" elif value[:3] == "rgb": # color is rgb or rgba r = re.findall("(?<=\\()[0-9\\s%]+(?=,)", value)[0].strip() @@ -2579,7 +2582,7 @@ def _escape_latex(s: str) -> str: def _math_mode_with_dollar(s: str) -> str: - r""" + """ All characters in LaTeX math mode are preserved. The substrings in LaTeX math mode, which start with @@ -2597,17 +2600,18 @@ def _math_mode_with_dollar(s: str) -> str: Escaped string """ s = s.replace(r"\$", r"rt8§=§7wz") - pattern = re.compile(r"\$.*?\$") pos = 0 - ps = pattern.search(s, pos) res = [] - while ps: - res.append(_escape_latex(s[pos : ps.span()[0]])) - res.append(ps.group()) - pos = ps.span()[1] - ps = pattern.search(s, pos) - - res.append(_escape_latex(s[pos : len(s)])) + matches = list(_DOLLAR_PATTERN.finditer(s)) + # Pre-calculate slices to avoid repeated function calls + for m in matches: + start, end = m.span() + if pos < start: + res.append(_escape_latex(s[pos:start])) + res.append(m.group()) + pos = end + if pos < len(s): + res.append(_escape_latex(s[pos : len(s)])) return "".join(res).replace(r"rt8§=§7wz", r"\$")