'].run('fake event')
26 |
27 | assert what_happened == [
28 | (1, False), (2, False),
29 | (1, True), (2, True),
30 | ]
31 |
--------------------------------------------------------------------------------
/tests/test_platform_info.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import teek
4 |
5 |
6 | def test_versions():
7 | for version in [teek.TCL_VERSION, teek.TK_VERSION]:
8 | assert isinstance(version, tuple)
9 | major, minor = version
10 | assert isinstance(major, int)
11 | assert isinstance(minor, int)
12 |
13 |
14 | def test_version_check(monkeypatch):
15 | with pytest.raises(AttributeError):
16 | teek.version_check
17 |
18 | from teek import _platform_info
19 | monkeypatch.setattr(_platform_info, 'TCL_VERSION', (1, 2))
20 | monkeypatch.setattr(_platform_info, 'TK_VERSION', (3, 4))
21 |
22 | with pytest.raises(RuntimeError) as error:
23 | _platform_info._version_check()
24 | assert str(error.value) == (
25 | "sorry, your Tcl/Tk installation is too old "
26 | "(expected 8.5 or newer, found Tcl 1.2 and Tk 3.4)")
27 |
28 |
29 | def test_windowingsystem():
30 | assert teek.windowingsystem() in {'x11', 'aqua', 'win32'}
31 |
--------------------------------------------------------------------------------
/docs/eventloop.rst:
--------------------------------------------------------------------------------
1 | .. _eventloop:
2 |
3 | Event Loop
4 | ==========
5 |
6 | Tk is event-based. When you click a :class:`~teek.Button`, a click event is
7 | generated, and Tk processes it. Usually that involves making the button look
8 | like it's pressed down, and maybe calling a callback function that you have
9 | told the button to run.
10 |
11 | The **event loop** works essentially like this pseudo code::
12 |
13 | while True:
14 | handle_an_event()
15 | if there_are_no_more_events_because_we_handled_all_of_them:
16 | wait_for_more_events()
17 |
18 | These functions can be used for working with the event loop:
19 |
20 | .. autofunction:: teek.run
21 | .. autofunction:: teek.quit
22 |
23 | .. data:: teek.before_quit
24 |
25 | :func:`.quit` runs this callback with no arguments before it does anything
26 | else. This means that when this callback runs, widgets have not been
27 | destroyed yet, but they will be destroyed soon.
28 |
29 | .. data:: teek.after_quit
30 |
31 | :func:`.quit` runs this callback when it has done everything else
32 | successfully.
33 |
34 | .. autofunction:: teek.update
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-2019 Akuli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/timeout.py:
--------------------------------------------------------------------------------
1 | import teek
2 |
3 |
4 | class TimeoutDemo(teek.Frame):
5 |
6 | def __init__(self, *args, **kwargs):
7 | super().__init__(*args, **kwargs)
8 | self.timeout = None
9 |
10 | startbutton = teek.Button(self, "Start", command=self.start)
11 | startbutton.pack()
12 | cancelbutton = teek.Button(self, "Cancel", command=self.cancel)
13 | cancelbutton.pack()
14 |
15 | def start(self):
16 | if self.timeout is None:
17 | self.timeout = teek.after(3000, self.callback)
18 | print("running callback after 3 seconds")
19 | else:
20 | print("already started")
21 |
22 | def cancel(self):
23 | if self.timeout is None:
24 | print("already cancelled")
25 | else:
26 | self.timeout.cancel()
27 | self.timeout = None
28 | print("callback won't be ran")
29 |
30 | def callback(self):
31 | print("*** running the callback ***")
32 | self.timeout = None
33 |
34 |
35 | window = teek.Window()
36 | TimeoutDemo(window).pack()
37 | window.on_delete_window.connect(teek.quit)
38 | teek.run()
39 |
--------------------------------------------------------------------------------
/examples/soup.py:
--------------------------------------------------------------------------------
1 | import bs4
2 |
3 | import teek
4 | from teek.extras.soup import SoupViewer
5 |
6 |
7 | html_string = """
8 | Hello World!
9 |
10 |
11 | This is some HTML text.
12 | Bold, italics and stuff are supported.
13 | This is a link.
14 |
15 |
16 | print("this is some code")
17 |
18 | Text in <code> tags displays with monospace font.
19 | It's easy to see the difference with the letter i:
20 |
21 | Without a code tag: iiiiiiiiii
22 | With a code tag: iiiiiiiiii
23 |
24 |
25 | This is teek's travis badge:
26 |
27 |
29 |
30 |
31 | iibb
32 | """
33 |
34 | teek.init_threads() # image loading uses threads
35 |
36 | window = teek.Window()
37 | text = teek.Text(window, font=('', 11, ''))
38 | text.pack(fill='both', expand=True)
39 |
40 | viewer = SoupViewer(text)
41 | viewer.create_tags()
42 | for element in bs4.BeautifulSoup(html_string, 'lxml').body:
43 | viewer.add_soup(element)
44 |
45 | window.on_delete_window.connect(teek.quit)
46 | teek.run()
47 |
48 | viewer.stop_loading(cleanup=True)
49 |
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 | === Text ===
2 | tag lower *2
3 | sel.first, sel.last could be used everywhere if they existed
4 | edit separator
5 | edit reset
6 | Text.search
7 | text index with @
8 |
9 | === winfo and wm ===
10 | winfo reqwidth *2
11 | winfo reqheight *2
12 | winfo x
13 | winfo y
14 | wm attributes
15 | wm min,maxsize
16 | wm resizable
17 | wm iconphoto
18 | wm overrideredirect
19 |
20 | === Entry ===
21 | selection range
22 |
23 | === Notebook ===
24 | lookup tab by xy
25 |
26 | === misc stuffs ===
27 | clipboard
28 | identify
29 | Style api
30 |
31 | === new extras ===
32 | simple dialogs would allow removing a lot of code from porcu
33 | ttkthemes needs Style api
34 |
35 |
36 |
37 | make threading stuff faster, this is slow:
38 |
39 | import threading
40 | import teek
41 |
42 | teek.init_threads()
43 | text = teek.Text(teek.Window())
44 | text.pack()
45 |
46 | def inserter():
47 | i = 0
48 | while True:
49 | i += 1
50 | text.insert(text.end, 'hello %d\n' % i)
51 | #teek.tcl_call(None, text, 'insert', 'end - 1 char', 'hello %d\n' % i)
52 |
53 | threading.Thread(target=inserter).start()
54 | teek.run()
55 |
56 | because text.insert and text.end end up in the queue as separate items, which
57 | is a problem because this is a particularly common way to use things in a
58 | thread... maybe add an insert_to_end method?
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
91 | .pytest_cache/
92 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. Teek documentation master file, created by
2 | sphinx-quickstart on Wed Aug 15 22:15:40 2018.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to Teek's documentation!
7 | ===================================
8 |
9 | Teek is a pythonic way to write Tk GUIs in Python. See
10 | `the README `_ for an introduction
11 | to teek, and then click the tutorial link below to get started.
12 |
13 |
14 | Tutorials and Guides
15 | --------------------
16 |
17 | .. toctree::
18 | :maxdepth: 1
19 |
20 | Beginner-friendly tutorial (start here)
21 | tkinter
22 | concurrency
23 |
24 |
25 | Reference
26 | ---------
27 |
28 | Use this section when you want to know something about things that were not
29 | covered in the tutorial.
30 |
31 | .. toctree::
32 | :maxdepth: 1
33 |
34 | geometry-managers
35 | widgets
36 | bind
37 | dialog
38 | misc-objs
39 | platform-info
40 | extras
41 |
42 |
43 | Understanding teek
44 | ---------------------
45 |
46 | Things documented here are useful if you want to know how stuff works or you
47 | want to do some advanced tricks with teek. I recommend reading these things
48 | if you want to help me with developing teek.
49 |
50 | .. toctree::
51 | :maxdepth: 1
52 |
53 | eventloop
54 | tcl-calls
55 |
56 | ..
57 | Indices and tables
58 | ==================
59 |
60 | * :ref:`genindex`
61 | * :ref:`modindex`
62 | * :ref:`search`
63 |
--------------------------------------------------------------------------------
/docs/platform-info.rst:
--------------------------------------------------------------------------------
1 | Platform Information
2 | ====================
3 |
4 | This page documents things that can tell you which platform your program is
5 | running on.
6 |
7 | .. data:: teek.TCL_VERSION
8 | teek.TK_VERSION
9 |
10 | These can be used for checking the versions of the Tcl interpreter and its
11 | Tk library that teek is using. These are two-tuples of integers, and you
12 | can compare integer tuples nicely, so you can do e.g. this::
13 |
14 | if teek.TK_VERSION >= (8, 6):
15 | # use a feature new in Tk 8.6
16 | else:
17 | # show an error message or do things without the new feature
18 |
19 | Teek refuses to run if Tcl or Tk is older than 8.5, so you can use all
20 | features new in Tcl/Tk 8.5 freely.
21 |
22 | .. note::
23 | The manual page links in this tutorial like :man:`label(3tk)` always
24 | point to the latest manual pages, which are for Tcl/Tk 8.6 at the time
25 | of writing this.
26 |
27 | .. autofunction:: teek.windowingsystem
28 |
29 | This function returns ``'win32'``, ``'aqua'`` or ``'x11'``. Use it instead
30 | of :func:`platform.system` when you have platform-specific teek code.
31 | For example, it's possible to run X11 on a Mac, in which case
32 | :func:`platform.system` returns ``'Darwin'`` and this function returns
33 | ``'x11'``. If you have code that should even then behave like it would
34 | normally behave on a Mac, use :func:`platform.system`.
35 |
36 | The Tk documentation for this function is ``tk windowingsystem`` in
37 | :man:`tk(3tk)`.
38 |
--------------------------------------------------------------------------------
/teek/extras/cross_platform.py:
--------------------------------------------------------------------------------
1 | import functools
2 |
3 | import teek
4 |
5 |
6 | # this is not called bind_tab to avoid confusing with:
7 | # * \t characters
8 | # * web browser tabs as in teek.Notebook
9 | def bind_tab_key(widget, callback, **bind_kwargs):
10 | """A cross-platform way to bind Tab and Shift+Tab.
11 |
12 | Use this function like this::
13 |
14 | from teek.extras import cross_platform
15 |
16 | def on_tab(shifted):
17 | if shifted:
18 | print("Shift+Tab was pressed")
19 | else:
20 | print("Tab was pressed")
21 |
22 | cross_platform.bind_tab_key(some_widget, on_tab)
23 |
24 | Binding ``''`` works on all systems I've tried it on, but if you also
25 | want to bind tab presses where the shift key is held down, use this
26 | function instead.
27 |
28 | This function can also take any of the keyword arguments that
29 | :meth:`teek.Widget.bind` takes. If you pass ``event=True``, the callback
30 | will be called like ``callback(shifted, event)``; that is, the ``shifted``
31 | bool is the first argument, and the event object is the second.
32 | """
33 | if teek.windowingsystem() == 'x11':
34 | # even though the event keysym says Left, holding down the right
35 | # shift and pressing tab also works :D
36 | shift_tab = ''
37 | else:
38 | shift_tab = '' # pragma: no cover
39 |
40 | widget.bind('', functools.partial(callback, False), **bind_kwargs)
41 | widget.bind(shift_tab, functools.partial(callback, True), **bind_kwargs)
42 |
--------------------------------------------------------------------------------
/tests/test_examples.py:
--------------------------------------------------------------------------------
1 | import os
2 | import teek
3 |
4 | try:
5 | # examples/soup.py does bs4.BeautifulSoup(html_string, 'lxml')
6 | import bs4 # noqa
7 | import lxml # noqa
8 | soup_py_can_run = True
9 | except ImportError:
10 | soup_py_can_run = False
11 |
12 |
13 | EXAMPLES_DIR = os.path.join(
14 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
15 | 'examples')
16 |
17 |
18 | # magic ftw
19 | # TODO: this doesn't work with pytest-xdist and pythons that don't have
20 | # ordered dict, i have no idea why and i don't know how to fix it
21 | def _create_test_function(filename):
22 | if filename == 'soup.py' and not soup_py_can_run:
23 | return
24 |
25 | with open(os.path.join(EXAMPLES_DIR, filename), 'r') as file:
26 | code = file.read()
27 |
28 | def func(monkeypatch, handy_callback):
29 | @handy_callback
30 | def fake_run():
31 | pass
32 |
33 | with monkeypatch.context() as monkey:
34 | monkey.setattr(teek, 'run', fake_run)
35 | exec(code, {'__file__': os.path.join(EXAMPLES_DIR, filename)})
36 |
37 | assert fake_run.ran_once()
38 |
39 | # make sure that nothing breaks if the real .run() is called
40 | teek.update()
41 | teek.after_idle(teek.quit)
42 | teek.run()
43 |
44 | func.__name__ = func.__qualname__ = 'test_' + filename.replace('.', '_')
45 | globals()[func.__name__] = func
46 |
47 |
48 | for filename in sorted(os.listdir(EXAMPLES_DIR)):
49 | if filename.endswith('.py') and not filename.startswith('_'):
50 | _create_test_function(filename)
51 |
--------------------------------------------------------------------------------
/docs/widgets.rst:
--------------------------------------------------------------------------------
1 | .. _widgets:
2 |
3 | Widget Reference
4 | ================
5 |
6 | This is a big list of widgets and explanations of what they do. As usual, don't
7 | try to read all of it, unless you're very bored; it's best to find what you're
8 | looking for, and ignore rest of this.
9 |
10 | .. toctree::
11 | :hidden:
12 |
13 | canvas
14 | menu
15 | notebook
16 | textwidget
17 |
18 | .. autoclass:: teek.Widget
19 | :members: from_tcl, to_tcl, destroy, focus, winfo_exists, winfo_children, winfo_toplevel, winfo_width, winfo_height, winfo_reqwidth, winfo_reqheight, winfo_x, winfo_y, winfo_rootx, winfo_rooty, winfo_id
20 |
21 | .. autoclass:: teek.Button
22 | :members:
23 |
24 | .. class:: teek.Canvas
25 | :noindex:
26 |
27 | See :ref:`canvaswidget`.
28 |
29 | .. autoclass:: teek.Checkbutton
30 | :members:
31 | .. autoclass:: teek.Combobox
32 | :members:
33 | .. autoclass:: teek.Entry
34 | :members:
35 | .. autoclass:: teek.Frame
36 | :members:
37 | .. autoclass:: teek.Label
38 | :members:
39 | .. autoclass:: teek.LabelFrame
40 | :members:
41 |
42 | .. class:: teek.Menu
43 | :noindex:
44 |
45 | See :ref:`menu`.
46 |
47 | .. class:: teek.Notebook
48 | :noindex:
49 |
50 | See :ref:`notebook`.
51 |
52 | .. autoclass:: teek.Progressbar
53 | :members:
54 | .. autoclass:: teek.Scrollbar
55 | :members:
56 | .. autoclass:: teek.Separator
57 | :members:
58 | .. autoclass:: teek.Spinbox
59 | :members:
60 |
61 | .. class:: teek.Text
62 | :noindex:
63 |
64 | See :ref:`textwidget`.
65 |
66 | .. autoclass:: teek.Toplevel
67 | :members:
68 | .. autoclass:: teek.Window
69 | :members:
70 |
--------------------------------------------------------------------------------
/teek/__init__.py:
--------------------------------------------------------------------------------
1 | """Teek is a pythonic alternative to tkinter."""
2 | # flake8: noqa
3 |
4 | import os as _os
5 | import sys as _sys
6 |
7 | __version__ = '0.6'
8 |
9 | if _os.environ.get('READTHEDOCS', None) == 'True': # pragma: no cover
10 | # readthedocs must be able to import everything without _tkinter
11 | import types
12 | _sys.modules['_tkinter'] = types.SimpleNamespace(
13 | TclError=None,
14 | TCL_VERSION='8.6',
15 | TK_VERSION='8.6',
16 | )
17 | else: # pragma: no cover
18 | # python 3.4's tkinter does this BEFORE importing _tkinter
19 | if _sys.platform.startswith("win32") and _sys.version_info < (3, 5):
20 | import tkinter._fix
21 |
22 |
23 | # not to be confused with _tkinter's TclError, this is defined here because
24 | # this way error messages say teek.TclError instead of
25 | # teek._something.TclError, or worse yet, _tkinter.TclError
26 | class TclError(Exception):
27 | """This is raised when a Tcl command fails."""
28 |
29 |
30 | # _platform_info does a version check, it must be first
31 | from teek._platform_info import TCL_VERSION, TK_VERSION, windowingsystem
32 | from teek._font import Font, NamedFont
33 | from teek._structures import (
34 | Callback, Color, Image, ScreenDistance, TclVariable, StringVar, IntVar,
35 | FloatVar, BooleanVar, before_quit, after_quit)
36 | from teek._tcl_calls import (
37 | tcl_call, tcl_eval, create_command, delete_command, run, quit, update,
38 | init_threads, make_thread_safe)
39 | from teek._timeouts import after, after_idle
40 | from teek._widgets.base import Widget
41 | from teek._widgets.canvas import Canvas
42 | from teek._widgets.menu import Menu, MenuItem
43 | from teek._widgets.misc import (
44 | Button, Checkbutton, Combobox, Entry, Frame, Label, LabelFrame,
45 | Progressbar, Scrollbar, Separator, Spinbox)
46 | from teek._widgets.notebook import Notebook, NotebookTab
47 | from teek._widgets.text import Text
48 | from teek._widgets.windows import Window, Toplevel
49 | from teek import dialog, extras
50 |
--------------------------------------------------------------------------------
/tests/extras/test_links.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import webbrowser
3 |
4 | import teek
5 | from teek.extras import links
6 |
7 |
8 | def test_links_clicking():
9 | text = teek.Text(teek.Window())
10 | text.insert(text.end, 'Blah')
11 |
12 | stuff = []
13 | links.add_function_link(
14 | text, functools.partial(stuff.append, 1), (1, 0), (1, 2))
15 | links.add_function_link(
16 | text, functools.partial(stuff.append, 2), (1, 2), (1, 4))
17 |
18 | all_tag_names = (tag.name for tag in text.get_all_tags())
19 | assert {'teek-extras-link-1',
20 | 'teek-extras-link-2',
21 | 'teek-extras-link-common'}.issubset(all_tag_names)
22 |
23 | assert text.get_tag('teek-extras-link-1').ranges() == [((1, 0), (1, 2))]
24 | assert text.get_tag('teek-extras-link-2').ranges() == [((1, 2), (1, 4))]
25 |
26 | text.get_tag('teek-extras-link-1').bindings['<1>'].run(None)
27 | text.get_tag('teek-extras-link-2').bindings['<1>'].run(None)
28 | assert stuff == [1, 2]
29 |
30 |
31 | def test_links_cursor_changes():
32 | text = teek.Text(teek.Window())
33 | text.config['cursor'] = 'clock'
34 | text.insert(text.end, 'abc')
35 |
36 | links.add_function_link(text, print, text.start, text.end)
37 | assert text.config['cursor'] == 'clock'
38 |
39 | for binding, cursor in [('', 'clock'),
40 | ('', 'hand2'),
41 | ('', 'clock')]:
42 | text.get_tag('teek-extras-link-common').bindings[binding].run(None)
43 | assert text.config['cursor'] == cursor
44 |
45 |
46 | URL = 'https://github.com/Akuli/teek'
47 |
48 |
49 | def test_add_url_link(monkeypatch, handy_callback):
50 | stuff = []
51 | monkeypatch.setattr(webbrowser, 'open', stuff.append)
52 |
53 | text = teek.Text(teek.Window())
54 | text.insert(text.end, 'teek')
55 | links.add_url_link(text, URL, text.start, text.end)
56 | text.get_tag('teek-extras-link-1').bindings['<1>'].run(None)
57 | assert stuff == [URL]
58 |
--------------------------------------------------------------------------------
/tests/extras/test_tooltips.py:
--------------------------------------------------------------------------------
1 | import time
2 | import types
3 | import pytest
4 |
5 | import teek
6 | from teek.extras import tooltips
7 |
8 |
9 | def run_event_loop(for_how_long):
10 | # this is dumb
11 | start = time.time()
12 | while time.time() < start + for_how_long:
13 | teek.update()
14 |
15 |
16 | @pytest.mark.slow
17 | def test_set_tooltip():
18 | window = teek.Window()
19 | assert not hasattr(window, '_tooltip_manager')
20 |
21 | tooltips.set_tooltip(window, None)
22 | assert not hasattr(window, '_tooltip_manager')
23 |
24 | tooltips.set_tooltip(window, 'Boo')
25 | assert window._tooltip_manager.text == 'Boo'
26 |
27 | tooltips.set_tooltip(window, None)
28 | assert window._tooltip_manager.text is None
29 |
30 | tooltips.set_tooltip(window, 'lol')
31 | assert window._tooltip_manager.text == 'lol'
32 |
33 | N = types.SimpleNamespace # because pep8 line length
34 |
35 | assert not window._tooltip_manager.got_mouse
36 | window._tooltip_manager.enter(N(widget=window, rootx=123, rooty=456))
37 | assert window._tooltip_manager.got_mouse
38 | assert window._tooltip_manager.mousex == 123
39 | assert window._tooltip_manager.mousey == 456
40 |
41 | window._tooltip_manager.motion(N(rootx=789, rooty=101112))
42 | assert window._tooltip_manager.got_mouse
43 | assert window._tooltip_manager.mousex == 789
44 | assert window._tooltip_manager.mousey == 101112
45 |
46 | run_event_loop(1.1)
47 | assert window._tooltip_manager.tipwindow is not None
48 | assert window._tooltip_manager.got_mouse
49 | window._tooltip_manager.leave(N(widget=window))
50 | assert not window._tooltip_manager.got_mouse
51 | assert window._tooltip_manager.tipwindow is None
52 |
53 | # what happens if the window gets destroyed before it's supposed to show?
54 | window._tooltip_manager.enter(N(widget=window, rootx=1, rooty=2))
55 | window._tooltip_manager.leave(N(widget=window))
56 | assert window._tooltip_manager.tipwindow is None
57 | run_event_loop(1.1)
58 | assert window._tooltip_manager.tipwindow is None
59 |
--------------------------------------------------------------------------------
/tests/test_timeouts.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 |
4 | import pytest
5 |
6 | import teek
7 |
8 |
9 | @pytest.mark.slow
10 | @pytest.mark.skipif('CI' in os.environ,
11 | reason="this fails randomly in travis, no idea why")
12 | def test_after():
13 | start = time.time()
14 | timeout = teek.after(200, teek.quit)
15 | assert repr(timeout).startswith("', enter)
25 | tag.bind('', leave)
26 | return tag
27 |
28 |
29 | def add_function_link(textwidget, function, start, end):
30 | """
31 | Like :func:`add_url_link`, but calls a function instead of opening a URL
32 | in a web browser.
33 | """
34 | common_tag = _init_links(textwidget)
35 |
36 | names = {tag.name for tag in textwidget.get_all_tags()}
37 | i = 1
38 | while _TAG_PREFIX + str(i) in names:
39 | i += 1
40 | specific_tag = textwidget.get_tag(_TAG_PREFIX + str(i))
41 |
42 | # bind callbacks must return None or 'break', but this ignores the
43 | # function's return value
44 | def none_return_function():
45 | function()
46 |
47 | specific_tag.bind('', none_return_function)
48 | for tag in [common_tag, specific_tag]:
49 | tag.add(start, end)
50 |
51 |
52 | def add_url_link(textwidget, url, start, end):
53 | """
54 | Make some of the text in the textwidget to be clickable so that clicking
55 | it will open ``url``.
56 |
57 | The text between the :ref:`text indexes ` ``start`` and
58 | ``end`` becomes clickable, and rest of the text is not touched.
59 |
60 | Do this if you want to insert some text and make it a link immediately::
61 |
62 | from teek.extras import links
63 |
64 | ...
65 |
66 | old_end = textwidget.end # adding text to end changes textwidget.end
67 | textwidget.insert(textwidget.end, 'Click me')
68 | links.add_url_link(textwidget, 'https://example.com/', old_end, textwi\
69 | dget.end)
70 |
71 | This function uses :func:`webbrowser.open` for opening ``url``.
72 | """
73 | add_function_link(textwidget, functools.partial(webbrowser.open, url),
74 | start, end)
75 |
--------------------------------------------------------------------------------
/docs/misc-objs.rst:
--------------------------------------------------------------------------------
1 | Miscellaneous Classes
2 | =====================
3 |
4 | This page contains documentation of classes that represent different kinds of
5 | things.
6 |
7 |
8 | Colors
9 | ------
10 |
11 | .. autoclass:: teek.Color
12 | :members:
13 |
14 |
15 | Callbacks
16 | ---------
17 |
18 | .. autoclass:: teek.Callback
19 | :members:
20 |
21 |
22 | .. _font-objs:
23 |
24 | Fonts
25 | -----
26 |
27 | There are different kinds of fonts in Tk:
28 |
29 | * **Named fonts** are mutable; if you set the font of a widget to a named font
30 | and you then change that named font (e.g. make it bigger), the widget's font
31 | will change as well.
32 | * **Anonymous fonts** don't work like that, but they are handy if you don't
33 | want to create a font object just to set the font of a widget.
34 |
35 | For example, if you have :class:`.Label`...
36 |
37 | >>> window = teek.Window()
38 | >>> label = teek.Label(window, "Hello World")
39 | >>> label.pack()
40 |
41 | ...and you want to make its text bigger, you can do this::
42 |
43 | >>> label.config['font'] = ('Helvetica', 20)
44 |
45 | This form is the teek equivalent of the alternative ``[3]`` in the
46 | ``FONT DESCRIPTIONS`` part of :man:`font(3tk)`. All of the other font
47 | descriptions work as well.
48 |
49 | If you then get the font of the label, you get a :class:`teek.Font` object:
50 |
51 | >>> label.config['font']
52 | Font('Helvetica 20')
53 |
54 | With a named font, the code looks like this:
55 |
56 | >>> named_font = teek.NamedFont(family='Helvetica', size=20)
57 | >>> label.config['font'] = named_font
58 | >>> named_font.size = 50 # even bigger! label will use this new size automatically
59 |
60 | Of course, :class:`.Font` and :class:`.NamedFont` objects can also be set to
61 | ``label.config['font']``.
62 |
63 | .. autoclass:: teek.Font
64 | :members:
65 |
66 | .. autoclass:: teek.NamedFont
67 | :members:
68 |
69 |
70 | Tcl Variable Objects
71 | --------------------
72 |
73 | .. autoclass:: teek.TclVariable
74 | :members:
75 |
76 | .. class:: teek.StringVar
77 | teek.IntVar
78 | teek.FloatVar
79 | teek.BooleanVar
80 |
81 | Handy :class:`.TclVariable` subclasses for variables with :class:`str`,
82 | :class:`int`, :class:`float` and :class:`bool` values, respectively.
83 |
84 |
85 | Images
86 | ------
87 |
88 | .. autoclass:: teek.Image
89 | :members:
90 |
91 |
92 | Screen Distance Objects
93 | -----------------------
94 |
95 | .. autoclass:: teek.ScreenDistance
96 | :members:
97 |
--------------------------------------------------------------------------------
/tests/test_vars.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import time
3 |
4 | import pytest
5 |
6 | import teek
7 |
8 |
9 | class IntListVar(teek.TclVariable):
10 | type_spec = [int]
11 |
12 |
13 | def test_basic_stuff():
14 | var = teek.IntVar()
15 | var.set('123') # doesn't need to be of same type
16 | assert var.get() == 123
17 |
18 | var = IntListVar()
19 | var.set([1, 2])
20 | assert var.get() == [1, 2]
21 |
22 |
23 | def test_eq_hash():
24 | stringvar1 = teek.StringVar(name='test_var')
25 | stringvar2 = teek.StringVar(name='test_var')
26 | lol = teek.StringVar(name='lol_var')
27 | intvar = teek.IntVar(name='test_var')
28 |
29 | assert stringvar1 == stringvar1
30 | assert stringvar1 == stringvar2
31 | assert stringvar1 != lol
32 | assert stringvar1 != intvar
33 | assert stringvar1 != 1234
34 |
35 | assert {stringvar1: 'asd'}[stringvar2] == 'asd'
36 | with pytest.raises(KeyError):
37 | {stringvar1: 'asd'}[intvar]
38 |
39 |
40 | def test_write_trace(handy_callback):
41 | var = IntListVar()
42 |
43 | @handy_callback
44 | def tracer(arg):
45 | assert arg is var
46 |
47 | assert var.write_trace is var.write_trace
48 | var.write_trace.connect(tracer)
49 | var.set([1, 2, 3])
50 | assert tracer.ran_once()
51 |
52 | # nothing in write trace must break if this is done, only get() breaks
53 | var.set('osiadjfoaisdj')
54 |
55 |
56 | def test_creating_var_objects_from_name():
57 | asd = []
58 | var = teek.StringVar()
59 | var.write_trace.connect(lambda junk: asd.append(var.get()))
60 |
61 | var.set('a')
62 | teek.StringVar(name=var.to_tcl()).set('b')
63 | teek.StringVar.from_tcl(var.to_tcl()).set('c')
64 | assert asd == ['a', 'b', 'c']
65 |
66 |
67 | def test_repr():
68 | var = teek.StringVar(name='testie_var')
69 | assert repr(var) == ""
70 | var.set('asd')
71 | assert repr(var) == ""
72 |
73 |
74 | @pytest.mark.slow
75 | def test_wait():
76 | var = teek.StringVar()
77 | start = time.time()
78 | teek.after(500, functools.partial(var.set, "boo"))
79 | var.wait() # should run the event loop ==> after callback works
80 | end = time.time()
81 | assert (end - start) > 0.5
82 |
83 |
84 | def test_not_subclassed():
85 | with pytest.raises(TypeError) as error:
86 | teek.TclVariable()
87 | assert "cannot create instances of TclVariable" in str(error.value)
88 | assert "subclass TclVariable" in str(error.value)
89 | assert "'type_spec' class attribute" in str(error.value)
90 |
--------------------------------------------------------------------------------
/teek/extras/image_loader.py:
--------------------------------------------------------------------------------
1 | # if you change this file, consider also changing image_loader_dummy.py
2 |
3 | import io
4 |
5 | # see pyproject.toml
6 | try:
7 | import lxml.etree
8 | import PIL.Image
9 | import reportlab.graphics.renderPM
10 | from svglib import svglib
11 | except ImportError as e:
12 | raise ImportError(str(e) + ". Maybe try 'pip install teek[image_loader]' "
13 | "to fix this?").with_traceback(e.__traceback__) from None
14 |
15 | import teek
16 |
17 | # both of these files have the same from_pil implementation, because it doesn't
18 | # actually need to import PIL
19 | from teek.extras.image_loader_dummy import from_pil
20 |
21 |
22 | def from_file(file):
23 | """Creates a :class:`teek.Image` from a file object.
24 |
25 | The file object must be readable, and it must be in bytes mode. It must
26 | have ``read()``, ``seek()`` and ``tell()`` methods. For example, files from
27 | ``open(some_path, 'rb')`` and ``io.BytesIO(some_data)`` work.
28 |
29 | This supports all file formats that PIL supports and SVG.
30 |
31 | Example::
32 |
33 | from teek.extras import image_loader
34 |
35 | with open(the_image_path, 'rb') as file:
36 | image = image_loader.from_file(file)
37 | """
38 | first_3_bytes = file.read(3)
39 | file.seek(0)
40 | if first_3_bytes == b'GIF':
41 | return teek.Image(data=file.read())
42 |
43 | # https://stackoverflow.com/a/15136684
44 | try:
45 | event, element = next(lxml.etree.iterparse(file, ('start',)))
46 | is_svg = (element.tag == '{http://www.w3.org/2000/svg}svg')
47 | except (lxml.etree.ParseError, StopIteration):
48 | is_svg = False
49 | file.seek(0)
50 |
51 | if is_svg:
52 | # svglib takes open file objects, even though it doesn't look like it
53 | # https://github.com/deeplook/svglib/issues/173
54 | rlg = svglib.svg2rlg(io.TextIOWrapper(file, encoding='utf-8'))
55 |
56 | with reportlab.graphics.renderPM.drawToPIL(rlg) as pil_image:
57 | return from_pil(pil_image)
58 |
59 | with PIL.Image.open(file) as pil_image:
60 | return from_pil(pil_image)
61 |
62 |
63 | def from_bytes(bytes_):
64 | """
65 | Creates a :class:`teek.Image` from bytes that would normally be in an image
66 | file.
67 |
68 | Example::
69 |
70 | # if you have a file, it's recommended to use
71 | # from_file() instead, but this example works anyway
72 | with open(the_image_path, 'rb') as file:
73 | data = file.read()
74 |
75 | image = image_loader.from_bytes(data)
76 | """
77 | with io.BytesIO(bytes_) as fake_file:
78 | return from_file(fake_file)
79 |
--------------------------------------------------------------------------------
/teek/_timeouts.py:
--------------------------------------------------------------------------------
1 | import teek
2 | from teek._tcl_calls import make_thread_safe
3 |
4 |
5 | # there's no after_info because i don't see how it would be useful in
6 | # teek
7 |
8 | class _Timeout:
9 |
10 | def __init__(self, after_what, callback, args, kwargs):
11 | if kwargs is None:
12 | kwargs = {}
13 |
14 | self._callback = callback
15 | self._args = args
16 | self._kwargs = kwargs
17 |
18 | self._state = 'pending' # just for __repr__ and error messages
19 | self._tcl_command = teek.create_command(self._run)
20 | self._id = teek.tcl_call(str, 'after', after_what, self._tcl_command)
21 |
22 | def __repr__(self):
23 | name = getattr(self._callback, '__name__', self._callback)
24 | return '<%s %r timeout %r>' % (self._state, name, self._id)
25 |
26 | def _run(self):
27 | needs_cleanup = True
28 |
29 | # this is important, thread tests freeze without this special
30 | # case for some reason
31 | def quit_callback():
32 | nonlocal needs_cleanup
33 | needs_cleanup = False
34 |
35 | teek.before_quit.connect(quit_callback)
36 |
37 | try:
38 | self._callback(*self._args, **self._kwargs)
39 | self._state = 'successfully completed'
40 | except Exception as e:
41 | self._state = 'failed'
42 | raise e
43 | finally:
44 | teek.before_quit.disconnect(quit_callback)
45 | if needs_cleanup:
46 | teek.delete_command(self._tcl_command)
47 |
48 | @make_thread_safe
49 | def cancel(self):
50 | """Prevent this timeout from running as scheduled.
51 |
52 | :exc:`RuntimeError` is raised if the timeout has already ran or
53 | it has been cancelled.
54 |
55 | There is example code in :source:`examples/timeout.py`.
56 | """
57 | if self._state != 'pending':
58 | raise RuntimeError("cannot cancel a %s timeout" % self._state)
59 | teek.tcl_call(None, 'after', 'cancel', self._id)
60 | self._state = 'cancelled'
61 | teek.delete_command(self._tcl_command)
62 |
63 |
64 | @make_thread_safe
65 | def after(ms, callback, args=(), kwargs=None):
66 | """Run ``callback(*args, **kwargs)`` after waiting for the given time.
67 |
68 | The *ms* argument should be a waiting time in milliseconds, and
69 | *kwargs* defaults to ``{}``. This returns a timeout object with a
70 | ``cancel()`` method that takes no arguments; you can use that to
71 | cancel the timeout before it runs.
72 | """
73 | return _Timeout(ms, callback, args, kwargs)
74 |
75 |
76 | @make_thread_safe
77 | def after_idle(callback, args=(), kwargs=None):
78 | """Like :func:`after`, but runs the timeout as soon as possible."""
79 | return _Timeout('idle', callback, args, kwargs)
80 |
--------------------------------------------------------------------------------
/docs/dialog.rst:
--------------------------------------------------------------------------------
1 | Dialogs
2 | =======
3 |
4 | .. module:: teek.dialog
5 |
6 | This page contains things for asking the user things like file names and
7 | colors. If you want to display a custom dialog, create a :class:`.Window`, add
8 | some stuff to it and use :meth:`~.Toplevel.wait_window`.
9 |
10 | .. note::
11 | All functions documented on this page take a ``parent`` keyword argument.
12 | Use that whenever you are calling the functions from a program that has
13 | another window. This way the dialog will look like it belongs to that
14 | parent window.
15 |
16 |
17 | Message Boxes
18 | -------------
19 |
20 | These functions call :man:`tk_messageBox(3tk)`. Options are passed to
21 | :man:`tk_messageBox(3tk)` so that this code...
22 | ::
23 |
24 | teek.dialog.ok_cancel("Question", "Do you want that?")
25 |
26 | ...does a Tcl call like this:
27 |
28 | .. code-block:: tcl
29 |
30 | tk_messageBox -type okcancel -title "Question" -message "Do you want that?" -icon question
31 |
32 | .. function:: info(title, message, detail=None, **kwargs)
33 | warning(title, message, detail=None, **kwargs)
34 | error(title, message, detail=None, **kwargs)
35 |
36 | Each of these functions shows a message box that has an "ok" button. The
37 | icon option is ``'info'``, ``'warning'`` or ``'error'`` respectively. These
38 | functions always return ``None``.
39 |
40 | .. function:: ok_cancel(title, message, detail=None, **kwargs)
41 |
42 | Shows a message box with "ok" and "cancel" buttons. The icon is
43 | ``'question'`` by default, but you can change it by passing a keyword
44 | argument, e.g. ``icon='warning'``. Returns ``True`` if "ok" is clicked, and
45 | ``False`` if "cancel" is clicked.
46 |
47 | .. function:: retry_cancel(title, message, detail=None, **kwargs)
48 |
49 | Like :func:`ok_cancel`, but with a "retry" button instead of an "ok" button
50 | and ``'warning'`` as the default icon.
51 |
52 | .. function:: yes_no(title, message, detail=None, **kwargs)
53 |
54 | Shows a message box with "yes" and "no" buttons. The icon is ``'question'``
55 | by default. Returns ``True`` for "yes" and ``False`` for "no".
56 |
57 | .. function:: yes_no_cancel(title, message, detail=None, **kwargs)
58 |
59 | Shows a message box with "yes", "no" and "cancel" buttons. The icon is
60 | ``'question'`` by default. Returns one of the strings ``'yes'``, ``'no'``
61 | or ``'cancel'``.
62 |
63 | .. function:: abort_retry_ignore(title, message, detail=None, **kwargs)
64 |
65 | Like :func:`yes_no_cancel`, but with different buttons and return value
66 | strings. The icon is ``'error'`` by default.
67 |
68 |
69 | File and Directory Dialogs
70 | --------------------------
71 |
72 | Keyword arguments work as usual. Note that paths are returned as strings of
73 | absolute paths, not e.g. :class:`pathlib.Path` objects.
74 |
75 | .. autofunction:: open_file
76 | .. autofunction:: open_multiple_files
77 | .. autofunction:: save_file
78 | .. autofunction:: directory
79 |
80 |
81 | Other Dialogs
82 | -------------
83 |
84 | .. autofunction:: color
85 |
--------------------------------------------------------------------------------
/docs/extensions.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import os
3 | import re
4 | import urllib.request # because requests might not be installed
5 |
6 | import docutils.nodes
7 | import docutils.utils
8 | from sphinx.util.nodes import split_explicit_title
9 |
10 |
11 | @functools.lru_cache()
12 | def check_url(url):
13 | # this is kind of slow on my system, so do nothing unless running in
14 | # readthedocs
15 | if os.environ.get('READTHEDOCS', None) != 'True':
16 | return
17 |
18 | print("\ndocs/extensions.py: checking if url exists: %s" % url)
19 |
20 | # this doesn't work with urllib's default User-Agent for some reason
21 | request = urllib.request.Request(url, headers={'User-Agent': 'asd'})
22 | response = urllib.request.urlopen(request)
23 | assert response.status == 200
24 | assert (b'The page you are looking for cannot be found.'
25 | not in response.read()), url
26 |
27 |
28 | # there are urls like .../man/tcl8.6/... and .../man/tcl/...
29 | # the non-8.6 ones always point to latest version, which is good because no
30 | # need to hard-code version number
31 | MAN_URL_TEMPLATE = 'https://www.tcl.tk/man/tcl/%s%s/%s.htm'
32 |
33 | # because multiple functions are documented in the same man page
34 | # for example, 'man Tk_GetBoolean' and 'man Tk_GetInt' open the same manual
35 | # page on my system
36 | MANPAGE_REDIRECTS = {
37 | 'Tcl_GetBoolean': 'Tcl_GetInt',
38 | 'tk_getSaveFile': 'tk_getOpenFile',
39 | }
40 |
41 | SOURCE_URI_PREFIX = 'https://github.com/Akuli/teek/blob/master/'
42 |
43 |
44 | def get_manpage_url(manpage_name, tcl_or_tk):
45 | manpage_name = MANPAGE_REDIRECTS.get(manpage_name, manpage_name)
46 |
47 | # c functions are named like Tk_GetColor, and the URLs only contain the
48 | # GetColor part for some reason
49 | is_c_function = manpage_name.startswith(tcl_or_tk.capitalize() + '_')
50 |
51 | # ik, this is weird
52 | if manpage_name.startswith('ttk_'):
53 | # ttk_separator --> ttk_separator
54 | name_part = manpage_name
55 | else:
56 | # tk_chooseColor --> chooseColor
57 | # Tk_GetColor --> GetColor
58 | name_part = manpage_name.split('_')[-1]
59 |
60 | return MAN_URL_TEMPLATE % (tcl_or_tk.capitalize(),
61 | 'Lib' if is_c_function else 'Cmd',
62 | name_part)
63 |
64 |
65 | # i don't know how to use sphinx, i copy/pasted things from here:
66 | # https://doughellmann.com/blog/2010/05/09/defining-custom-roles-in-sphinx/
67 | def man_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
68 | match = re.fullmatch(r'(\w+)\(3(tcl|tk)\)', text)
69 | assert match is not None, "invalid man page %r" % text
70 | manpage_name, tcl_or_tk = match.groups()
71 |
72 | url = get_manpage_url(manpage_name, tcl_or_tk)
73 | check_url(url)
74 |
75 | # this is the copy/pasta part
76 | node = docutils.nodes.reference(rawtext, text, refuri=url, **options)
77 | return [node], []
78 |
79 |
80 | # this is mostly copy/pasted from cpython's Doc/tools/extensions/pyspecific.py
81 | def source_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
82 | has_t, title, target = split_explicit_title(text)
83 | title = docutils.utils.unescape(title)
84 | target = docutils.utils.unescape(target)
85 |
86 | url = SOURCE_URI_PREFIX + target
87 | check_url(url)
88 |
89 | refnode = docutils.nodes.reference(title, title, refuri=url)
90 | return [refnode], []
91 |
92 |
93 | def setup(app):
94 | app.add_role('source', source_role)
95 | app.add_role('man', man_role)
96 |
--------------------------------------------------------------------------------
/docs/notebook.rst:
--------------------------------------------------------------------------------
1 | .. _notebook:
2 |
3 | Notebook Widget
4 | ===============
5 |
6 | **Manual page:** :man:`ttk_notebook(3tk)`
7 |
8 | This widget is useful for creating tabs as in the tabs of your web browser, not
9 | ``\t`` characters. Let's look at an example.
10 |
11 | ::
12 |
13 | import teek
14 |
15 | window = teek.Window("Notebook Example")
16 | notebook = teek.Notebook(window)
17 | notebook.pack(fill='both', expand=True)
18 |
19 | for number in [1, 2, 3]:
20 | label = teek.Label(notebook, "Hello {}!".format(number))
21 | tab = teek.NotebookTab(label, text="Tab {}".format(number))
22 | notebook.append(tab)
23 |
24 | window.geometry(300, 200)
25 | window.on_delete_window.connect(teek.quit)
26 | teek.run()
27 |
28 | This program displays a notebook widget with 3 tabs. Let's go trough some of
29 | the code.
30 |
31 | ::
32 |
33 | label = teek.Label(notebook, "Hello {}!".format(number))
34 |
35 | The label is created as a child widget of the notebook, because it will be
36 | added to the notebook eventually. However, we *don't* use a
37 | :ref:`geometry manager ` because the notebook itself handles
38 | showing the widget.
39 |
40 | ::
41 |
42 | tab = teek.NotebookTab(label, text="Tab {}".format(number))
43 |
44 | We need to create a :class:`.NotebookTab` object in order to add a tab to the
45 | notebook. The :class:`.NotebookTab` objects themselves are **not** widgets. A
46 | :class:`.NotebookTab` represents a widget and its tab options like ``text``.
47 |
48 | ::
49 |
50 | notebook.append(tab)
51 |
52 | Lists also have an append method, and this is no coincidence.
53 | :class:`.Notebook` widgets behave like lists, and if you can do something to a
54 | list, you can probably do it to a notebook as well. However, there are a few
55 | things you can't do to notebook widgets, but you can do to lists:
56 |
57 | * You can't put any objects you want into a :class:`.Notebook`. All the objects
58 | must be :class:`NotebookTabs <.NotebookTab>`.
59 | * You can't slice notebooks like ``notebook[1:]``. However, you can get a list
60 | of all tabs in the notebook with ``list(notebook)`` and then slice that.
61 | * You can't sort notebooks like ``notebook.sort()``. However, you can do
62 | ``sorted(notebook)`` to get a sorted list of
63 | :class:`NotebookTabs <.NotebookTab>`, except that it doesn't quite work
64 | because tab objects can't be compared with each other. Something like
65 | ``sorted(notebook, key=(lambda tab: tab.config['text']))`` might be useful
66 | though.
67 | * You can't add the same :class:`.NotebookTab` to the notebook twice, and in
68 | fact, you can't create two :class:`NotebookTabs <.NotebookTab>` that
69 | represent the same widget, so you can't add the same widget to the notebook
70 | as two different tabs.
71 |
72 | Note that instead of this...
73 |
74 | ::
75 |
76 | label = teek.Label(notebook, "Hello {}!".format(number))
77 | tab = teek.NotebookTab(label, text="Tab {}".format(number))
78 | notebook.append(tab)
79 |
80 | ...you can also do this::
81 |
82 | notebook.append(
83 | teek.NotebookTab(
84 | teek.Label(notebook, "Hello {}!".format(number)),
85 | text="Tab {}".format(number)
86 | )
87 | )
88 |
89 | I recommend using common sense here. The first alternative is actually only
90 | half as many lines of code as the second one, even though it uses more
91 | variables. Try to keep the code readable, as usual.
92 |
93 | Here is some reference:
94 |
95 | .. autoclass:: teek.Notebook
96 | :members:
97 | .. autoclass:: teek.NotebookTab
98 | :members:
99 |
--------------------------------------------------------------------------------
/teek/extras/tooltips.py:
--------------------------------------------------------------------------------
1 | import teek
2 |
3 |
4 | class _TooltipManager:
5 |
6 | # This needs to be shared by all instances because there's only one
7 | # mouse pointer.
8 | tipwindow = None
9 |
10 | def __init__(self, widget: teek.Widget):
11 | widget.bind('', self.enter, event=True)
12 | widget.bind('', self.leave, event=True)
13 | widget.bind('', self.motion, event=True)
14 | self.widget = widget
15 | self.got_mouse = False
16 | self.text = None
17 |
18 | @classmethod
19 | def destroy_tipwindow(cls):
20 | if cls.tipwindow is not None:
21 | cls.tipwindow.destroy()
22 | cls.tipwindow = None
23 |
24 | def enter(self, event):
25 | # For some reason, toplevels get also notified of their
26 | # childrens' events.
27 | if event.widget is self.widget:
28 | self.destroy_tipwindow()
29 | self.got_mouse = True
30 | teek.after(1000, self.show)
31 |
32 | # these are important, it's possible to enter without mouse move
33 | self.mousex = event.rootx
34 | self.mousey = event.rooty
35 |
36 | def leave(self, event):
37 | if event.widget is self.widget:
38 | self.destroy_tipwindow()
39 | self.got_mouse = False
40 |
41 | def motion(self, event):
42 | self.mousex = event.rootx
43 | self.mousey = event.rooty
44 |
45 | def show(self):
46 | if not self.got_mouse:
47 | return
48 |
49 | self.destroy_tipwindow()
50 | if self.text is not None:
51 | # the label and the tipwindow are not ttk widgets because that way
52 | # they can look different than other widgets, which is what
53 | # tooltips are usually like
54 | tipwindow = type(self).tipwindow = teek.Toplevel()
55 | tipwindow.geometry(x=(self.mousex + 10), y=(self.mousey - 10))
56 | tipwindow.bind('', self.destroy_tipwindow)
57 |
58 | # TODO: add overrideredirect to teek
59 | teek.tcl_call(None, 'wm', 'overrideredirect', tipwindow, 1)
60 |
61 | # i don't think there's a need to add better support for things
62 | # like non-ttk labels because they are not needed very often
63 | #
64 | # refactoring note: if you change this, make sure that either BOTH
65 | # of fg and bg are set, or NEITHER is set, because e.g. bg='white'
66 | # with no fg gives white text on white background on systems with
67 | # white default foreground, but works fine on systems with black
68 | # default foreground
69 | label = teek.tcl_call(str, 'label', tipwindow.to_tcl() + '.label',
70 | '-text', self.text, '-bd', 1, '-relief',
71 | 'solid', '-fg', 'black', '-bg', '#ffc',
72 | '-padx', 2, '-pady', 2)
73 | teek.tcl_call(None, 'pack', label)
74 |
75 |
76 | def set_tooltip(widget, text):
77 | """Create tooltips for a widget.
78 |
79 | After calling ``set_tooltip(some_widget, "hello")``, "hello" will be
80 | displayed in a small window when the user moves the mouse over the
81 | widget and waits for a small period of time. Do
82 | ``set_tooltip(some_widget, None)`` to get rid of a tooltip.
83 | """
84 | if text is None:
85 | if hasattr(widget, '_tooltip_manager'):
86 | widget._tooltip_manager.text = None
87 | else:
88 | if not hasattr(widget, '_tooltip_manager'):
89 | widget._tooltip_manager = _TooltipManager(widget)
90 | widget._tooltip_manager.text = text
91 |
--------------------------------------------------------------------------------
/tests/extras/test_image_loader.py:
--------------------------------------------------------------------------------
1 | import io
2 | import os
3 | import sys
4 |
5 | import pytest
6 |
7 | import teek
8 | from teek.extras import image_loader_dummy
9 | try:
10 | from teek.extras import image_loader
11 | loader_libs = [image_loader, image_loader_dummy]
12 | except ImportError as e:
13 | assert 'pip install teek[image_loader]' in str(e)
14 | image_loader = None
15 | loader_libs = [image_loader_dummy]
16 | else:
17 | import PIL.Image
18 |
19 |
20 | ignore_svglib_warnings = pytest.mark.filterwarnings(
21 | "ignore:The 'warn' method is deprecated")
22 | needs_image_loader = pytest.mark.skipif(
23 | image_loader is None,
24 | reason="teek.extras.image_loader dependencies are not installed")
25 |
26 | DATA_DIR = os.path.join(
27 | os.path.dirname(os.path.abspath(__file__)), '..', 'data')
28 |
29 |
30 | def test_nice_import_error(monkeypatch, tmp_path):
31 | old_modules = sys.modules.copy()
32 | try:
33 | for module in ['PIL', 'PIL.Image', 'teek.extras.image_loader']:
34 | if module in sys.modules:
35 | del sys.modules[module]
36 |
37 | with open(os.path.join(str(tmp_path), 'PIL.py'), 'w') as bad_pil:
38 | bad_pil.write('raise ImportError("oh no")')
39 |
40 | monkeypatch.syspath_prepend(str(tmp_path))
41 | with pytest.raises(ImportError) as error:
42 | import teek.extras.image_loader # noqa
43 |
44 | assert str(error.value) == (
45 | "oh no. Maybe try 'pip install teek[image_loader]' to fix this?")
46 | finally:
47 | sys.modules.update(old_modules)
48 |
49 |
50 | def images_equal(image1, image2):
51 | if image1.width != image2.width or image1.height != image2.height:
52 | return False
53 | return image1.get_bytes('gif') == image2.get_bytes('gif')
54 |
55 |
56 | @needs_image_loader
57 | @pytest.mark.filterwarnings("ignore:unclosed file") # because PIL
58 | def test_from_pil():
59 | for lib in loader_libs:
60 | smiley1 = teek.Image(file=os.path.join(DATA_DIR, 'smiley.gif'))
61 | with PIL.Image.open(os.path.join(DATA_DIR, 'smiley.gif')) as pillu:
62 | smiley2 = lib.from_pil(pillu)
63 | assert images_equal(smiley1, smiley2)
64 |
65 |
66 | # yields different kinds of file objects that contain the data from the file
67 | def open_file(path):
68 | with open(path, 'rb') as file:
69 | yield io.BytesIO(file.read())
70 | file.seek(0)
71 | yield file
72 |
73 |
74 | def test_from_file_gif():
75 | for lib in loader_libs:
76 | smiley1 = teek.Image(file=os.path.join(DATA_DIR, 'smiley.gif'))
77 | for file in open_file(os.path.join(DATA_DIR, 'smiley.gif')):
78 | smiley2 = lib.from_file(file)
79 | assert images_equal(smiley1, smiley2)
80 |
81 |
82 | @needs_image_loader
83 | def test_from_file_pil():
84 | smileys = []
85 | for file in open_file(os.path.join(DATA_DIR, 'smiley.jpg')):
86 | smileys.append(image_loader.from_file(file))
87 |
88 | for smiley in smileys:
89 | assert smiley.width == 32
90 | assert smiley.height == 32
91 | assert images_equal(smiley, smileys[0])
92 |
93 |
94 | @needs_image_loader
95 | @ignore_svglib_warnings
96 | @pytest.mark.slow
97 | def test_from_file_svg():
98 | for filename in ['firefox.svg', 'rectangle.svg']:
99 | images = []
100 | for file in open_file(os.path.join(DATA_DIR, filename)):
101 | images.append(image_loader.from_file(file))
102 |
103 | for image in images:
104 | assert images_equal(image, images[0])
105 |
106 |
107 | def test_from_bytes():
108 | for lib in loader_libs:
109 | with open(os.path.join(DATA_DIR, 'smiley.gif'), 'rb') as file1:
110 | smiley1 = lib.from_file(file1)
111 | with open(os.path.join(DATA_DIR, 'smiley.gif'), 'rb') as file2:
112 | smiley2 = lib.from_bytes(file2.read())
113 | assert images_equal(smiley1, smiley2)
114 |
--------------------------------------------------------------------------------
/teek/dialog.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 | import os
3 |
4 | import teek
5 |
6 |
7 | def _options(kwargs):
8 | if 'parent' in kwargs and isinstance(kwargs['parent'], teek.Window):
9 | kwargs['parent'] = kwargs['parent'].toplevel
10 |
11 | for name, value in kwargs.items():
12 | yield '-' + name
13 | yield value
14 |
15 |
16 | def color(**kwargs):
17 | """Calls :man:`tk_chooseColor(3tk)`.
18 |
19 | The color selected by the user is returned, or ``None`` if the user
20 | cancelled the dialog.
21 | """
22 | return teek.tcl_call(teek.Color, 'tk_chooseColor', *_options(kwargs))
23 |
24 |
25 | def _messagebox(type, title, message, detail=None, **kwargs):
26 | kwargs['type'] = type
27 | kwargs['title'] = title
28 | kwargs['message'] = message
29 | if detail is not None:
30 | kwargs['detail'] = detail
31 |
32 | if type == 'ok':
33 | teek.tcl_call(None, 'tk_messageBox', *_options(kwargs))
34 | return None
35 | if type == 'okcancel':
36 | return teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) == 'ok'
37 | if type == 'retrycancel':
38 | return (
39 | teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) == 'retry')
40 | if type == 'yesno':
41 | return teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) == 'yes'
42 |
43 | # for anything else, return a string
44 | return teek.tcl_call(str, 'tk_messageBox', *_options(kwargs))
45 |
46 |
47 | info = partial(_messagebox, 'ok', icon='info')
48 | warning = partial(_messagebox, 'ok', icon='warning')
49 | error = partial(_messagebox, 'ok', icon='error')
50 | ok_cancel = partial(_messagebox, 'okcancel', icon='question')
51 | retry_cancel = partial(_messagebox, 'retrycancel', icon='warning')
52 | yes_no = partial(_messagebox, 'yesno', icon='question')
53 | yes_no_cancel = partial(_messagebox, 'yesnocancel', icon='question')
54 | abort_retry_ignore = partial(_messagebox, 'abortretryignore', icon='error')
55 |
56 |
57 | def _check_multiple(kwargs):
58 | if 'multiple' in kwargs:
59 | raise TypeError(
60 | "the 'multiple' option is not supported, use open_file() or "
61 | "open_multiple_files() depending on whether you want to support "
62 | "selecting multiple files at once")
63 |
64 |
65 | def open_file(**kwargs):
66 | """
67 | Ask the user to choose an existing file. Returns the path.
68 |
69 | This calls :man:`tk_getOpenFile(3tk)` without ``-multiple``. ``None`` is
70 | returned if the user cancels.
71 | """
72 | _check_multiple(kwargs)
73 | result = teek.tcl_call(str, 'tk_getOpenFile', *_options(kwargs))
74 | if not result:
75 | return None
76 | return os.path.abspath(result)
77 |
78 |
79 | def open_multiple_files(**kwargs):
80 | """
81 | Ask the user to choose one or more existing files. Returns a list of paths.
82 |
83 | This calls :man:`tk_getOpenFile(3tk)` with ``-multiple`` set to true. An
84 | empty list is returned if the user cancels.
85 | """
86 | _check_multiple(kwargs)
87 | result = teek.tcl_call(
88 | [str], 'tk_getOpenFile', '-multiple', True, *_options(kwargs))
89 | return list(map(os.path.abspath, result))
90 |
91 |
92 | def save_file(**kwargs):
93 | """Ask the user to choose a path for a new file. Return the path.
94 |
95 | This calls :man:`tk_getSaveFile(3tk)`, and returns ``None`` if the user
96 | cancels.
97 | """
98 | result = teek.tcl_call(str, 'tk_getSaveFile', *_options(kwargs))
99 | if not result:
100 | return None
101 | return os.path.abspath(result)
102 |
103 |
104 | def directory(**kwargs):
105 | """Asks the user to choose a directory, and return a path to it.
106 |
107 | This calls :man:`tk_chooseDirectory(3tk)`, and returns ``None`` if the user
108 | cancels.
109 |
110 | .. note::
111 | By default, the user can choose a directory that doesn't exist yet.
112 | This behaviour is documented in :man:`tk_chooseDirectory(3tk)`. If you
113 | want the user to choose an existing directory, use ``mustexist=True``.
114 | """
115 | result = teek.tcl_call(str, 'tk_chooseDirectory', *_options(kwargs))
116 | if not result:
117 | return None
118 | return os.path.abspath(result)
119 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import os
3 |
4 | import pytest
5 |
6 | import teek
7 |
8 |
9 | # https://docs.pytest.org/en/latest/doctest.html#the-doctest-namespace-fixture
10 | @pytest.fixture(autouse=True)
11 | def add_teek(doctest_namespace):
12 | doctest_namespace['teek'] = teek
13 |
14 |
15 | # the following url is on 2 lines because pep8 line length
16 | #
17 | # https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tes
18 | # ts-according-to-command-line-option
19 | def pytest_addoption(parser):
20 | parser.addoption(
21 | "--skipslow", action="store_true", default=False, help="run slow tests"
22 | )
23 |
24 |
25 | def pytest_cmdline_preparse(config, args):
26 | try:
27 | import teek.extras.image_loader
28 | except ImportError:
29 | import teek.extras
30 | path = os.path.join(teek.extras.__path__[0], 'image_loader.py')
31 | args.append('--ignore=' + path)
32 |
33 |
34 | def pytest_collection_modifyitems(config, items):
35 | if config.getoption("--skipslow"):
36 | skip_slow = pytest.mark.skip(reason="--skipslow was used")
37 | for item in items:
38 | if "slow" in item.keywords:
39 | item.add_marker(skip_slow)
40 |
41 |
42 | @pytest.fixture
43 | def deinit_threads():
44 | """Make sure that init_threads() has not been called when test completes.
45 |
46 | If you have a test like this...
47 |
48 | def test_tootie():
49 | teek.init_threads()
50 |
51 | ...the test will cause problems for any other tests that also call
52 | init_threads(), because it can't be called twice. Using this fixture in
53 | test_tootie() would fix that problem.
54 | """
55 | yield
56 | teek.after_idle(teek.quit)
57 | teek.run()
58 |
59 |
60 | @pytest.fixture
61 | def handy_callback():
62 | def handy_callback_decorator(function):
63 | def result(*args, **kwargs):
64 | return_value = function(*args, **kwargs)
65 | result.ran += 1
66 | return return_value
67 |
68 | result.ran = 0
69 | result.ran_once = (lambda: result.ran == 1)
70 | return result
71 |
72 | return handy_callback_decorator
73 |
74 |
75 | @pytest.fixture
76 | def check_config_types():
77 | def checker(config, debug_info, *, ignore_list=()):
78 | # this converts all values to their types, and this probably fails if
79 | # the types are wrong
80 | dict(config)
81 |
82 | complained = set()
83 |
84 | # were there keys that defaulted to str?
85 | known_keys = config._types.keys() | config._special.keys()
86 | for key in (config.keys() - known_keys - set(ignore_list)):
87 | print('\ncheck_config_types', debug_info, 'warning: type of',
88 | key, 'was guessed to be str')
89 | complained.add(key)
90 |
91 | return complained
92 |
93 | return checker
94 |
95 |
96 | # this is tested in test_widgets.py
97 | @pytest.fixture
98 | def all_widgets():
99 | window = teek.Window()
100 | return [
101 | teek.Button(window),
102 | teek.Canvas(window),
103 | teek.Checkbutton(window),
104 | teek.Combobox(window),
105 | teek.Entry(window),
106 | teek.Frame(window),
107 | teek.Label(window),
108 | teek.LabelFrame(window),
109 | teek.Notebook(window),
110 | teek.Menu(),
111 | teek.Progressbar(window),
112 | teek.Scrollbar(window),
113 | teek.Separator(window),
114 | teek.Spinbox(window),
115 | teek.Text(window),
116 | teek.Toplevel(),
117 | window,
118 | ]
119 |
120 |
121 | @pytest.fixture
122 | def fake_command():
123 | @contextlib.contextmanager
124 | def faker(name, return_value=None):
125 | called = []
126 |
127 | def command_func(*args):
128 | called.append(list(args))
129 | return return_value
130 |
131 | fake = teek.create_command(command_func, [], extra_args_type=str)
132 |
133 | teek.tcl_call(None, 'rename', name, name + '_real')
134 | try:
135 | teek.tcl_call(None, 'rename', fake, name)
136 | yield called
137 | finally:
138 | try:
139 | teek.delete_command(name)
140 | except teek.TclError:
141 | pass
142 | teek.tcl_call(None, 'rename', name + '_real', name)
143 |
144 | return faker
145 |
--------------------------------------------------------------------------------
/tests/widgets/test_canvas.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import teek
4 |
5 |
6 | def test_item_eq_hash_repr_fromtcl_totcl():
7 | canvas = teek.Canvas(teek.Window())
8 | rect = canvas.create_rectangle(100, 100, 200, 200)
9 | oval = canvas.create_oval(150, 150, 250, 250)
10 |
11 | assert repr(rect) == (
12 | '')
13 | assert repr(oval) == ''
14 |
15 | assert oval == oval
16 | assert rect != oval
17 | assert rect != 'lol wat'
18 | assert len({rect, oval}) == 2
19 |
20 | assert oval == canvas.Item.from_tcl(oval.to_tcl())
21 | assert hash(oval) == hash(canvas.Item.from_tcl(oval.to_tcl()))
22 |
23 | oval.delete()
24 | assert repr(oval) == ''
25 |
26 |
27 | def test_trying_2_create_item_directly():
28 | canvas = teek.Canvas(teek.Window())
29 | with pytest.raises(TypeError) as error:
30 | canvas.Item()
31 | assert str(error.value).startswith("don't create canvas.Item objects")
32 |
33 |
34 | def test_item_config_usage():
35 | canvas = teek.Canvas(teek.Window())
36 | rect = canvas.create_rectangle(100, 100, 200, 200, dash='-')
37 |
38 | assert rect.config['dash'] == '-'
39 | assert rect.config['fill'] is None
40 | rect.config['fill'] = 'blue'
41 | assert rect.config['fill'] == teek.Color('blue')
42 |
43 |
44 | def create_different_items(canvas):
45 | return [
46 | canvas.create_rectangle(100, 200, 300, 400),
47 | canvas.create_oval(100, 200, 300, 400),
48 | canvas.create_line(100, 200, 300, 400, 500, 600),
49 | ]
50 |
51 |
52 | def test_create_different_items_util_function():
53 | canvas = teek.Canvas(teek.Window())
54 | from_method_names = {name.split('_')[1] for name in dir(canvas)
55 | if name.startswith('create_')}
56 | from_util_func = {item.type_string
57 | for item in create_different_items(canvas)}
58 | assert from_method_names == from_util_func
59 |
60 |
61 | def test_config_types(check_config_types):
62 | canvas = teek.Canvas(teek.Window())
63 | check_config_types(canvas.config, 'Canvas')
64 |
65 | # it would be repeatitive to see the same warnings over and over again
66 | already_heard = set()
67 |
68 | for item in create_different_items(canvas):
69 | already_heard |= check_config_types(
70 | item.config, 'Canvas %s item' % item.type_string,
71 | ignore_list=already_heard)
72 |
73 |
74 | def test_coords():
75 | canvas = teek.Canvas(teek.Window())
76 | oval = canvas.create_oval(150, 150, 250, 250)
77 |
78 | assert oval.coords == (150, 150, 250, 250)
79 | oval.coords = (50, 50, 100, 100.123)
80 | assert oval.coords == (50, 50, 100, 100.123)
81 | assert repr(oval) == ''
82 |
83 |
84 | def test_tags():
85 | canvas = teek.Canvas(teek.Window())
86 | rect = canvas.create_rectangle(100, 100, 200, 200)
87 | oval = canvas.create_oval(150, 150, 250, 250)
88 |
89 | assert list(rect.tags) == []
90 | assert list(oval.tags) == []
91 | rect.tags.add('a')
92 | assert list(rect.tags) == ['a']
93 | assert list(oval.tags) == []
94 | rect.tags.add('a')
95 | assert list(rect.tags) == ['a']
96 | assert list(oval.tags) == []
97 | rect.tags.add('b')
98 | assert list(rect.tags) == ['a', 'b']
99 | assert list(oval.tags) == []
100 | rect.tags.discard('b')
101 | assert list(rect.tags) == ['a']
102 | assert list(oval.tags) == []
103 | rect.tags.discard('b')
104 | assert list(rect.tags) == ['a']
105 | assert list(oval.tags) == []
106 |
107 | assert 'a' in rect.tags
108 | assert 'b' not in rect.tags
109 |
110 |
111 | def test_find():
112 | canvas = teek.Canvas(teek.Window())
113 | rect = canvas.create_rectangle(150, 150, 200, 200)
114 | oval = canvas.create_oval(50, 50, 100, 100)
115 |
116 | assert canvas.find_closest(70, 70) == oval
117 | assert canvas.find_enclosed(40, 40, 110, 110) == [oval]
118 | assert canvas.find_overlapping(90, 90, 160, 160) == [rect, oval]
119 |
120 | assert rect.find_above() == oval
121 | assert oval.find_above() is None
122 | assert oval.find_below() == rect
123 | assert rect.find_below() is None
124 | assert canvas.find_all() == [rect, oval]
125 |
126 | rect.tags.add('asdf')
127 | assert canvas.find_withtag('asdf') == [rect]
128 | oval.tags.add('asdf')
129 | assert canvas.find_withtag('asdf') == [rect, oval]
130 |
--------------------------------------------------------------------------------
/tests/extras/test_soup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import urllib.request
4 |
5 | import pytest
6 | bs4 = pytest.importorskip('bs4') # noqa
7 | pytest.importorskip('lxml') # noqa
8 |
9 | import teek
10 | from teek.extras.soup import SoupViewer
11 | pytest.importorskip('teek.extras.image_loader')
12 |
13 |
14 | DATA_DIR = os.path.join(
15 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data')
16 | FIREFOX_SVG_URL = 'file://' + urllib.request.pathname2url(
17 | os.path.join(DATA_DIR, 'firefox.svg'))
18 |
19 | # from test_image_loader.py
20 | ignore_svglib_warnings = pytest.mark.filterwarnings(
21 | "ignore:The 'warn' method is deprecated")
22 |
23 | big_html = '''
24 | Header 1
25 | Header 2
26 | Header 3
27 | Header 4
28 | Header 5
29 | Header 6
30 |
31 | The below code uses the print function.
32 |
33 | print("Hello World")
34 |
35 | Line
break
36 |
37 | Italics 1Italics 2
38 | Bold 1Bold 2
39 |
40 |
42 | This contains
43 |
44 | many spaces
45 |
46 |
47 | - One
48 | - Two
49 | - Three
50 |
51 |
52 |
53 | - One
54 | - Two
55 | - Three
56 |
57 |
58 | Link
59 |
60 |
61 | '''.format(FIREFOX_SVG_URL=FIREFOX_SVG_URL)
62 |
63 |
64 | def get_tag_names(widget, string):
65 | # TODO: add search to teek
66 | start = teek.tcl_call(widget.TextIndex, widget, 'search', string, '1.0')
67 | end = start.forward(chars=len(string))
68 | tags = set(widget.get_all_tags(start))
69 |
70 | # must be same tags in the whole text
71 | index = start
72 | while index < end:
73 | assert set(widget.get_all_tags(index)) == tags
74 | index = index.forward(chars=1)
75 |
76 | return {tag.name[len('soup-'):] for tag in tags
77 | if tag.name.startswith('soup-')}
78 |
79 |
80 | def create_souped_widget(html, **kwargs):
81 | widget = teek.Text(teek.Window())
82 | souper = SoupViewer(widget, **kwargs)
83 | souper.create_tags()
84 | for element in bs4.BeautifulSoup(html, 'lxml').body:
85 | souper.add_soup(element)
86 |
87 | return (souper, widget)
88 |
89 |
90 | def test_tagging():
91 | souper, widget = create_souped_widget(big_html, threads=False)
92 |
93 | for h in '123456':
94 | assert get_tag_names(widget, 'Header ' + h) == {'h' + h}
95 | assert get_tag_names(widget, 'print') == {'p', 'code'}
96 | assert get_tag_names(widget, 'print(') == {'pre'}
97 | assert 'Line\nbreak' in widget.get()
98 | assert get_tag_names(widget, 'Italics 1') == {'i'}
99 | assert get_tag_names(widget, 'Italics 2') == {'em'}
100 | assert get_tag_names(widget, 'Bold 1') == {'b'}
101 | assert get_tag_names(widget, 'Bold 2') == {'strong'}
102 | assert 'This contains many spaces' in widget.get()
103 | assert get_tag_names(widget, '\N{bullet} One') == {'li', 'ul'}
104 | assert get_tag_names(widget, '1. One') == {'li', 'ol'}
105 | assert get_tag_names(widget, 'Link') == {'p', 'a'}
106 | assert get_tag_names(widget, 'firefox pic alt') == {'img'}
107 |
108 |
109 | @pytest.mark.slow
110 | def test_image_doesnt_load_without_threads():
111 | souper, widget = create_souped_widget(big_html, threads=False)
112 | assert 'firefox pic alt' in widget.get()
113 |
114 | end = time.time() + 0.5
115 | while time.time() < end:
116 | teek.update()
117 | assert 'firefox pic alt' in widget.get()
118 |
119 |
120 | @ignore_svglib_warnings
121 | @pytest.mark.slow
122 | def test_image_loads_with_threads(deinit_threads):
123 | teek.init_threads()
124 | souper, widget = create_souped_widget(big_html) # threads=True is default
125 |
126 | assert 'firefox pic alt' in widget.get()
127 | time.sleep(1) # this used to be 0.1 and it made tests fail randomly
128 | teek.update()
129 | assert 'firefox pic alt' not in widget.get()
130 |
131 |
132 | def test_unknown_tag():
133 | with pytest.warns(RuntimeWarning,
134 | match=(r'contains a