aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py
diff options
context:
space:
mode:
authorshadchin <shadchin@yandex-team.ru>2022-02-10 16:44:39 +0300
committerDaniil Cherednik <dcherednik@yandex-team.ru>2022-02-10 16:44:39 +0300
commite9656aae26e0358d5378e5b63dcac5c8dbe0e4d0 (patch)
tree64175d5cadab313b3e7039ebaa06c5bc3295e274 /contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py
parent2598ef1d0aee359b4b6d5fdd1758916d5907d04f (diff)
downloadydb-e9656aae26e0358d5378e5b63dcac5c8dbe0e4d0.tar.gz
Restoring authorship annotation for <shadchin@yandex-team.ru>. Commit 2 of 2.
Diffstat (limited to 'contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py')
-rw-r--r--contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py808
1 files changed, 404 insertions, 404 deletions
diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py
index 8b1eb78745..1474360fd2 100644
--- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py
+++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py
@@ -1,404 +1,404 @@
-"""
-Tool for creating styles from a dictionary.
-"""
-import itertools
-import re
-import sys
-from enum import Enum
-from typing import Dict, Hashable, List, Set, Tuple, TypeVar
-
-from prompt_toolkit.cache import SimpleCache
-
-from .base import (
- ANSI_COLOR_NAMES,
- ANSI_COLOR_NAMES_ALIASES,
- DEFAULT_ATTRS,
- Attrs,
- BaseStyle,
-)
-from .named_colors import NAMED_COLORS
-
-__all__ = [
- "Style",
- "parse_color",
- "Priority",
- "merge_styles",
-]
-
-_named_colors_lowercase = {k.lower(): v.lstrip("#") for k, v in NAMED_COLORS.items()}
-
-
-def parse_color(text: str) -> str:
- """
- Parse/validate color format.
-
- Like in Pygments, but also support the ANSI color names.
- (These will map to the colors of the 16 color palette.)
- """
- # ANSI color names.
- if text in ANSI_COLOR_NAMES:
- return text
- if text in ANSI_COLOR_NAMES_ALIASES:
- return ANSI_COLOR_NAMES_ALIASES[text]
-
- # 140 named colors.
- try:
- # Replace by 'hex' value.
- return _named_colors_lowercase[text.lower()]
- except KeyError:
- pass
-
- # Hex codes.
- if text[0:1] == "#":
- col = text[1:]
-
- # Keep this for backwards-compatibility (Pygments does it).
- # I don't like the '#' prefix for named colors.
- if col in ANSI_COLOR_NAMES:
- return col
- elif col in ANSI_COLOR_NAMES_ALIASES:
- return ANSI_COLOR_NAMES_ALIASES[col]
-
- # 6 digit hex color.
- elif len(col) == 6:
- return col
-
- # 3 digit hex color.
- elif len(col) == 3:
- return col[0] * 2 + col[1] * 2 + col[2] * 2
-
- # Default.
- elif text in ("", "default"):
- return text
-
- raise ValueError("Wrong color format %r" % text)
-
-
-# Attributes, when they are not filled in by a style. None means that we take
-# the value from the parent.
-_EMPTY_ATTRS = Attrs(
- color=None,
- bgcolor=None,
- bold=None,
- underline=None,
- strike=None,
- italic=None,
- blink=None,
- reverse=None,
- hidden=None,
-)
-
-
-def _expand_classname(classname: str) -> List[str]:
- """
- Split a single class name at the `.` operator, and build a list of classes.
-
- E.g. 'a.b.c' becomes ['a', 'a.b', 'a.b.c']
- """
- result = []
- parts = classname.split(".")
-
- for i in range(1, len(parts) + 1):
- result.append(".".join(parts[:i]).lower())
-
- return result
-
-
-def _parse_style_str(style_str: str) -> Attrs:
- """
- Take a style string, e.g. 'bg:red #88ff00 class:title'
- and return a `Attrs` instance.
- """
- # Start from default Attrs.
- if "noinherit" in style_str:
- attrs = DEFAULT_ATTRS
- else:
- attrs = _EMPTY_ATTRS
-
- # Now update with the given attributes.
- for part in style_str.split():
- if part == "noinherit":
- pass
- elif part == "bold":
- attrs = attrs._replace(bold=True)
- elif part == "nobold":
- attrs = attrs._replace(bold=False)
- elif part == "italic":
- attrs = attrs._replace(italic=True)
- elif part == "noitalic":
- attrs = attrs._replace(italic=False)
- elif part == "underline":
- attrs = attrs._replace(underline=True)
- elif part == "nounderline":
- attrs = attrs._replace(underline=False)
- elif part == "strike":
- attrs = attrs._replace(strike=True)
- elif part == "nostrike":
- attrs = attrs._replace(strike=False)
-
- # prompt_toolkit extensions. Not in Pygments.
- elif part == "blink":
- attrs = attrs._replace(blink=True)
- elif part == "noblink":
- attrs = attrs._replace(blink=False)
- elif part == "reverse":
- attrs = attrs._replace(reverse=True)
- elif part == "noreverse":
- attrs = attrs._replace(reverse=False)
- elif part == "hidden":
- attrs = attrs._replace(hidden=True)
- elif part == "nohidden":
- attrs = attrs._replace(hidden=False)
-
- # Pygments properties that we ignore.
- elif part in ("roman", "sans", "mono"):
- pass
- elif part.startswith("border:"):
- pass
-
- # Ignore pieces in between square brackets. This is internal stuff.
- # Like '[transparent]' or '[set-cursor-position]'.
- elif part.startswith("[") and part.endswith("]"):
- pass
-
- # Colors.
- elif part.startswith("bg:"):
- attrs = attrs._replace(bgcolor=parse_color(part[3:]))
- elif part.startswith("fg:"): # The 'fg:' prefix is optional.
- attrs = attrs._replace(color=parse_color(part[3:]))
- else:
- attrs = attrs._replace(color=parse_color(part))
-
- return attrs
-
-
-CLASS_NAMES_RE = re.compile(r"^[a-z0-9.\s_-]*$") # This one can't contain a comma!
-
-
-class Priority(Enum):
- """
- The priority of the rules, when a style is created from a dictionary.
-
- In a `Style`, rules that are defined later will always override previous
- defined rules, however in a dictionary, the key order was arbitrary before
- Python 3.6. This means that the style could change at random between rules.
-
- We have two options:
-
- - `DICT_KEY_ORDER`: This means, iterate through the dictionary, and take
- the key/value pairs in order as they come. This is a good option if you
- have Python >3.6. Rules at the end will override rules at the beginning.
- - `MOST_PRECISE`: keys that are defined with most precision will get higher
- priority. (More precise means: more elements.)
- """
-
- DICT_KEY_ORDER = "KEY_ORDER"
- MOST_PRECISE = "MOST_PRECISE"
-
-
-# In the latest python verions, we take the dictionary ordering like it is,
-# In older versions, we sort by by precision. If you need to write code that
-# runs on all Python versions, it's best to sort them manually, with the most
-# precise rules at the bottom.
-if sys.version_info >= (3, 6):
- default_priority = Priority.DICT_KEY_ORDER
-else:
- default_priority = Priority.MOST_PRECISE
-
-
-class Style(BaseStyle):
- """
- Create a ``Style`` instance from a list of style rules.
-
- The `style_rules` is supposed to be a list of ('classnames', 'style') tuples.
- The classnames are a whitespace separated string of class names and the
- style string is just like a Pygments style definition, but with a few
- additions: it supports 'reverse' and 'blink'.
-
- Later rules always override previous rules.
-
- Usage::
-
- Style([
- ('title', '#ff0000 bold underline'),
- ('something-else', 'reverse'),
- ('class1 class2', 'reverse'),
- ])
-
- The ``from_dict`` classmethod is similar, but takes a dictionary as input.
- """
-
- def __init__(self, style_rules: List[Tuple[str, str]]) -> None:
- class_names_and_attrs = []
-
- # Loop through the rules in the order they were defined.
- # Rules that are defined later get priority.
- for class_names, style_str in style_rules:
- assert CLASS_NAMES_RE.match(class_names), repr(class_names)
-
- # The order of the class names doesn't matter.
- # (But the order of rules does matter.)
- class_names_set = frozenset(class_names.lower().split())
- attrs = _parse_style_str(style_str)
-
- class_names_and_attrs.append((class_names_set, attrs))
-
- self._style_rules = style_rules
- self.class_names_and_attrs = class_names_and_attrs
-
- @property
- def style_rules(self) -> List[Tuple[str, str]]:
- return self._style_rules
-
- @classmethod
- def from_dict(
- cls, style_dict: Dict[str, str], priority: Priority = default_priority
- ) -> "Style":
- """
- :param style_dict: Style dictionary.
- :param priority: `Priority` value.
- """
- if priority == Priority.MOST_PRECISE:
-
- def key(item: Tuple[str, str]) -> int:
- # Split on '.' and whitespace. Count elements.
- return sum(len(i.split(".")) for i in item[0].split())
-
- return cls(sorted(style_dict.items(), key=key))
- else:
- return cls(list(style_dict.items()))
-
- def get_attrs_for_style_str(
- self, style_str: str, default: Attrs = DEFAULT_ATTRS
- ) -> Attrs:
- """
- Get `Attrs` for the given style string.
- """
- list_of_attrs = [default]
- class_names: Set[str] = set()
-
- # Apply default styling.
- for names, attr in self.class_names_and_attrs:
- if not names:
- list_of_attrs.append(attr)
-
- # Go from left to right through the style string. Things on the right
- # take precedence.
- for part in style_str.split():
- # This part represents a class.
- # Do lookup of this class name in the style definition, as well
- # as all class combinations that we have so far.
- if part.startswith("class:"):
- # Expand all class names (comma separated list).
- new_class_names = []
- for p in part[6:].lower().split(","):
- new_class_names.extend(_expand_classname(p))
-
- for new_name in new_class_names:
- # Build a set of all possible class combinations to be applied.
- combos = set()
- combos.add(frozenset([new_name]))
-
- for count in range(1, len(class_names) + 1):
- for c2 in itertools.combinations(class_names, count):
- combos.add(frozenset(c2 + (new_name,)))
-
- # Apply the styles that match these class names.
- for names, attr in self.class_names_and_attrs:
- if names in combos:
- list_of_attrs.append(attr)
-
- class_names.add(new_name)
-
- # Process inline style.
- else:
- inline_attrs = _parse_style_str(part)
- list_of_attrs.append(inline_attrs)
-
- return _merge_attrs(list_of_attrs)
-
- def invalidation_hash(self) -> Hashable:
- return id(self.class_names_and_attrs)
-
-
-_T = TypeVar("_T")
-
-
-def _merge_attrs(list_of_attrs: List[Attrs]) -> Attrs:
- """
- Take a list of :class:`.Attrs` instances and merge them into one.
- Every `Attr` in the list can override the styling of the previous one. So,
- the last one has highest priority.
- """
-
- def _or(*values: _T) -> _T:
- "Take first not-None value, starting at the end."
- for v in values[::-1]:
- if v is not None:
- return v
- raise ValueError # Should not happen, there's always one non-null value.
-
- return Attrs(
- color=_or("", *[a.color for a in list_of_attrs]),
- bgcolor=_or("", *[a.bgcolor for a in list_of_attrs]),
- bold=_or(False, *[a.bold for a in list_of_attrs]),
- underline=_or(False, *[a.underline for a in list_of_attrs]),
- strike=_or(False, *[a.strike for a in list_of_attrs]),
- italic=_or(False, *[a.italic for a in list_of_attrs]),
- blink=_or(False, *[a.blink for a in list_of_attrs]),
- reverse=_or(False, *[a.reverse for a in list_of_attrs]),
- hidden=_or(False, *[a.hidden for a in list_of_attrs]),
- )
-
-
-def merge_styles(styles: List[BaseStyle]) -> "_MergedStyle":
- """
- Merge multiple `Style` objects.
- """
- styles = [s for s in styles if s is not None]
- return _MergedStyle(styles)
-
-
-class _MergedStyle(BaseStyle):
- """
- Merge multiple `Style` objects into one.
- This is supposed to ensure consistency: if any of the given styles changes,
- then this style will be updated.
- """
-
- # NOTE: previously, we used an algorithm where we did not generate the
- # combined style. Instead this was a proxy that called one style
- # after the other, passing the outcome of the previous style as the
- # default for the next one. This did not work, because that way, the
- # priorities like described in the `Style` class don't work.
- # 'class:aborted' was for instance never displayed in gray, because
- # the next style specified a default color for any text. (The
- # explicit styling of class:aborted should have taken priority,
- # because it was more precise.)
- def __init__(self, styles: List[BaseStyle]) -> None:
- self.styles = styles
- self._style: SimpleCache[Hashable, Style] = SimpleCache(maxsize=1)
-
- @property
- def _merged_style(self) -> Style:
- "The `Style` object that has the other styles merged together."
-
- def get() -> Style:
- return Style(self.style_rules)
-
- return self._style.get(self.invalidation_hash(), get)
-
- @property
- def style_rules(self) -> List[Tuple[str, str]]:
- style_rules = []
- for s in self.styles:
- style_rules.extend(s.style_rules)
- return style_rules
-
- def get_attrs_for_style_str(
- self, style_str: str, default: Attrs = DEFAULT_ATTRS
- ) -> Attrs:
- return self._merged_style.get_attrs_for_style_str(style_str, default)
-
- def invalidation_hash(self) -> Hashable:
- return tuple(s.invalidation_hash() for s in self.styles)
+"""
+Tool for creating styles from a dictionary.
+"""
+import itertools
+import re
+import sys
+from enum import Enum
+from typing import Dict, Hashable, List, Set, Tuple, TypeVar
+
+from prompt_toolkit.cache import SimpleCache
+
+from .base import (
+ ANSI_COLOR_NAMES,
+ ANSI_COLOR_NAMES_ALIASES,
+ DEFAULT_ATTRS,
+ Attrs,
+ BaseStyle,
+)
+from .named_colors import NAMED_COLORS
+
+__all__ = [
+ "Style",
+ "parse_color",
+ "Priority",
+ "merge_styles",
+]
+
+_named_colors_lowercase = {k.lower(): v.lstrip("#") for k, v in NAMED_COLORS.items()}
+
+
+def parse_color(text: str) -> str:
+ """
+ Parse/validate color format.
+
+ Like in Pygments, but also support the ANSI color names.
+ (These will map to the colors of the 16 color palette.)
+ """
+ # ANSI color names.
+ if text in ANSI_COLOR_NAMES:
+ return text
+ if text in ANSI_COLOR_NAMES_ALIASES:
+ return ANSI_COLOR_NAMES_ALIASES[text]
+
+ # 140 named colors.
+ try:
+ # Replace by 'hex' value.
+ return _named_colors_lowercase[text.lower()]
+ except KeyError:
+ pass
+
+ # Hex codes.
+ if text[0:1] == "#":
+ col = text[1:]
+
+ # Keep this for backwards-compatibility (Pygments does it).
+ # I don't like the '#' prefix for named colors.
+ if col in ANSI_COLOR_NAMES:
+ return col
+ elif col in ANSI_COLOR_NAMES_ALIASES:
+ return ANSI_COLOR_NAMES_ALIASES[col]
+
+ # 6 digit hex color.
+ elif len(col) == 6:
+ return col
+
+ # 3 digit hex color.
+ elif len(col) == 3:
+ return col[0] * 2 + col[1] * 2 + col[2] * 2
+
+ # Default.
+ elif text in ("", "default"):
+ return text
+
+ raise ValueError("Wrong color format %r" % text)
+
+
+# Attributes, when they are not filled in by a style. None means that we take
+# the value from the parent.
+_EMPTY_ATTRS = Attrs(
+ color=None,
+ bgcolor=None,
+ bold=None,
+ underline=None,
+ strike=None,
+ italic=None,
+ blink=None,
+ reverse=None,
+ hidden=None,
+)
+
+
+def _expand_classname(classname: str) -> List[str]:
+ """
+ Split a single class name at the `.` operator, and build a list of classes.
+
+ E.g. 'a.b.c' becomes ['a', 'a.b', 'a.b.c']
+ """
+ result = []
+ parts = classname.split(".")
+
+ for i in range(1, len(parts) + 1):
+ result.append(".".join(parts[:i]).lower())
+
+ return result
+
+
+def _parse_style_str(style_str: str) -> Attrs:
+ """
+ Take a style string, e.g. 'bg:red #88ff00 class:title'
+ and return a `Attrs` instance.
+ """
+ # Start from default Attrs.
+ if "noinherit" in style_str:
+ attrs = DEFAULT_ATTRS
+ else:
+ attrs = _EMPTY_ATTRS
+
+ # Now update with the given attributes.
+ for part in style_str.split():
+ if part == "noinherit":
+ pass
+ elif part == "bold":
+ attrs = attrs._replace(bold=True)
+ elif part == "nobold":
+ attrs = attrs._replace(bold=False)
+ elif part == "italic":
+ attrs = attrs._replace(italic=True)
+ elif part == "noitalic":
+ attrs = attrs._replace(italic=False)
+ elif part == "underline":
+ attrs = attrs._replace(underline=True)
+ elif part == "nounderline":
+ attrs = attrs._replace(underline=False)
+ elif part == "strike":
+ attrs = attrs._replace(strike=True)
+ elif part == "nostrike":
+ attrs = attrs._replace(strike=False)
+
+ # prompt_toolkit extensions. Not in Pygments.
+ elif part == "blink":
+ attrs = attrs._replace(blink=True)
+ elif part == "noblink":
+ attrs = attrs._replace(blink=False)
+ elif part == "reverse":
+ attrs = attrs._replace(reverse=True)
+ elif part == "noreverse":
+ attrs = attrs._replace(reverse=False)
+ elif part == "hidden":
+ attrs = attrs._replace(hidden=True)
+ elif part == "nohidden":
+ attrs = attrs._replace(hidden=False)
+
+ # Pygments properties that we ignore.
+ elif part in ("roman", "sans", "mono"):
+ pass
+ elif part.startswith("border:"):
+ pass
+
+ # Ignore pieces in between square brackets. This is internal stuff.
+ # Like '[transparent]' or '[set-cursor-position]'.
+ elif part.startswith("[") and part.endswith("]"):
+ pass
+
+ # Colors.
+ elif part.startswith("bg:"):
+ attrs = attrs._replace(bgcolor=parse_color(part[3:]))
+ elif part.startswith("fg:"): # The 'fg:' prefix is optional.
+ attrs = attrs._replace(color=parse_color(part[3:]))
+ else:
+ attrs = attrs._replace(color=parse_color(part))
+
+ return attrs
+
+
+CLASS_NAMES_RE = re.compile(r"^[a-z0-9.\s_-]*$") # This one can't contain a comma!
+
+
+class Priority(Enum):
+ """
+ The priority of the rules, when a style is created from a dictionary.
+
+ In a `Style`, rules that are defined later will always override previous
+ defined rules, however in a dictionary, the key order was arbitrary before
+ Python 3.6. This means that the style could change at random between rules.
+
+ We have two options:
+
+ - `DICT_KEY_ORDER`: This means, iterate through the dictionary, and take
+ the key/value pairs in order as they come. This is a good option if you
+ have Python >3.6. Rules at the end will override rules at the beginning.
+ - `MOST_PRECISE`: keys that are defined with most precision will get higher
+ priority. (More precise means: more elements.)
+ """
+
+ DICT_KEY_ORDER = "KEY_ORDER"
+ MOST_PRECISE = "MOST_PRECISE"
+
+
+# In the latest python verions, we take the dictionary ordering like it is,
+# In older versions, we sort by by precision. If you need to write code that
+# runs on all Python versions, it's best to sort them manually, with the most
+# precise rules at the bottom.
+if sys.version_info >= (3, 6):
+ default_priority = Priority.DICT_KEY_ORDER
+else:
+ default_priority = Priority.MOST_PRECISE
+
+
+class Style(BaseStyle):
+ """
+ Create a ``Style`` instance from a list of style rules.
+
+ The `style_rules` is supposed to be a list of ('classnames', 'style') tuples.
+ The classnames are a whitespace separated string of class names and the
+ style string is just like a Pygments style definition, but with a few
+ additions: it supports 'reverse' and 'blink'.
+
+ Later rules always override previous rules.
+
+ Usage::
+
+ Style([
+ ('title', '#ff0000 bold underline'),
+ ('something-else', 'reverse'),
+ ('class1 class2', 'reverse'),
+ ])
+
+ The ``from_dict`` classmethod is similar, but takes a dictionary as input.
+ """
+
+ def __init__(self, style_rules: List[Tuple[str, str]]) -> None:
+ class_names_and_attrs = []
+
+ # Loop through the rules in the order they were defined.
+ # Rules that are defined later get priority.
+ for class_names, style_str in style_rules:
+ assert CLASS_NAMES_RE.match(class_names), repr(class_names)
+
+ # The order of the class names doesn't matter.
+ # (But the order of rules does matter.)
+ class_names_set = frozenset(class_names.lower().split())
+ attrs = _parse_style_str(style_str)
+
+ class_names_and_attrs.append((class_names_set, attrs))
+
+ self._style_rules = style_rules
+ self.class_names_and_attrs = class_names_and_attrs
+
+ @property
+ def style_rules(self) -> List[Tuple[str, str]]:
+ return self._style_rules
+
+ @classmethod
+ def from_dict(
+ cls, style_dict: Dict[str, str], priority: Priority = default_priority
+ ) -> "Style":
+ """
+ :param style_dict: Style dictionary.
+ :param priority: `Priority` value.
+ """
+ if priority == Priority.MOST_PRECISE:
+
+ def key(item: Tuple[str, str]) -> int:
+ # Split on '.' and whitespace. Count elements.
+ return sum(len(i.split(".")) for i in item[0].split())
+
+ return cls(sorted(style_dict.items(), key=key))
+ else:
+ return cls(list(style_dict.items()))
+
+ def get_attrs_for_style_str(
+ self, style_str: str, default: Attrs = DEFAULT_ATTRS
+ ) -> Attrs:
+ """
+ Get `Attrs` for the given style string.
+ """
+ list_of_attrs = [default]
+ class_names: Set[str] = set()
+
+ # Apply default styling.
+ for names, attr in self.class_names_and_attrs:
+ if not names:
+ list_of_attrs.append(attr)
+
+ # Go from left to right through the style string. Things on the right
+ # take precedence.
+ for part in style_str.split():
+ # This part represents a class.
+ # Do lookup of this class name in the style definition, as well
+ # as all class combinations that we have so far.
+ if part.startswith("class:"):
+ # Expand all class names (comma separated list).
+ new_class_names = []
+ for p in part[6:].lower().split(","):
+ new_class_names.extend(_expand_classname(p))
+
+ for new_name in new_class_names:
+ # Build a set of all possible class combinations to be applied.
+ combos = set()
+ combos.add(frozenset([new_name]))
+
+ for count in range(1, len(class_names) + 1):
+ for c2 in itertools.combinations(class_names, count):
+ combos.add(frozenset(c2 + (new_name,)))
+
+ # Apply the styles that match these class names.
+ for names, attr in self.class_names_and_attrs:
+ if names in combos:
+ list_of_attrs.append(attr)
+
+ class_names.add(new_name)
+
+ # Process inline style.
+ else:
+ inline_attrs = _parse_style_str(part)
+ list_of_attrs.append(inline_attrs)
+
+ return _merge_attrs(list_of_attrs)
+
+ def invalidation_hash(self) -> Hashable:
+ return id(self.class_names_and_attrs)
+
+
+_T = TypeVar("_T")
+
+
+def _merge_attrs(list_of_attrs: List[Attrs]) -> Attrs:
+ """
+ Take a list of :class:`.Attrs` instances and merge them into one.
+ Every `Attr` in the list can override the styling of the previous one. So,
+ the last one has highest priority.
+ """
+
+ def _or(*values: _T) -> _T:
+ "Take first not-None value, starting at the end."
+ for v in values[::-1]:
+ if v is not None:
+ return v
+ raise ValueError # Should not happen, there's always one non-null value.
+
+ return Attrs(
+ color=_or("", *[a.color for a in list_of_attrs]),
+ bgcolor=_or("", *[a.bgcolor for a in list_of_attrs]),
+ bold=_or(False, *[a.bold for a in list_of_attrs]),
+ underline=_or(False, *[a.underline for a in list_of_attrs]),
+ strike=_or(False, *[a.strike for a in list_of_attrs]),
+ italic=_or(False, *[a.italic for a in list_of_attrs]),
+ blink=_or(False, *[a.blink for a in list_of_attrs]),
+ reverse=_or(False, *[a.reverse for a in list_of_attrs]),
+ hidden=_or(False, *[a.hidden for a in list_of_attrs]),
+ )
+
+
+def merge_styles(styles: List[BaseStyle]) -> "_MergedStyle":
+ """
+ Merge multiple `Style` objects.
+ """
+ styles = [s for s in styles if s is not None]
+ return _MergedStyle(styles)
+
+
+class _MergedStyle(BaseStyle):
+ """
+ Merge multiple `Style` objects into one.
+ This is supposed to ensure consistency: if any of the given styles changes,
+ then this style will be updated.
+ """
+
+ # NOTE: previously, we used an algorithm where we did not generate the
+ # combined style. Instead this was a proxy that called one style
+ # after the other, passing the outcome of the previous style as the
+ # default for the next one. This did not work, because that way, the
+ # priorities like described in the `Style` class don't work.
+ # 'class:aborted' was for instance never displayed in gray, because
+ # the next style specified a default color for any text. (The
+ # explicit styling of class:aborted should have taken priority,
+ # because it was more precise.)
+ def __init__(self, styles: List[BaseStyle]) -> None:
+ self.styles = styles
+ self._style: SimpleCache[Hashable, Style] = SimpleCache(maxsize=1)
+
+ @property
+ def _merged_style(self) -> Style:
+ "The `Style` object that has the other styles merged together."
+
+ def get() -> Style:
+ return Style(self.style_rules)
+
+ return self._style.get(self.invalidation_hash(), get)
+
+ @property
+ def style_rules(self) -> List[Tuple[str, str]]:
+ style_rules = []
+ for s in self.styles:
+ style_rules.extend(s.style_rules)
+ return style_rules
+
+ def get_attrs_for_style_str(
+ self, style_str: str, default: Attrs = DEFAULT_ATTRS
+ ) -> Attrs:
+ return self._merged_style.get_attrs_for_style_str(style_str, default)
+
+ def invalidation_hash(self) -> Hashable:
+ return tuple(s.invalidation_hash() for s in self.styles)