Stav 23.06.2026
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
# pylint: disable=W0611
|
||||
'''
|
||||
Utils
|
||||
=====
|
||||
|
||||
The Utils module provides a selection of general utility functions and classes
|
||||
that may be useful for various applications. These include maths, color,
|
||||
algebraic and platform functions.
|
||||
|
||||
.. versionchanged:: 1.6.0
|
||||
The OrderedDict class has been removed. Use collections.OrderedDict
|
||||
instead.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('intersection', 'difference', 'strtotuple',
|
||||
'get_color_from_hex', 'get_hex_from_color', 'get_random_color',
|
||||
'is_color_transparent', 'hex_colormap', 'colormap', 'boundary',
|
||||
'deprecated', 'SafeList',
|
||||
'interpolate', 'QueryDict',
|
||||
'platform', 'escape_markup', 'reify', 'rgba', 'pi_version',
|
||||
'format_bytes_to_human')
|
||||
|
||||
from os import environ, path
|
||||
from sys import platform as _sys_platform
|
||||
from re import match, split, search, MULTILINE, IGNORECASE
|
||||
from kivy.compat import string_types
|
||||
|
||||
|
||||
def boundary(value, minvalue, maxvalue):
|
||||
'''Limit a value between a minvalue and maxvalue.'''
|
||||
return min(max(value, minvalue), maxvalue)
|
||||
|
||||
|
||||
def intersection(set1, set2):
|
||||
'''Return the intersection of 2 lists.'''
|
||||
return [s for s in set1 if s in set2]
|
||||
|
||||
|
||||
def difference(set1, set2):
|
||||
'''Return the difference between 2 lists.'''
|
||||
return [s for s in set1 if s not in set2]
|
||||
|
||||
|
||||
def strtotuple(s):
|
||||
'''Convert a tuple string into a tuple
|
||||
with some security checks. Designed to be used
|
||||
with the eval() function::
|
||||
|
||||
a = (12, 54, 68)
|
||||
b = str(a) # return '(12, 54, 68)'
|
||||
c = strtotuple(b) # return (12, 54, 68)
|
||||
|
||||
'''
|
||||
# security
|
||||
if not match(r'^[,.0-9 ()\[\]]*$', s):
|
||||
raise Exception('Invalid characters in string for tuple conversion')
|
||||
# fast syntax check
|
||||
if s.count('(') != s.count(')'):
|
||||
raise Exception('Invalid count of ( and )')
|
||||
if s.count('[') != s.count(']'):
|
||||
raise Exception('Invalid count of [ and ]')
|
||||
r = eval(s)
|
||||
if type(r) not in (list, tuple):
|
||||
raise Exception('Conversion failed')
|
||||
return r
|
||||
|
||||
|
||||
def rgba(s, *args):
|
||||
'''Return a Kivy color (4 value from 0-1 range) from either a hex string or
|
||||
a list of 0-255 values.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
'''
|
||||
if isinstance(s, string_types):
|
||||
return get_color_from_hex(s)
|
||||
if isinstance(s, (list, tuple)):
|
||||
s = [x / 255. for x in s]
|
||||
if len(s) == 3:
|
||||
s.append(1)
|
||||
return s
|
||||
if isinstance(s, (int, float)):
|
||||
s = [s / 255.]
|
||||
s.extend(x / 255. for x in args)
|
||||
if len(s) == 3:
|
||||
s.append(1)
|
||||
return s
|
||||
raise Exception('Invalid value (not a string / list / tuple)')
|
||||
|
||||
|
||||
def get_color_from_hex(s):
|
||||
'''Transform a hex string color to a kivy
|
||||
:class:`~kivy.graphics.Color`.
|
||||
'''
|
||||
if s.startswith('#'):
|
||||
return get_color_from_hex(s[1:])
|
||||
|
||||
value = [int(x, 16) / 255.
|
||||
for x in split('([0-9a-f]{2})', s.lower()) if x != '']
|
||||
if len(value) == 3:
|
||||
value.append(1.0)
|
||||
return value
|
||||
|
||||
|
||||
def get_hex_from_color(color):
|
||||
'''Transform a kivy :class:`~kivy.graphics.Color` to a hex value::
|
||||
|
||||
>>> get_hex_from_color((0, 1, 0))
|
||||
'#00ff00'
|
||||
>>> get_hex_from_color((.25, .77, .90, .5))
|
||||
'#3fc4e57f'
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
'''
|
||||
return '#' + ''.join(['{0:02x}'.format(int(x * 255)) for x in color])
|
||||
|
||||
|
||||
def get_random_color(alpha=1.0):
|
||||
'''Returns a random color (4 tuple).
|
||||
|
||||
:Parameters:
|
||||
`alpha`: float, defaults to 1.0
|
||||
If alpha == 'random', a random alpha value is generated.
|
||||
'''
|
||||
from random import random
|
||||
if alpha == 'random':
|
||||
return [random(), random(), random(), random()]
|
||||
else:
|
||||
return [random(), random(), random(), alpha]
|
||||
|
||||
|
||||
def is_color_transparent(c):
|
||||
'''Return True if the alpha channel is 0.'''
|
||||
if len(c) < 4:
|
||||
return False
|
||||
if float(c[3]) == 0.:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
hex_colormap = {
|
||||
'aliceblue': '#f0f8ff',
|
||||
'antiquewhite': '#faebd7',
|
||||
'aqua': '#00ffff',
|
||||
'aquamarine': '#7fffd4',
|
||||
'azure': '#f0ffff',
|
||||
'beige': '#f5f5dc',
|
||||
'bisque': '#ffe4c4',
|
||||
'black': '#000000',
|
||||
'blanchedalmond': '#ffebcd',
|
||||
'blue': '#0000ff',
|
||||
'blueviolet': '#8a2be2',
|
||||
'brown': '#a52a2a',
|
||||
'burlywood': '#deb887',
|
||||
'cadetblue': '#5f9ea0',
|
||||
'chartreuse': '#7fff00',
|
||||
'chocolate': '#d2691e',
|
||||
'coral': '#ff7f50',
|
||||
'cornflowerblue': '#6495ed',
|
||||
'cornsilk': '#fff8dc',
|
||||
'crimson': '#dc143c',
|
||||
'cyan': '#00ffff',
|
||||
'darkblue': '#00008b',
|
||||
'darkcyan': '#008b8b',
|
||||
'darkgoldenrod': '#b8860b',
|
||||
'darkgray': '#a9a9a9',
|
||||
'darkgrey': '#a9a9a9',
|
||||
'darkgreen': '#006400',
|
||||
'darkkhaki': '#bdb76b',
|
||||
'darkmagenta': '#8b008b',
|
||||
'darkolivegreen': '#556b2f',
|
||||
'darkorange': '#ff8c00',
|
||||
'darkorchid': '#9932cc',
|
||||
'darkred': '#8b0000',
|
||||
'darksalmon': '#e9967a',
|
||||
'darkseagreen': '#8fbc8f',
|
||||
'darkslateblue': '#483d8b',
|
||||
'darkslategray': '#2f4f4f',
|
||||
'darkslategrey': '#2f4f4f',
|
||||
'darkturquoise': '#00ced1',
|
||||
'darkviolet': '#9400d3',
|
||||
'deeppink': '#ff1493',
|
||||
'deepskyblue': '#00bfff',
|
||||
'dimgray': '#696969',
|
||||
'dimgrey': '#696969',
|
||||
'dodgerblue': '#1e90ff',
|
||||
'firebrick': '#b22222',
|
||||
'floralwhite': '#fffaf0',
|
||||
'forestgreen': '#228b22',
|
||||
'fuchsia': '#ff00ff',
|
||||
'gainsboro': '#dcdcdc',
|
||||
'ghostwhite': '#f8f8ff',
|
||||
'gold': '#ffd700',
|
||||
'goldenrod': '#daa520',
|
||||
'gray': '#808080',
|
||||
'grey': '#808080',
|
||||
'green': '#008000',
|
||||
'greenyellow': '#adff2f',
|
||||
'honeydew': '#f0fff0',
|
||||
'hotpink': '#ff69b4',
|
||||
'indianred': '#cd5c5c',
|
||||
'indigo': '#4b0082',
|
||||
'ivory': '#fffff0',
|
||||
'khaki': '#f0e68c',
|
||||
'lavender': '#e6e6fa',
|
||||
'lavenderblush': '#fff0f5',
|
||||
'lawngreen': '#7cfc00',
|
||||
'lemonchiffon': '#fffacd',
|
||||
'lightblue': '#add8e6',
|
||||
'lightcoral': '#f08080',
|
||||
'lightcyan': '#e0ffff',
|
||||
'lightgoldenrodyellow': '#fafad2',
|
||||
'lightgreen': '#90ee90',
|
||||
'lightgray': '#d3d3d3',
|
||||
'lightgrey': '#d3d3d3',
|
||||
'lightpink': '#ffb6c1',
|
||||
'lightsalmon': '#ffa07a',
|
||||
'lightseagreen': '#20b2aa',
|
||||
'lightskyblue': '#87cefa',
|
||||
'lightslategray': '#778899',
|
||||
'lightslategrey': '#778899',
|
||||
'lightsteelblue': '#b0c4de',
|
||||
'lightyellow': '#ffffe0',
|
||||
'lime': '#00ff00',
|
||||
'limegreen': '#32cd32',
|
||||
'linen': '#faf0e6',
|
||||
'magenta': '#ff00ff',
|
||||
'maroon': '#800000',
|
||||
'mediumaquamarine': '#66cdaa',
|
||||
'mediumblue': '#0000cd',
|
||||
'mediumorchid': '#ba55d3',
|
||||
'mediumpurple': '#9370db',
|
||||
'mediumseagreen': '#3cb371',
|
||||
'mediumslateblue': '#7b68ee',
|
||||
'mediumspringgreen': '#00fa9a',
|
||||
'mediumturquoise': '#48d1cc',
|
||||
'mediumvioletred': '#c71585',
|
||||
'midnightblue': '#191970',
|
||||
'mintcream': '#f5fffa',
|
||||
'mistyrose': '#ffe4e1',
|
||||
'moccasin': '#ffe4b5',
|
||||
'navajowhite': '#ffdead',
|
||||
'navy': '#000080',
|
||||
'oldlace': '#fdf5e6',
|
||||
'olive': '#808000',
|
||||
'olivedrab': '#6b8e23',
|
||||
'orange': '#ffa500',
|
||||
'orangered': '#ff4500',
|
||||
'orchid': '#da70d6',
|
||||
'palegoldenrod': '#eee8aa',
|
||||
'palegreen': '#98fb98',
|
||||
'paleturquoise': '#afeeee',
|
||||
'palevioletred': '#db7093',
|
||||
'papayawhip': '#ffefd5',
|
||||
'peachpuff': '#ffdab9',
|
||||
'peru': '#cd853f',
|
||||
'pink': '#ffc0cb',
|
||||
'plum': '#dda0dd',
|
||||
'powderblue': '#b0e0e6',
|
||||
'purple': '#800080',
|
||||
'red': '#ff0000',
|
||||
'rosybrown': '#bc8f8f',
|
||||
'royalblue': '#4169e1',
|
||||
'saddlebrown': '#8b4513',
|
||||
'salmon': '#fa8072',
|
||||
'sandybrown': '#f4a460',
|
||||
'seagreen': '#2e8b57',
|
||||
'seashell': '#fff5ee',
|
||||
'sienna': '#a0522d',
|
||||
'silver': '#c0c0c0',
|
||||
'skyblue': '#87ceeb',
|
||||
'slateblue': '#6a5acd',
|
||||
'slategray': '#708090',
|
||||
'slategrey': '#708090',
|
||||
'snow': '#fffafa',
|
||||
'springgreen': '#00ff7f',
|
||||
'steelblue': '#4682b4',
|
||||
'tan': '#d2b48c',
|
||||
'teal': '#008080',
|
||||
'thistle': '#d8bfd8',
|
||||
'tomato': '#ff6347',
|
||||
'turquoise': '#40e0d0',
|
||||
'violet': '#ee82ee',
|
||||
'wheat': '#f5deb3',
|
||||
'white': '#ffffff',
|
||||
'whitesmoke': '#f5f5f5',
|
||||
'yellow': '#ffff00',
|
||||
'yellowgreen': '#9acd32',
|
||||
}
|
||||
|
||||
colormap = {k: get_color_from_hex(v) for k, v in hex_colormap.items()}
|
||||
|
||||
DEPRECATED_CALLERS = []
|
||||
|
||||
|
||||
def deprecated(func=None, msg=''):
|
||||
'''This is a decorator which can be used to mark functions
|
||||
as deprecated. It will result in a warning being emitted the first time
|
||||
the function is used.'''
|
||||
|
||||
import inspect
|
||||
import functools
|
||||
|
||||
if func is None:
|
||||
return functools.partial(deprecated, msg=msg)
|
||||
|
||||
@functools.wraps(func)
|
||||
def new_func(*args, **kwargs):
|
||||
file, line, caller = inspect.stack()[1][1:4]
|
||||
caller_id = "%s:%s:%s" % (file, line, caller)
|
||||
# We want to print deprecated warnings only once:
|
||||
if caller_id not in DEPRECATED_CALLERS:
|
||||
DEPRECATED_CALLERS.append(caller_id)
|
||||
warning = (
|
||||
'Call to deprecated function %s in %s line %d.'
|
||||
'Called from %s line %d'
|
||||
' by %s().' % (
|
||||
func.__name__,
|
||||
func.__code__.co_filename,
|
||||
func.__code__.co_firstlineno + 1,
|
||||
file, line, caller))
|
||||
|
||||
if msg:
|
||||
warning = '{}: {}'.format(msg, warning)
|
||||
warning = 'Deprecated: ' + warning
|
||||
|
||||
from kivy.logger import Logger
|
||||
Logger.warning(warning)
|
||||
if func.__doc__:
|
||||
Logger.warning(func.__doc__)
|
||||
return func(*args, **kwargs)
|
||||
return new_func
|
||||
|
||||
|
||||
@deprecated
|
||||
def interpolate(value_from, value_to, step=10):
|
||||
'''Interpolate between two values, by providing the
|
||||
reciprocal of the proportion between two points.
|
||||
|
||||
.. deprecated:: 2.3.0
|
||||
For animations, consider using the
|
||||
`AnimationTransition.linear()` for a similar purpose.
|
||||
|
||||
.. warning::
|
||||
These interpolations work only on lists/tuples/doubles with the same
|
||||
dimensions. No test is done to check the dimensions are the same.
|
||||
'''
|
||||
if type(value_from) in (list, tuple):
|
||||
out = []
|
||||
for x, y in zip(value_from, value_to):
|
||||
out.append(interpolate(x, y, step))
|
||||
return out
|
||||
else:
|
||||
return value_from + (value_to - value_from) / float(step)
|
||||
|
||||
|
||||
class SafeList(list):
|
||||
'''List with a clear() method.
|
||||
|
||||
.. warning::
|
||||
Usage of the iterate() function will decrease your performance.
|
||||
'''
|
||||
|
||||
@deprecated
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clear(self):
|
||||
del self[:]
|
||||
|
||||
@deprecated
|
||||
def iterate(self, reverse=False):
|
||||
if reverse:
|
||||
return iter(reversed(self))
|
||||
return iter(self)
|
||||
|
||||
|
||||
class QueryDict(dict):
|
||||
'''QueryDict is a dict() that can be queried with dot.
|
||||
|
||||
::
|
||||
|
||||
d = QueryDict()
|
||||
# create a key named toto, with the value 1
|
||||
d.toto = 1
|
||||
# it's the same as
|
||||
d['toto'] = 1
|
||||
|
||||
.. versionadded:: 1.0.4
|
||||
'''
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
return self.__getitem__(attr)
|
||||
except KeyError:
|
||||
raise AttributeError("%r object has no attribute %r" % (
|
||||
self.__class__.__name__self, attr))
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
self.__setitem__(attr, value)
|
||||
|
||||
|
||||
def format_bytes_to_human(size, precision=2):
|
||||
'''Format a byte value to a human readable representation (B, KB, MB...).
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:Parameters:
|
||||
`size`: int
|
||||
Number that represents the bytes value
|
||||
`precision`: int, defaults to 2
|
||||
Precision after the comma
|
||||
|
||||
Examples::
|
||||
|
||||
>>> format_bytes_to_human(6463)
|
||||
'6.31 KB'
|
||||
>>> format_bytes_to_human(646368746541)
|
||||
'601.98 GB'
|
||||
|
||||
'''
|
||||
size = int(size)
|
||||
fmt = '%%1.%df %%s' % precision
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if size < 1024.0:
|
||||
return fmt % (size, unit)
|
||||
size /= 1024.0
|
||||
|
||||
|
||||
def _get_platform():
|
||||
# On Android sys.platform returns 'linux2', so prefer to check the
|
||||
# existence of environ variables set during Python initialization
|
||||
kivy_build = environ.get('KIVY_BUILD', '')
|
||||
if kivy_build in {'android', 'ios'}:
|
||||
return kivy_build
|
||||
elif 'P4A_BOOTSTRAP' in environ:
|
||||
return 'android'
|
||||
elif 'ANDROID_ARGUMENT' in environ:
|
||||
# We used to use this method to detect android platform,
|
||||
# leaving it here to be backwards compatible with `pydroid3`
|
||||
# and similar tools outside kivy's ecosystem
|
||||
return 'android'
|
||||
elif _sys_platform in ('win32', 'cygwin'):
|
||||
return 'win'
|
||||
elif _sys_platform == 'darwin':
|
||||
return 'macosx'
|
||||
elif _sys_platform.startswith('linux'):
|
||||
return 'linux'
|
||||
elif _sys_platform.startswith('freebsd'):
|
||||
return 'linux'
|
||||
return 'unknown'
|
||||
|
||||
|
||||
platform = _get_platform()
|
||||
'''
|
||||
A string identifying the current operating system. It is one
|
||||
of: `'win'`, `'linux'`, `'android'`, `'macosx'`, `'ios'` or `'unknown'`.
|
||||
You can use it as follows::
|
||||
|
||||
from kivy.utils import platform
|
||||
if platform == 'linux':
|
||||
do_linux_things()
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
|
||||
platform is now a variable instead of a function.
|
||||
'''
|
||||
|
||||
|
||||
def escape_markup(text):
|
||||
'''
|
||||
Escape markup characters found in the text. Intended to be used when markup
|
||||
text is activated on the Label::
|
||||
|
||||
untrusted_text = escape_markup('Look at the example [1]')
|
||||
text = '[color=ff0000]' + untrusted_text + '[/color]'
|
||||
w = Label(text=text, markup=True)
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
'''
|
||||
return text.replace('&', '&').replace('[', '&bl;').replace(']', '&br;')
|
||||
|
||||
|
||||
class reify(object):
|
||||
'''
|
||||
Put the result of a method which uses this (non-data) descriptor decorator
|
||||
in the instance dict after the first call, effectively replacing the
|
||||
decorator with an instance variable.
|
||||
|
||||
It acts like @property, except that the function is only ever called once;
|
||||
after that, the value is cached as a regular attribute. This gives you lazy
|
||||
attribute creation on objects that are meant to be immutable.
|
||||
|
||||
Taken from the `Pyramid project <https://pypi.python.org/pypi/pyramid/>`_.
|
||||
|
||||
To use this as a decorator::
|
||||
|
||||
@reify
|
||||
def lazy(self):
|
||||
...
|
||||
return hard_to_compute_int
|
||||
first_time = self.lazy # lazy is reify obj, reify.__get__() runs
|
||||
second_time = self.lazy # lazy is hard_to_compute_int
|
||||
'''
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
self.__doc__ = func.__doc__
|
||||
|
||||
def __get__(self, inst, cls):
|
||||
if inst is None:
|
||||
return self
|
||||
retval = self.func(inst)
|
||||
setattr(inst, self.func.__name__, retval)
|
||||
return retval
|
||||
|
||||
|
||||
def _get_pi_version():
|
||||
"""Detect the version of the Raspberry Pi by reading the revision field value from '/proc/cpuinfo'
|
||||
See: https://www.raspberrypi.org/documentation/hardware/raspberrypi/revision-codes/README.md
|
||||
Based on: https://github.com/adafruit/Adafruit_Python_GPIO/blob/master/Adafruit_GPIO/Platform.py
|
||||
""" # noqa
|
||||
# Check if file exist
|
||||
if not path.isfile('/proc/cpuinfo'):
|
||||
return None
|
||||
|
||||
with open('/proc/cpuinfo', 'r') as f:
|
||||
cpuinfo = f.read()
|
||||
|
||||
# Match a line like 'Revision : a01041'
|
||||
revision = search(r'^Revision\s+:\s+(\w+)$', cpuinfo,
|
||||
flags=MULTILINE | IGNORECASE)
|
||||
if not revision:
|
||||
# Couldn't find the hardware revision, assume it is not a Pi
|
||||
return None
|
||||
|
||||
# Determine the Pi version using the processor bits using the new-style
|
||||
# revision format
|
||||
revision = int(revision.group(1), base=16)
|
||||
if revision & 0x800000:
|
||||
return ((revision & 0xF000) >> 12) + 1
|
||||
|
||||
# If it is not using the new style revision format,
|
||||
# then it must be a Raspberry Pi 1
|
||||
return 1
|
||||
|
||||
|
||||
pi_version = _get_pi_version()
|
||||
Reference in New Issue
Block a user