You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ORPA-pyOpenRPA/Resources/WPy64-3720/python-3.7.2.amd64/Lib/site-packages/prompt_toolkit/styles/style.py

373 lines
12 KiB

"""
Tool for creating styles from a dictionary.
"""
from __future__ import unicode_literals, absolute_import
import itertools
import re
import sys
from .base import BaseStyle, DEFAULT_ATTRS, ANSI_COLOR_NAMES, ANSI_COLOR_NAMES_ALIASES, Attrs
from .named_colors import NAMED_COLORS
from prompt_toolkit.cache import SimpleCache
__all__ = [
'Style',
'parse_color',
'Priority',
'merge_styles',
]
_named_colors_lowercase = dict(
(k.lower(), v.lstrip('#')) for k, v in NAMED_COLORS.items())
def parse_color(text):
"""
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,
italic=None, blink=None, reverse=None, hidden=None)
def _expand_classname(classname):
"""
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):
"""
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)
# 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:
"""
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'
_ALL = [DICT_KEY_ORDER, 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):
assert isinstance(style_rules, list)
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 = frozenset(class_names.lower().split())
attrs = _parse_style_str(style_str)
class_names_and_attrs.append((class_names, attrs))
self._style_rules = style_rules
self.class_names_and_attrs = class_names_and_attrs
@property
def style_rules(self):
return self._style_rules
@classmethod
def from_dict(cls, style_dict, priority=default_priority):
"""
:param style_dict: Style dictionary.
:param priority: `Priority` value.
"""
assert priority in Priority._ALL
if priority == Priority.MOST_PRECISE:
def key(item):
# 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, default=DEFAULT_ATTRS):
"""
Get `Attrs` for the given style string.
"""
list_of_attrs = [default]
class_names = 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):
return id(self.class_names_and_attrs)
def _merge_attrs(list_of_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):
" Take first not-None value, starting at the end. "
for v in values[::-1]:
if v is not None:
return v
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]),
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):
"""
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):
assert all(isinstance(style, BaseStyle) for style in styles)
self.styles = styles
self._style = SimpleCache(maxsize=1)
@property
def _merged_style(self):
" The `Style` object that has the other styles merged together. "
def get():
return Style(self.style_rules)
return self._style.get(self.invalidation_hash(), get)
@property
def style_rules(self):
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, default=DEFAULT_ATTRS):
return self._merged_style.get_attrs_for_style_str(style_str, default)
def invalidation_hash(self):
return tuple(s.invalidation_hash() for s in self.styles)