Source code for teek._structures

import abc
import base64
import collections.abc
import functools
import itertools
import os
import sys
import tempfile
import traceback

import teek
from teek._tcl_calls import make_thread_safe


def _is_from_teek(traceback_frame_summary):
    try:
        filename = traceback_frame_summary.filename
    except AttributeError:
        # python 3.4 or older, the frame summary is a tuple
        filename = traceback_frame_summary[0]

    teek_prefix = os.path.normcase(teek.__path__[0]).rstrip(os.sep) + os.sep
    return filename.startswith(teek_prefix)


[docs]class Callback: """An object that calls functions. Example:: >>> c = Callback() >>> c.connect(print, args=["hello", "world"]) >>> c.run() # runs print("hello", "world"), usually teek does this hello world >>> c.connect(print, args=["hello", "again"]) >>> c.run() hello world hello again """ def __init__(self): self._connections = []
[docs] def connect(self, function, args=(), kwargs=None): """Schedule ``callback(*args, **kwargs)`` to run. If some arguments are passed to :meth:`run`, they will appear before the *args* given here. For example: >>> c = Callback() >>> c.connect(print, args=['hello'], kwargs={'sep': '-'}) >>> c.run(1, 2) # print(1, 2, 'hello', sep='-') 1-2-hello The callback may return ``None`` or ``'break'``. In the above example, ``print`` returned ``None``. If the callback returns ``'break'``, two things are done differently: 1. No more connected callbacks will be ran. 2. :meth:`.run` returns ``'break'``, so that the code that called :meth:`.run` knows that one of the callbacks returned ``'break'``. This is used in :ref:`bindings <binding-break>`. """ stack = traceback.extract_stack() # skip some teek implementation details, they are too verbose while stack and _is_from_teek(stack[-1]): del stack[-1] stack_info = ''.join(traceback.format_list(stack)) if kwargs is None: kwargs = {} self._connections.append((function, args, kwargs, stack_info))
[docs] def disconnect(self, function): """Undo a :meth:`~connect` call. Note that this method doesn't do anything to the *args* and *kwargs* passed to :meth:`~connect`, so when disconnecting a function connected multiple times with different arguments, only the first connection is undone. >>> c = Callback() >>> c.connect(print, ["hello"]) >>> c.connect(print, ["hello", "again"]) >>> c.run() hello hello again >>> c.disconnect(print) >>> c.run() hello """ # enumerate objects aren't reversible :( for index in range(len(self._connections) - 1, -1, -1): # can't use is because cpython does this: # >>> class Thing: # ... def stuff(): pass # ... # >>> t = Thing() # >>> t.stuff == t.stuff # True # >>> t.stuff is t.stuff # False if self._connections[index][0] == function: del self._connections[index] return raise ValueError("not connected: %r" % (function,))
[docs] def run(self, *args): """Run the connected callbacks. If a callback returns ``'break'``, this returns ``'break'`` too without running more callbacks. If all callbacks run without returning ``'break'``, this returns ``None``. If a callback raises an exception, a traceback is printed and ``None`` is returned. """ for func, extra_args, kwargs, stack_info in self._connections: try: result = func(*(args + tuple(extra_args)), **kwargs) if result == 'break': return 'break' elif result is not None: raise ValueError( "expected None or 'break', got " + repr(result)) except Exception: # it's important that this does NOT call sys.stderr.write # directly because sys.stderr is None when running in windows # with pythonw.exe, and print('blah', file=None) does nothing # but None.write('blah\n') is an error traceback_blabla, rest = traceback.format_exc().split('\n', 1) print(traceback_blabla, file=sys.stderr) print(stack_info + rest, end='', file=sys.stderr) return None return None
# these are here because they are used in many places, like most other things # in this file before_quit = Callback() after_quit = Callback() class ConfigDict(collections.abc.MutableMapping): def __init__(self): # {option: type spec} # use .get(option, str), this is not a defaultdict because it tests # search for options not in this self._types = {} # {option: function called with 0 args that returns the value} self._special = {} # {option: return value from a function in self._special} self._special_values = {} def __repr__(self): return '<a config object, behaves like a dict>' def __call__(self, *args, **kwargs): raise TypeError("use widget.config['option'] = value, " "not widget.config(option=value)") @abc.abstractmethod def _set(self, option, value): """Sets an option to the given value. *option* is an option name without a leading dash. """ @abc.abstractmethod def _get(self, option): """Returns the value of an option. See _set. Should return a value of type self._types.get(option, str). """ @abc.abstractmethod def _list_options(self): """Returns an iterable of options that can be passed to _get.""" def _check_option(self, option): # by default, e.g. -tex would be equivalent to -text, but that's # disabled to make lookups in self._types and self._disabled # easier if option not in self._list_options(): raise KeyError(option) # for compatibility with dicts # the type of value is not checked with self._types because python is # dynamically typed @make_thread_safe def __setitem__(self, option, value): self._check_option(option) if option in self._special: message = "cannot set the value of %r" % option if isinstance(self[option], Callback): message += ( ", maybe use widget.config[%r].connect() instead?" % option ) raise ValueError(message) self._set(option, value) @make_thread_safe def __getitem__(self, option): self._check_option(option) if option in self._special_values: return self._special_values[option] if option in self._special: value = self._special[option]() self._special_values[option] = value return value return self._get(option) # MutableMapping requires that there is a __delitem__ def __delitem__(self, option): raise TypeError("options cannot be deleted") def __iter__(self): return iter(self._list_options()) def __len__(self): options = self._list_options() try: return len(options) except TypeError: # why can't len() consume iterators like 'in' :(( return len(list(options)) class CgetConfigureConfigDict(ConfigDict): def __init__(self, caller_func): super().__init__() self._caller_func = caller_func def _set(self, option, value): self._caller_func(None, 'configure', '-' + option, value) def _get(self, option): return self._caller_func(self._types.get(option, str), 'cget', '-' + option) def _list_options(self): infos = self._caller_func([[str]], 'configure') return (info[0].lstrip('-') for info in infos)
[docs]class Color: """Represents an RGB color. There are a few ways to create color objects: * ``Color(red, green, blue)`` creates a new color from an RGB value. The ``red``, ``green`` and ``blue`` should be integers between 0 and 255 (inclusive). * ``Color(hex_string)`` creates a color from a hexadecimal color string. For example, ``Color('#ff0000')`` is equivalent to ``Color(0xff, 0x00, 0x00)`` where ``0xff`` is hexadecimal notation for 255, and ``0x00`` is 0. * ``Color(color_name)`` creates a color object from a Tk color. There is a long list of color names in :man:`colors(3tk)`. Examples:: >>> Color(255, 255, 255) # r, g and b are all maximum, this is white <Color '#ffffff': red=255, green=255, blue=255> >>> Color('white') # 'white' is a Tk color name <Color 'white': red=255, green=255, blue=255> The string argument things are implemented by letting Tk interpret the color, so all of the ways to define colors as strings shown in :man:`Tk_GetColor(3tk)` are supported. Color objects are hashable, and they can be compared with ``==``:: >>> Color(0, 0, 255) == Color(0, 0, 255) True >>> Color(0, 255, 0) == Color(0, 0, 255) False Color objects are immutable. If you want to change a color, create a new Color object. .. attribute:: red green blue These are the values passed to ``Color()``. >>> Color(0, 0, 255).red 0 >>> Color(0, 0, 255).green 0 >>> Color(0, 0, 255).blue 255 Assigning to these like ``some_color.red = 255`` raises an exception. """ def __init__(self, *args): if len(args) == 3: for name, value in zip(['red', 'green', 'blue'], args): if value not in range(256): raise ValueError("invalid %s value: %r" % (name, value)) self._color_string = '#%02x%02x%02x' % args elif len(args) == 1: self._color_string = args[0] else: # python raises TypeError for wrong number of arguments raise TypeError("use {0}(red, green, blue) or {0}(color_string)" .format(type(self).__name__)) # any widget will do, i'm using the '.' root window because it # always exists rgb = teek.tcl_call([int], 'winfo', 'rgb', '.', self._color_string) # tk uses 16-bit colors for some reason, but most people are more # familiar with 8-bit colors so we'll shift away the "useless" bits self._rgb = tuple(value >> 8 for value in rgb) assert len(self._rgb) == 3 def __repr__(self): return '<%s %r: red=%d, green=%d, blue=%d>' % ( type(self).__name__, self._color_string, self.red, self.green, self.blue)
[docs] @classmethod def from_tcl(cls, color_string): """``Color.from_tcl(color_string)`` returns ``Color(color_string)``. This is just for compatibility with :ref:`type specifications <type-spec>`. """ return cls(color_string)
red = property(lambda self: self._rgb[0]) green = property(lambda self: self._rgb[1]) blue = property(lambda self: self._rgb[2])
[docs] def to_tcl(self): """Return this color as a Tk-compatible string. The string is *often* a hexadecimal ``'#rrggbb'`` string, but not always; it can be also e.g. a color name like ``'white'``. Use :attr:`red`, :attr:`green` and :attr:`blue` if you want a consistent representation. >>> Color(255, 0, 0).to_tcl() '#ff0000' >>> Color('red').to_tcl() 'red' >>> Color('red') == Color(255, 0, 0) True """ return self._color_string
# must not compare self._color_string because 'white' and '#ffffff' should # be equal def __eq__(self, other): if isinstance(other, Color): return self._rgb == other._rgb return NotImplemented # equal objects MUST have the same hash, so self._color_string can't be # used here def __hash__(self): return hash(self._rgb)
[docs]class TclVariable: """Represents a global Tcl variable. In Tcl, it's possible to e.g. run code when the value of a variable changes, or wait until the variable is set. Python's variables can't do things like that, so Tcl variables are represented as :class:`.TclVariable` objects in Python. If you want to set the value of the variable object, ``variable_object = new_value`` doesn't work because that only sets a Python variable, and you need ``variable_object.set(new_value)`` instead. Similarly, ``variable_object.get()`` returns the value of the Tcl variable. The :class:`.TclVariable` class is useless by itself. Usable variable classes are subclasses of it that override :attr:`type_spec`. Use ``SomeUsableTclVarSubclass(name='asd')`` to create a variable object that represents a Tcl variable named ``asd``, or ``SomeUsableTclVarSubclass()`` to let teek choose a variable name for you. .. attribute:: type_spec This class attribute should be set to a :ref:`type specification <type-spec>` of what :meth:`get` returns. """ _default_names = map('teek_var_{}'.format, itertools.count(1)) type_spec = None def __init__(self, *, name=None): if type(self).type_spec is None: raise TypeError(("cannot create instances of {0}, subclass {0} " "and set a 'type_spec' class attribute " "instead").format(type(self).__name__)) if name is None: name = next(type(self)._default_names) self._name = name self._write_trace = None def __repr__(self): try: value_repr = repr(teek.tcl_call(str, 'set', self)) except teek.TclError: value_repr = 'no value has been set' return '<%s %r: %s>' % (type(self).__name__, self.to_tcl(), value_repr) def __eq__(self, other): if not isinstance(other, TclVariable): return NotImplemented return (type(self).type_spec is type(other).type_spec and self._name == other._name) def __hash__(self): return hash((type(self).type_spec, self._name))
[docs] @classmethod def from_tcl(cls, varname): """Creates a variable object from a name string. See :ref:`type-spec` for details. """ return cls(name=varname)
[docs] def to_tcl(self): """Returns the variable name as a string.""" return self._name
[docs] def set(self, new_value): """Sets the value of the variable. The value does not need to be of the variable's type; it can be anything that can be :ref:`converted to tcl <to-tcl>`. """ teek.tcl_call(None, 'set', self, new_value)
[docs] def get(self): """Returns the value of the variable.""" return teek.tcl_call(type(self).type_spec, 'set', self._name)
[docs] def wait(self): """Waits for this variable to be modified. The GUI remains responsive during the waiting. See ``tkwait variable`` in :man:`tkwait(3tk)` for details. """ teek.tcl_call(None, 'tkwait', 'variable', self)
@property def write_trace(self): """ A :class:`.Callback` that runs when the value of the variable changes. The connected functions will be called with one argument, the variable object. This is implemented with ``trace add variable``, documented in :man:`trace(3tcl)`. """ if self._write_trace is None: self._write_trace = Callback() def runner(*junk): self._write_trace.run(self) command = teek.create_command(runner, [str, str, str]) teek.tcl_call(None, 'trace', 'add', 'variable', self, 'write', command) return self._write_trace
class StringVar(TclVariable): type_spec = str # noqa: E302 class IntVar(TclVariable): type_spec = int # noqa: E302 class FloatVar(TclVariable): type_spec = float # noqa: E302 class BooleanVar(TclVariable): type_spec = bool # noqa: E302
[docs]@functools.total_ordering class ScreenDistance: """Represents a Tk screen distance. If you don't know or care what screen distances are, use the :attr:`pixels` attribute. The ``value`` can be an integer or float of pixels or a string that :man:`Tk_GetPixels(3tk)` accepts; for example, ``123`` or ``'2i'``. ``ScreenDistance`` objects are hashable, and they can be compared with each other: >>> funny_dict = {ScreenDistance(1): 'lol'} >>> funny_dict[ScreenDistance(1)] 'lol' >>> ScreenDistance('1c') == ScreenDistance('1i') False >>> ScreenDistance('1c') < ScreenDistance('1i') True .. attribute:: pixels The number of pixels that this screen distance represents as an int. This is implemented with ``winfo pixels``, documented in :man:`winfo(3tk)`. .. attribute:: fpixels The number of pixels that this screen distance represents as a float. This is implemented with ``winfo fpixels``, documented in :man:`winfo(3tk)`. """ def __init__(self, value): self._value = str(value) # creating a ScreenDistance object must fail if the screen distance # is invalid, that's why this is here self.pixels = teek.tcl_call(int, 'winfo', 'pixels', '.', self._value) self.fpixels = teek.tcl_call(float, 'winfo', 'fpixels', '.', self._value) def __repr__(self): return '%s(%r)' % (type(self).__name__, self._value) # comparing works with integer pixels because floating point errors are # not fun def __eq__(self, other): if not isinstance(other, ScreenDistance): return NotImplemented return self.pixels == other.pixels def __gt__(self, other): if not isinstance(other, ScreenDistance): return NotImplemented return self.pixels > other.pixels def __hash__(self): return hash(self.fpixels)
[docs] @classmethod def from_tcl(cls, value_string): """Creates a screen distance object from a Tk screen distance string. See :ref:`type-spec` for details. """ return cls(value_string)
[docs] def to_tcl(self): """Return the ``value`` as a string.""" return self._value
def _options(kwargs): for name, value in kwargs.items(): yield ('-from' if name == 'from_' else '-' + name) yield value
[docs]class Image: """Represents a Tk photo image. If you want to display an image to the user, use :class:`.Label` with its ``image`` option. See :source:`examples/image.py`. Image objects are wrappers for things documented in :man:`image(3tk)` and :man:`photo(3tk)`. They are mutable, so you can e.g. set a label's image to an image object and then later change that image object; the label will update automatically. .. note:: PNG support was added in Tk 8.6. Use GIF images if you want backwards compatibility with Tk 8.5. If you want to create a program that can read as many different kinds of images as possible, use :mod:`teek.extras.image_loader`. Creating a new ``Image`` object with ``Image(...)`` calls ``image create photo`` followed by the options in Tcl. See :man:`image(3tk)` for details. Keyword arguments are passed as options to :man:`photo(3tk)` as usual, except that if a ``data`` keyword argument is given, it should be a :class:`bytes` object of data that came from e.g. an image file opened with ``'rb'``; it will be automatically converted to base64. Image objects can be compared with ``==``, and they compare equal if they represent the same Tk image; that is, ``image1 == image2`` returns ``image1.to_tcl() == image2.to_tcl()``. Image objects are also hashable. .. attribute:: config Similar to :ref:`the widget config attribute <options>`. """ @make_thread_safe def __init__(self, **kwargs): if 'data' in kwargs: kwargs['data'] = base64.b64encode(kwargs['data']).decode('ascii') if 'file' in kwargs: self._repr_info = 'from %r, ' % (kwargs['file'],) else: self._repr_info = '' name = teek.tcl_call(str, 'image', 'create', 'photo', *_options(kwargs)) self._init_from_name(name) @make_thread_safe def _init_from_name(self, name): self._name = name self.config = CgetConfigureConfigDict( lambda returntype, *args: teek.tcl_call(returntype, self, *args)) self.config._types.update({ 'data': str, 'format': str, 'file': str, 'gamma': float, 'width': int, 'height': int, 'palette': str, })
[docs] @classmethod def from_tcl(cls, name): """Create a new image object from the name of a Tk image. See :ref:`type-spec` for details. """ image = cls.__new__(cls) # create an instance without calling __init__ image._init_from_name(name) return image
[docs] def to_tcl(self): """Returns the Tk name of the image as a string.""" return self._name
def __repr__(self): try: size = '%dx%d' % (self.width, self.height) except teek.TclError: size = 'deleted' return '<%s: %s%s>' % (type(self).__name__, self._repr_info, size) def __eq__(self, other): if not isinstance(other, Image): return NotImplemented return self._name == other._name def __hash__(self): return hash(self._name)
[docs] def delete(self): """Calls ``image delete`` documented in :man:`image(3tk)`. The image object is useless after this, and most things will raise :exc:`.TclError`. """ teek.tcl_call(None, 'image', 'delete', self)
[docs] def in_use(self): """True if any widget uses this image, or False if not. This calls ``image inuse`` documented in :man:`image(3tk)`. """ return teek.tcl_call(bool, 'image', 'inuse', self)
[docs] @classmethod def get_all_images(cls): """Return all existing images as a list of :class:`.Image` objects.""" return teek.tcl_call([cls], 'image', 'names')
[docs] def blank(self): """See ``imageName blank`` in :man:`photo(3tk)`.""" teek.tcl_call(None, self, 'blank')
[docs] def copy_from(self, source_image, **kwargs): """See ``imageName copy sourceImage`` documented in :man:`photo(3tk)`. Options are passed as usual, except that ``from=something`` is invalid syntax in Python, so this method supports ``from_=something`` instead. If you do ``image1.copy_from(image2)``, the ``imageName`` in :man:`photo(3tk)` means ``image1``, and ``sourceImage`` means ``image2``. """ teek.tcl_call(None, self, 'copy', source_image, *_options(kwargs))
[docs] def copy(self, **kwargs): """ Create a new image with the same content as this image so that changing the new image doesn't change this image. This creates a new image and then calls :meth:`copy_from`, so that this... :: image2 = image1.copy() ...does the same thing as this:: image2 = teek.Image() image2.copy_from(image1) Keyword arguments passed to ``image1.copy()`` are passed to ``image2.copy_from()``. This means that it is possible to do some things with both :meth:`copy` and :meth:`copy_from`, but :meth:`copy` is consistent with e.g. :meth:`list.copy` and :meth:`dict.copy`. """ result = Image() result.copy_from(self, **kwargs) return result
@property def width(self): """The current width of the image as pixels. Note that ``image.width`` is different from ``image.config['width']``; ``image.width`` changes if the image's size changes, but ``image.config['width']`` often represents the width that the image had when it was first created. **tl;dr:** Usually it's best to use ``image.width`` instead of ``image.config['width']``. """ return teek.tcl_call(int, 'image', 'width', self) @property def height(self): """See :attr:`width`.""" return teek.tcl_call(int, 'image', 'height', self) # TODO: data and put methods, will be hard because passing around binary
[docs] def get(self, x, y): """Returns the :class:`.Color` of the pixel at (x,y).""" r, g, b = teek.tcl_call([int], self, 'get', x, y) return Color(r, g, b)
[docs] def read(self, filename, **kwargs): """See ``imageName read filename`` in :man:`photo(3tk)`.""" teek.tcl_call(None, self, 'read', filename, *_options(kwargs))
[docs] def redither(self): """See ``imageName redither`` in :man:`photo(3tk)`.""" teek.tcl_call(None, self, 'redither')
[docs] def transparency_get(self, x, y): """Check if the pixel at (x,y) is transparent, and return a bool. The *x* and *y* are pixels, as integers. See ``imageName transparency get`` in :man:`photo(3tk)`. """ return teek.tcl_call(bool, self, 'transparency', 'get', x, y)
[docs] def transparency_set(self, x, y, is_transparent): """Make the pixel at (x,y) transparent or not transparent. See ``imageName transparency set`` in :man:`photo(3tk)` and :meth:`transparency_get`. """ teek.tcl_call(None, self, 'transparency', 'set', x, y, is_transparent)
[docs] def write(self, filename, **kwargs): """See ``imageName write`` in :man:`photo(3tk)`. .. seealso:: Use :meth:`get_bytes` if you don't want to create a file. """ teek.tcl_call(None, self, 'write', filename, *_options(kwargs))
[docs] def get_bytes(self, format_, **kwargs): """ Like :meth:`write`, but returns the data as a :class:`bytes` object instead of writing it to a file. The ``format_`` argument can be any string that is compatible with the ``-format`` option of ``imageName write`` documented in :man:`photo(3tk)`. All keyword arguments are same as for :meth:`write`. """ with tempfile.TemporaryDirectory() as temp_dir: self.write(os.path.join(temp_dir, 'picture'), format=format_, **kwargs) with open(os.path.join(temp_dir, 'picture'), 'rb') as file: return file.read()