From 9e03714cedf02b6c9becae3d9216dfe0e7df8592 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 9 Jan 2025 18:56:08 -0500 Subject: [PATCH 01/53] Add Travertino verbatim (except for line-length fixes and removed git/changelog stuff) --- travertino/AUTHORS | 8 + travertino/CONTRIBUTING.md | 7 + travertino/LICENSE | 27 + travertino/README.rst | 98 +++ travertino/pyproject.toml | 82 ++ travertino/src/travertino/__init__.py | 17 + travertino/src/travertino/colors.py | 403 ++++++++++ travertino/src/travertino/constants.py | 264 +++++++ travertino/src/travertino/declaration.py | 458 +++++++++++ travertino/src/travertino/fonts.py | 199 +++++ travertino/src/travertino/layout.py | 170 ++++ travertino/src/travertino/node.py | 196 +++++ travertino/src/travertino/size.py | 68 ++ travertino/tests/__init__.py | 0 travertino/tests/colors/__init__.py | 0 travertino/tests/colors/test_constructor.py | 170 ++++ travertino/tests/colors/test_parsing.py | 171 ++++ travertino/tests/fonts/__init__.py | 0 travertino/tests/fonts/test_constructor.py | 203 +++++ travertino/tests/fonts/test_parsing.py | 133 ++++ travertino/tests/test_choices.py | 399 ++++++++++ travertino/tests/test_declaration.py | 836 ++++++++++++++++++++ travertino/tests/test_layout.py | 407 ++++++++++ travertino/tests/test_node.py | 467 +++++++++++ travertino/tests/test_size.py | 138 ++++ travertino/tests/utils.py | 29 + travertino/tox.ini | 41 + 27 files changed, 4991 insertions(+) create mode 100644 travertino/AUTHORS create mode 100644 travertino/CONTRIBUTING.md create mode 100644 travertino/LICENSE create mode 100644 travertino/README.rst create mode 100644 travertino/pyproject.toml create mode 100644 travertino/src/travertino/__init__.py create mode 100644 travertino/src/travertino/colors.py create mode 100644 travertino/src/travertino/constants.py create mode 100644 travertino/src/travertino/declaration.py create mode 100644 travertino/src/travertino/fonts.py create mode 100644 travertino/src/travertino/layout.py create mode 100644 travertino/src/travertino/node.py create mode 100644 travertino/src/travertino/size.py create mode 100644 travertino/tests/__init__.py create mode 100644 travertino/tests/colors/__init__.py create mode 100644 travertino/tests/colors/test_constructor.py create mode 100644 travertino/tests/colors/test_parsing.py create mode 100644 travertino/tests/fonts/__init__.py create mode 100644 travertino/tests/fonts/test_constructor.py create mode 100644 travertino/tests/fonts/test_parsing.py create mode 100644 travertino/tests/test_choices.py create mode 100644 travertino/tests/test_declaration.py create mode 100644 travertino/tests/test_layout.py create mode 100644 travertino/tests/test_node.py create mode 100644 travertino/tests/test_size.py create mode 100644 travertino/tests/utils.py create mode 100644 travertino/tox.ini diff --git a/travertino/AUTHORS b/travertino/AUTHORS new file mode 100644 index 0000000000..017082d0bc --- /dev/null +++ b/travertino/AUTHORS @@ -0,0 +1,8 @@ +Travertino was originally created in December 2017. + +The PRIMARY AUTHORS are (and/or have been): + Russell Keith-Magee + +And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- +people who have submitted patches, reported bugs, added translations, helped +answer newbie questions, and generally made Travertino that much better: diff --git a/travertino/CONTRIBUTING.md b/travertino/CONTRIBUTING.md new file mode 100644 index 0000000000..ec4f1c03a0 --- /dev/null +++ b/travertino/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing + +BeeWare <3's contributions! + +Please be aware, BeeWare operates under a Code of Conduct. + +See [CONTRIBUTING to BeeWare](https://beeware.org/contributing) for details. diff --git a/travertino/LICENSE b/travertino/LICENSE new file mode 100644 index 0000000000..d4e6b204aa --- /dev/null +++ b/travertino/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017 Russell Keith-Magee. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Travertino nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/travertino/README.rst b/travertino/README.rst new file mode 100644 index 0000000000..6187b7b8f3 --- /dev/null +++ b/travertino/README.rst @@ -0,0 +1,98 @@ +.. |logo| image:: https://beeware.org/static/images/defaultlogo.png + :width: 72px + :target: https://beeware.org + +.. |pyversions| image:: https://img.shields.io/pypi/pyversions/travertino.svg + :target: https://pypi.python.org/pypi/travertino + :alt: Python Versions + +.. |version| image:: https://img.shields.io/pypi/v/travertino.svg + :target: https://pypi.python.org/pypi/travertino + :alt: Project version + +.. |maturity| image:: https://img.shields.io/pypi/status/travertino.svg + :target: https://pypi.python.org/pypi/travertino + :alt: Project status + +.. |license| image:: https://img.shields.io/pypi/l/travertino.svg + :target: https://github.com/beeware/travertino/blob/main/LICENSE + :alt: BSD License + +.. |ci| image:: https://github.com/beeware/travertino/workflows/CI/badge.svg?branch=main + :target: https://github.com/beeware/travertino/actions + :alt: Build Status + +.. |social| image:: https://img.shields.io/discord/836455665257021440?label=Discord%20Chat&logo=discord&style=plastic + :target: https://beeware.org/bee/chat/ + :alt: Discord server + +|logo| + +Travertino +========== + +|pyversions| |version| |maturity| |license| |ci| |social| + +Travertino is a set of constants and utilities for describing user +interfaces, including: + +* colors +* directions +* text alignment +* sizes + +Usage +----- + +Install Travertino: + + $ pip install travertino + +Then in your python code, import and use it:: + + >>> from travertino.colors import color, rgb, + + # Define a new color as an RGB triple + >>> red = rgb(0xff, 0x00, 0x00) + + # Parse a color from a string + >>> color('#dead00') + rgb(0xde, 0xad, 0x00) + + # Reference a pre-defined color + >>> color('RebeccaPurple') + rgb(102, 51, 153) + + +Community +--------- + +Travertino is part of the `BeeWare suite`_. You can talk to the community through: + +* `@beeware@fosstodon.org on Mastodon `__ + +* `Discord `__ + +We foster a welcoming and respectful community as described in our +`BeeWare Community Code of Conduct`_. + +Contributing +------------ + +If you experience problems with Travertino, `log them on GitHub`_. If you +want to contribute code, please `fork the code`_ and `submit a pull request`_. + +Travertino uses `Pre-commit `__ and `TownCrier +`__ to help maintain code quality. For +details on how to use these tools as part of your development environment, see +the `Briefcase code contribution guide +`__. +Although that document is for a different project, the details about setting up +your development environment are the same. + +.. _BeeWare suite: https://beeware.org +.. _Read The Docs: https://travertino.readthedocs.io +.. _BeeWare Community Code of Conduct: https://beeware.org/community/behavior/ +.. _log them on Github: https://github.com/beeware/travertino/issues +.. _fork the code: https://github.com/beeware/travertino +.. _submit a pull request: https://github.com/beeware/travertino/pulls diff --git a/travertino/pyproject.toml b/travertino/pyproject.toml new file mode 100644 index 0000000000..55a918f88a --- /dev/null +++ b/travertino/pyproject.toml @@ -0,0 +1,82 @@ +[build-system] +requires = [ + "setuptools==75.6.0", + "setuptools_scm==8.1.0", +] +build-backend = "setuptools.build_meta" + +[project] +dynamic = ["version"] +name = "travertino" +description = "A set of constants and base classes for describing user interface layouts." +readme = "README.rst" +requires-python = ">= 3.9" +license.text = "New BSD" +authors = [ + {name="Russell Keith-Magee", email="russell@keith-magee.com"}, +] +maintainers = [ + {name="BeeWare Team", email="team@beeware.org"}, +] +keywords = [ + "css", + "box model", + "layout", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development", + "Topic :: Software Development :: User Interfaces", +] + +[project.optional-dependencies] +# Extras used by developers *of* Travertino are pinned to specific versions to +# ensure environment consistency. +dev = [ + "pre-commit == 4.0.1", + "pytest == 8.3.4", + "setuptools_scm == 8.1.0", + "tox == 4.23.2", +] + +[project.urls] +Homepage = "https://beeware.org/travertino" +Funding = "https://beeware.org/contributing/membership/" +# Documentation = "https://travertino.readthedocs.io/en/latest/" +Tracker = "https://github.com/beeware/travertino/issues" +Source = "https://github.com/beeware/travertino" + +[tool.isort] +profile = "black" +split_on_trailing_comma = true +combine_as_imports = true + +[tool.setuptools_scm] +# To enable SCM versioning, we need an empty tool configuration for setuptools_scm + +[tool.towncrier] +directory = "changes" +package = "travertino" +package_dir = "src" +filename = "CHANGELOG.rst" +title_format = "{version} ({project_date})" +issue_format = "`#{issue} `_" +template = "changes/template.rst" +type = [ + { directory = "feature", name = "Features", showcontent = true }, + { directory = "bugfix", name = "Bugfixes", showcontent = true }, + { directory = "removal", name = "Backward Incompatible Changes", showcontent = true }, + { directory = "doc", name = "Documentation", showcontent = true }, + { directory = "misc", name = "Misc", showcontent = false }, +] diff --git a/travertino/src/travertino/__init__.py b/travertino/src/travertino/__init__.py new file mode 100644 index 0000000000..ec61d4b3ba --- /dev/null +++ b/travertino/src/travertino/__init__.py @@ -0,0 +1,17 @@ +try: + # Read version from SCM metadata + # This will only exist in a development environment + from setuptools_scm import get_version + + # Excluded from coverage because a pure test environment (such as the one + # used by tox in CI) won't have setuptools_scm + __version__ = get_version("../..", relative_to=__file__) # pragma: no cover +except (ModuleNotFoundError, LookupError): + # If setuptools_scm isn't in the environment, the call to import will fail. + # If it *is* in the environment, but the code isn't a git checkout (e.g., + # it's been pip installed non-editable) the call to get_version() will fail. + # If either of these occurs, read version from the installer metadata. + + from importlib.metadata import version + + __version__ = version("travertino") diff --git a/travertino/src/travertino/colors.py b/travertino/src/travertino/colors.py new file mode 100644 index 0000000000..a6b91eebc4 --- /dev/null +++ b/travertino/src/travertino/colors.py @@ -0,0 +1,403 @@ +# flake8: NOQA: F405 +from .constants import * + + +class Color: + "A base class for all colorspace representations" + pass + + def __eq__(self, other): + try: + c1 = self.rgba + c2 = other.rgba + + return c1.r == c2.r and c1.g == c2.g and c1.b == c2.b and c1.a == c2.a + except AttributeError: + return False + + @classmethod + def _validate_between(cls, content_name, value, min_value, max_value): + if value < min_value or value > max_value: + raise ValueError( + "{} value should be between {}-{}. Got {}".format( + content_name, min_value, max_value, value + ) + ) + + @classmethod + def _validate_partial(cls, content_name, value): + cls._validate_between(content_name, value, 0, 1) + + @classmethod + def _validate_alpha(cls, value): + cls._validate_partial("alpha", value) + + +class rgba(Color): + "A representation of an RGBA color" + + def __init__(self, r, g, b, a): + self._validate_rgb("red", r) + self._validate_rgb("green", g) + self._validate_rgb("blue", b) + self._validate_alpha(a) + self.r = r + self.g = g + self.b = b + self.a = a + + def __hash__(self): + return hash(("RGBA-color", self.r, self.g, self.b, self.a)) + + def __repr__(self): + return f"rgba({self.r}, {self.g}, {self.b}, {self.a})" + + @classmethod + def _validate_rgb(cls, content_name, value): + cls._validate_between(content_name, value, 0, 255) + + @property + def rgba(self): + return self + + +class rgb(rgba): + "A representation of an RGB color" + + def __init__(self, r, g, b): + super().__init__(r, g, b, 1.0) + + def __repr__(self): + return f"rgb({self.r}, {self.g}, {self.b})" + + +class hsla(Color): + "A representation of an HSLA color" + + def __init__(self, h, s, l, a=1.0): + self._validate_between("hue", h, 0, 360) + self._validate_partial("saturation", s) + self._validate_partial("lightness", l) + self._validate_alpha(a) + self.h = h + self.s = s + self.l = l # NOQA; E741 + self.a = a + + def __hash__(self): + return hash(("HSLA-color", self.h, self.s, self.l, self.a)) + + def __repr__(self): + return f"hsla({self.h}, {self.s}, {self.l}, {self.a})" + + @property + def rgba(self): + c = (1.0 - abs(2.0 * self.l - 1.0)) * self.s + h = self.h / 60.0 + x = c * (1.0 - abs(h % 2 - 1.0)) + m = self.l - 0.5 * c + + if h < 1.0: + r, g, b = c + m, x + m, m + elif h < 2.0: + r, g, b = x + m, c + m, m + elif h < 3.0: + r, g, b = m, c + m, x + m + elif h < 4.0: + r, g, b = m, x + m, c + m + elif h < 5.0: + r, g, b = m, x + m, c + m + else: + r, g, b = c + m, m, x + m + + return rgba( + round(r * 0xFF), + round(g * 0xFF), + round(b * 0xFF), + self.a, + ) + + +class hsl(hsla): + "A representation of an HSL color" + + def __init__(self, h, s, l): + super().__init__(h, s, l, 1.0) + + def __repr__(self): + return f"hsl({self.h}, {self.s}, {self.l})" + + +def color(value): + """Parse a color from a value. + + Accepts: + * rgb() instances + * hsl() instances + * '#rgb' + * '#rgba' + * '#rrggbb' + * '#rrggbbaa' + * '#RGB' + * '#RGBA' + * '#RRGGBB' + * '#RRGGBBAA' + * 'rgb(0, 0, 0)' + * 'rgba(0, 0, 0, 0.0)' + * 'hsl(0, 0%, 0%)' + * 'hsla(0, 0%, 0%, 0.0)' + * A named color + """ + + if isinstance(value, Color): + return value + + elif isinstance(value, str): + if value[0] == "#": + if len(value) == 4: + return rgb( + r=int(value[1] + value[1], 16), + g=int(value[2] + value[2], 16), + b=int(value[3] + value[3], 16), + ) + elif len(value) == 5: + return rgba( + r=int(value[1] + value[1], 16), + g=int(value[2] + value[2], 16), + b=int(value[3] + value[3], 16), + a=int(value[4] + value[4], 16) / 0xFF, + ) + elif len(value) == 7: + return rgb( + r=int(value[1:3], 16), + g=int(value[3:5], 16), + b=int(value[5:7], 16), + ) + elif len(value) == 9: + return rgba( + r=int(value[1:3], 16), + g=int(value[3:5], 16), + b=int(value[5:7], 16), + a=int(value[7:9], 16) / 0xFF, + ) + elif value.startswith("rgba"): + try: + values = value[5:-1].split(",") + if len(values) == 4: + return rgba( + int(values[0]), + int(values[1]), + int(values[2]), + float( + values[3], + ), + ) + except ValueError: + pass + elif value.startswith("rgb"): + try: + values = value[4:-1].split(",") + if len(values) == 3: + return rgb( + int(values[0]), + int(values[1]), + int(values[2]), + ) + except ValueError: + pass + + elif value.startswith("hsla"): + try: + values = value[5:-1].split(",") + if len(values) == 4: + return hsla( + int(values[0]), + int(values[1].strip().rstrip("%")) / 100.0, + int(values[2].strip().rstrip("%")) / 100.0, + float(values[3]), + ) + except ValueError: + pass + + elif value.startswith("hsl"): + try: + values = value[4:-1].split(",") + if len(values) == 3: + return hsl( + int(values[0]), + int(values[1].strip().rstrip("%")) / 100.0, + int(values[2].strip().rstrip("%")) / 100.0, + ) + except ValueError: + pass + else: + try: + return NAMED_COLOR[value.lower()] + except KeyError: + pass + + raise ValueError("Unknown color %s" % value) + + +NAMED_COLOR = { + ALICEBLUE: rgb(0xF0, 0xF8, 0xFF), + ANTIQUEWHITE: rgb(0xFA, 0xEB, 0xD7), + AQUA: rgb(0x00, 0xFF, 0xFF), + AQUAMARINE: rgb(0x7F, 0xFF, 0xD4), + AZURE: rgb(0xF0, 0xFF, 0xFF), + BEIGE: rgb(0xF5, 0xF5, 0xDC), + BISQUE: rgb(0xFF, 0xE4, 0xC4), + BLACK: rgb(0x00, 0x00, 0x00), + BLANCHEDALMOND: rgb(0xFF, 0xEB, 0xCD), + BLUE: rgb(0x00, 0x00, 0xFF), + BLUEVIOLET: rgb(0x8A, 0x2B, 0xE2), + BROWN: rgb(0xA5, 0x2A, 0x2A), + BURLYWOOD: rgb(0xDE, 0xB8, 0x87), + CADETBLUE: rgb(0x5F, 0x9E, 0xA0), + CHARTREUSE: rgb(0x7F, 0xFF, 0x00), + CHOCOLATE: rgb(0xD2, 0x69, 0x1E), + CORAL: rgb(0xFF, 0x7F, 0x50), + CORNFLOWERBLUE: rgb(0x64, 0x95, 0xED), + CORNSILK: rgb(0xFF, 0xF8, 0xDC), + CRIMSON: rgb(0xDC, 0x14, 0x3C), + CYAN: rgb(0x00, 0xFF, 0xFF), + DARKBLUE: rgb(0x00, 0x00, 0x8B), + DARKCYAN: rgb(0x00, 0x8B, 0x8B), + DARKGOLDENROD: rgb(0xB8, 0x86, 0x0B), + DARKGRAY: rgb(0xA9, 0xA9, 0xA9), + DARKGREY: rgb(0xA9, 0xA9, 0xA9), + DARKGREEN: rgb(0x00, 0x64, 0x00), + DARKKHAKI: rgb(0xBD, 0xB7, 0x6B), + DARKMAGENTA: rgb(0x8B, 0x00, 0x8B), + DARKOLIVEGREEN: rgb(0x55, 0x6B, 0x2F), + DARKORANGE: rgb(0xFF, 0x8C, 0x00), + DARKORCHID: rgb(0x99, 0x32, 0xCC), + DARKRED: rgb(0x8B, 0x00, 0x00), + DARKSALMON: rgb(0xE9, 0x96, 0x7A), + DARKSEAGREEN: rgb(0x8F, 0xBC, 0x8F), + DARKSLATEBLUE: rgb(0x48, 0x3D, 0x8B), + DARKSLATEGRAY: rgb(0x2F, 0x4F, 0x4F), + DARKSLATEGREY: rgb(0x2F, 0x4F, 0x4F), + DARKTURQUOISE: rgb(0x00, 0xCE, 0xD1), + DARKVIOLET: rgb(0x94, 0x00, 0xD3), + DEEPPINK: rgb(0xFF, 0x14, 0x93), + DEEPSKYBLUE: rgb(0x00, 0xBF, 0xFF), + DIMGRAY: rgb(0x69, 0x69, 0x69), + DIMGREY: rgb(0x69, 0x69, 0x69), + DODGERBLUE: rgb(0x1E, 0x90, 0xFF), + FIREBRICK: rgb(0xB2, 0x22, 0x22), + FLORALWHITE: rgb(0xFF, 0xFA, 0xF0), + FORESTGREEN: rgb(0x22, 0x8B, 0x22), + FUCHSIA: rgb(0xFF, 0x00, 0xFF), + GAINSBORO: rgb(0xDC, 0xDC, 0xDC), + GHOSTWHITE: rgb(0xF8, 0xF8, 0xFF), + GOLD: rgb(0xFF, 0xD7, 0x00), + GOLDENROD: rgb(0xDA, 0xA5, 0x20), + GRAY: rgb(0x80, 0x80, 0x80), + GREY: rgb(0x80, 0x80, 0x80), + GREEN: rgb(0x00, 0x80, 0x00), + GREENYELLOW: rgb(0xAD, 0xFF, 0x2F), + HONEYDEW: rgb(0xF0, 0xFF, 0xF0), + HOTPINK: rgb(0xFF, 0x69, 0xB4), + INDIANRED: rgb(0xCD, 0x5C, 0x5C), + INDIGO: rgb(0x4B, 0x00, 0x82), + IVORY: rgb(0xFF, 0xFF, 0xF0), + KHAKI: rgb(0xF0, 0xE6, 0x8C), + LAVENDER: rgb(0xE6, 0xE6, 0xFA), + LAVENDERBLUSH: rgb(0xFF, 0xF0, 0xF5), + LAWNGREEN: rgb(0x7C, 0xFC, 0x00), + LEMONCHIFFON: rgb(0xFF, 0xFA, 0xCD), + LIGHTBLUE: rgb(0xAD, 0xD8, 0xE6), + LIGHTCORAL: rgb(0xF0, 0x80, 0x80), + LIGHTCYAN: rgb(0xE0, 0xFF, 0xFF), + LIGHTGOLDENRODYELLOW: rgb(0xFA, 0xFA, 0xD2), + LIGHTGRAY: rgb(0xD3, 0xD3, 0xD3), + LIGHTGREY: rgb(0xD3, 0xD3, 0xD3), + LIGHTGREEN: rgb(0x90, 0xEE, 0x90), + LIGHTPINK: rgb(0xFF, 0xB6, 0xC1), + LIGHTSALMON: rgb(0xFF, 0xA0, 0x7A), + LIGHTSEAGREEN: rgb(0x20, 0xB2, 0xAA), + LIGHTSKYBLUE: rgb(0x87, 0xCE, 0xFA), + LIGHTSLATEGRAY: rgb(0x77, 0x88, 0x99), + LIGHTSLATEGREY: rgb(0x77, 0x88, 0x99), + LIGHTSTEELBLUE: rgb(0xB0, 0xC4, 0xDE), + LIGHTYELLOW: rgb(0xFF, 0xFF, 0xE0), + LIME: rgb(0x00, 0xFF, 0x00), + LIMEGREEN: rgb(0x32, 0xCD, 0x32), + LINEN: rgb(0xFA, 0xF0, 0xE6), + MAGENTA: rgb(0xFF, 0x00, 0xFF), + MAROON: rgb(0x80, 0x00, 0x00), + MEDIUMAQUAMARINE: rgb(0x66, 0xCD, 0xAA), + MEDIUMBLUE: rgb(0x00, 0x00, 0xCD), + MEDIUMORCHID: rgb(0xBA, 0x55, 0xD3), + MEDIUMPURPLE: rgb(0x93, 0x70, 0xDB), + MEDIUMSEAGREEN: rgb(0x3C, 0xB3, 0x71), + MEDIUMSLATEBLUE: rgb(0x7B, 0x68, 0xEE), + MEDIUMSPRINGGREEN: rgb(0x00, 0xFA, 0x9A), + MEDIUMTURQUOISE: rgb(0x48, 0xD1, 0xCC), + MEDIUMVIOLETRED: rgb(0xC7, 0x15, 0x85), + MIDNIGHTBLUE: rgb(0x19, 0x19, 0x70), + MINTCREAM: rgb(0xF5, 0xFF, 0xFA), + MISTYROSE: rgb(0xFF, 0xE4, 0xE1), + MOCCASIN: rgb(0xFF, 0xE4, 0xB5), + NAVAJOWHITE: rgb(0xFF, 0xDE, 0xAD), + NAVY: rgb(0x00, 0x00, 0x80), + OLDLACE: rgb(0xFD, 0xF5, 0xE6), + OLIVE: rgb(0x80, 0x80, 0x00), + OLIVEDRAB: rgb(0x6B, 0x8E, 0x23), + ORANGE: rgb(0xFF, 0xA5, 0x00), + ORANGERED: rgb(0xFF, 0x45, 0x00), + ORCHID: rgb(0xDA, 0x70, 0xD6), + PALEGOLDENROD: rgb(0xEE, 0xE8, 0xAA), + PALEGREEN: rgb(0x98, 0xFB, 0x98), + PALETURQUOISE: rgb(0xAF, 0xEE, 0xEE), + PALEVIOLETRED: rgb(0xDB, 0x70, 0x93), + PAPAYAWHIP: rgb(0xFF, 0xEF, 0xD5), + PEACHPUFF: rgb(0xFF, 0xDA, 0xB9), + PERU: rgb(0xCD, 0x85, 0x3F), + PINK: rgb(0xFF, 0xC0, 0xCB), + PLUM: rgb(0xDD, 0xA0, 0xDD), + POWDERBLUE: rgb(0xB0, 0xE0, 0xE6), + PURPLE: rgb(0x80, 0x00, 0x80), + REBECCAPURPLE: rgb(0x66, 0x33, 0x99), + RED: rgb(0xFF, 0x00, 0x00), + ROSYBROWN: rgb(0xBC, 0x8F, 0x8F), + ROYALBLUE: rgb(0x41, 0x69, 0xE1), + SADDLEBROWN: rgb(0x8B, 0x45, 0x13), + SALMON: rgb(0xFA, 0x80, 0x72), + SANDYBROWN: rgb(0xF4, 0xA4, 0x60), + SEAGREEN: rgb(0x2E, 0x8B, 0x57), + SEASHELL: rgb(0xFF, 0xF5, 0xEE), + SIENNA: rgb(0xA0, 0x52, 0x2D), + SILVER: rgb(0xC0, 0xC0, 0xC0), + SKYBLUE: rgb(0x87, 0xCE, 0xEB), + SLATEBLUE: rgb(0x6A, 0x5A, 0xCD), + SLATEGRAY: rgb(0x70, 0x80, 0x90), + SLATEGREY: rgb(0x70, 0x80, 0x90), + SNOW: rgb(0xFF, 0xFA, 0xFA), + SPRINGGREEN: rgb(0x00, 0xFF, 0x7F), + STEELBLUE: rgb(0x46, 0x82, 0xB4), + TAN: rgb(0xD2, 0xB4, 0x8C), + TEAL: rgb(0x00, 0x80, 0x80), + THISTLE: rgb(0xD8, 0xBF, 0xD8), + TOMATO: rgb(0xFF, 0x63, 0x47), + TURQUOISE: rgb(0x40, 0xE0, 0xD0), + VIOLET: rgb(0xEE, 0x82, 0xEE), + WHEAT: rgb(0xF5, 0xDE, 0xB3), + WHITE: rgb(0xFF, 0xFF, 0xFF), + WHITESMOKE: rgb(0xF5, 0xF5, 0xF5), + YELLOW: rgb(0xFF, 0xFF, 0x00), + YELLOWGREEN: rgb(0x9A, 0xCD, 0x32), +} + + +__all__ = [ + "Color", + "rgba", + "rgb", + "hsla", + "hsl", + "color", + "NAMED_COLOR", + "TRANSPARENT", +] + [name.upper() for name in NAMED_COLOR.keys()] diff --git a/travertino/src/travertino/constants.py b/travertino/src/travertino/constants.py new file mode 100644 index 0000000000..5460c39279 --- /dev/null +++ b/travertino/src/travertino/constants.py @@ -0,0 +1,264 @@ +###################################################################### +# Common constants +###################################################################### + +NORMAL = "normal" +LEFT = "left" +RIGHT = "right" +TOP = "top" +BOTTOM = "bottom" +CENTER = "center" +START = "start" +END = "end" + +###################################################################### +# Direction +###################################################################### + +ROW = "row" +COLUMN = "column" + +###################################################################### +# Visibility +###################################################################### + +VISIBLE = "visible" +HIDDEN = "hidden" +NONE = "none" + +###################################################################### +# Text Justification +###################################################################### + +JUSTIFY = "justify" + +###################################################################### +# Text Direction +###################################################################### + +RTL = "rtl" +LTR = "ltr" + +###################################################################### +# Font family +###################################################################### + +SYSTEM = "system" +MESSAGE = "message" + +SERIF = "serif" +SANS_SERIF = "sans-serif" +CURSIVE = "cursive" +FANTASY = "fantasy" +MONOSPACE = "monospace" + +###################################################################### +# Font Styling +###################################################################### + +ITALIC = "italic" +OBLIQUE = "oblique" + +FONT_STYLES = {ITALIC, OBLIQUE} + +###################################################################### +# Font Variant +###################################################################### + +SMALL_CAPS = "small-caps" + +FONT_VARIANTS = {SMALL_CAPS} + +###################################################################### +# Font boldness +###################################################################### + +BOLD = "bold" + +FONT_WEIGHTS = {BOLD} + +###################################################################### +# Font Size +###################################################################### + +SYSTEM_DEFAULT_FONT_SIZE = -1 + +XX_SMALL = "xx-small" +X_SMALL = "x-small" +SMALL = "small" +MEDIUM = "medium" +LARGE = "large" +X_LARGE = "x-large" +XX_LARGE = "xx-large" +XXX_LARGE = "xxx-large" + +ABSOLUTE_FONT_SIZES = { + XX_SMALL, + X_SMALL, + SMALL, + MEDIUM, + LARGE, + X_LARGE, + XX_LARGE, + XXX_LARGE, +} + +LARGER = "larger" +SMALLER = "smaller" + +RELATIVE_FONT_SIZES = {LARGER, SMALLER} + +###################################################################### +# Colors +###################################################################### + +TRANSPARENT = "transparent" + +ALICEBLUE = "aliceblue" +ANTIQUEWHITE = "antiquewhite" +AQUA = "aqua" +AQUAMARINE = "aquamarine" +AZURE = "azure" +BEIGE = "beige" +BISQUE = "bisque" +BLACK = "black" +BLANCHEDALMOND = "blanchedalmond" +BLUE = "blue" +BLUEVIOLET = "blueviolet" +BROWN = "brown" +BURLYWOOD = "burlywood" +CADETBLUE = "cadetblue" +CHARTREUSE = "chartreuse" +CHOCOLATE = "chocolate" +CORAL = "coral" +CORNFLOWERBLUE = "cornflowerblue" +CORNSILK = "cornsilk" +CRIMSON = "crimson" +CYAN = "cyan" +DARKBLUE = "darkblue" +DARKCYAN = "darkcyan" +DARKGOLDENROD = "darkgoldenrod" +DARKGRAY = "darkgray" +DARKGREY = "darkgrey" +DARKGREEN = "darkgreen" +DARKKHAKI = "darkkhaki" +DARKMAGENTA = "darkmagenta" +DARKOLIVEGREEN = "darkolivegreen" +DARKORANGE = "darkorange" +DARKORCHID = "darkorchid" +DARKRED = "darkred" +DARKSALMON = "darksalmon" +DARKSEAGREEN = "darkseagreen" +DARKSLATEBLUE = "darkslateblue" +DARKSLATEGRAY = "darkslategray" +DARKSLATEGREY = "darkslategrey" +DARKTURQUOISE = "darkturquoise" +DARKVIOLET = "darkviolet" +DEEPPINK = "deeppink" +DEEPSKYBLUE = "deepskyblue" +DIMGRAY = "dimgray" +DIMGREY = "dimgrey" +DODGERBLUE = "dodgerblue" +FIREBRICK = "firebrick" +FLORALWHITE = "floralwhite" +FORESTGREEN = "forestgreen" +FUCHSIA = "fuchsia" +GAINSBORO = "gainsboro" +GHOSTWHITE = "ghostwhite" +GOLD = "gold" +GOLDENROD = "goldenrod" +GRAY = "gray" +GREY = "grey" +GREEN = "green" +GREENYELLOW = "greenyellow" +HONEYDEW = "honeydew" +HOTPINK = "hotpink" +INDIANRED = "indianred" +INDIGO = "indigo" +IVORY = "ivory" +KHAKI = "khaki" +LAVENDER = "lavender" +LAVENDERBLUSH = "lavenderblush" +LAWNGREEN = "lawngreen" +LEMONCHIFFON = "lemonchiffon" +LIGHTBLUE = "lightblue" +LIGHTCORAL = "lightcoral" +LIGHTCYAN = "lightcyan" +LIGHTGOLDENRODYELLOW = "lightgoldenrodyellow" +LIGHTGRAY = "lightgray" +LIGHTGREY = "lightgrey" +LIGHTGREEN = "lightgreen" +LIGHTPINK = "lightpink" +LIGHTSALMON = "lightsalmon" +LIGHTSEAGREEN = "lightseagreen" +LIGHTSKYBLUE = "lightskyblue" +LIGHTSLATEGRAY = "lightslategray" +LIGHTSLATEGREY = "lightslategrey" +LIGHTSTEELBLUE = "lightsteelblue" +LIGHTYELLOW = "lightyellow" +LIME = "lime" +LIMEGREEN = "limegreen" +LINEN = "linen" +MAGENTA = "magenta" +MAROON = "maroon" +MEDIUMAQUAMARINE = "mediumaquamarine" +MEDIUMBLUE = "mediumblue" +MEDIUMORCHID = "mediumorchid" +MEDIUMPURPLE = "mediumpurple" +MEDIUMSEAGREEN = "mediumseagreen" +MEDIUMSLATEBLUE = "mediumslateblue" +MEDIUMSPRINGGREEN = "mediumspringgreen" +MEDIUMTURQUOISE = "mediumturquoise" +MEDIUMVIOLETRED = "mediumvioletred" +MIDNIGHTBLUE = "midnightblue" +MINTCREAM = "mintcream" +MISTYROSE = "mistyrose" +MOCCASIN = "moccasin" +NAVAJOWHITE = "navajowhite" +NAVY = "navy" +OLDLACE = "oldlace" +OLIVE = "olive" +OLIVEDRAB = "olivedrab" +ORANGE = "orange" +ORANGERED = "orangered" +ORCHID = "orchid" +PALEGOLDENROD = "palegoldenrod" +PALEGREEN = "palegreen" +PALETURQUOISE = "paleturquoise" +PALEVIOLETRED = "palevioletred" +PAPAYAWHIP = "papayawhip" +PEACHPUFF = "peachpuff" +PERU = "peru" +PINK = "pink" +PLUM = "plum" +POWDERBLUE = "powderblue" +PURPLE = "purple" +REBECCAPURPLE = "rebeccapurple" +RED = "red" +ROSYBROWN = "rosybrown" +ROYALBLUE = "royalblue" +SADDLEBROWN = "saddlebrown" +SALMON = "salmon" +SANDYBROWN = "sandybrown" +SEAGREEN = "seagreen" +SEASHELL = "seashell" +SIENNA = "sienna" +SILVER = "silver" +SKYBLUE = "skyblue" +SLATEBLUE = "slateblue" +SLATEGRAY = "slategray" +SLATEGREY = "slategrey" +SNOW = "snow" +SPRINGGREEN = "springgreen" +STEELBLUE = "steelblue" +TAN = "tan" +TEAL = "teal" +THISTLE = "thistle" +TOMATO = "tomato" +TURQUOISE = "turquoise" +VIOLET = "violet" +WHEAT = "wheat" +WHITE = "white" +WHITESMOKE = "whitesmoke" +YELLOW = "yellow" +YELLOWGREEN = "yellowgreen" diff --git a/travertino/src/travertino/declaration.py b/travertino/src/travertino/declaration.py new file mode 100644 index 0000000000..c44a35a794 --- /dev/null +++ b/travertino/src/travertino/declaration.py @@ -0,0 +1,458 @@ +from collections import defaultdict +from collections.abc import Mapping, Sequence +from warnings import filterwarnings, warn + +from .colors import color +from .constants import BOTTOM, LEFT, RIGHT, TOP + +# Make sure deprecation warnings are shown by default +filterwarnings("default", category=DeprecationWarning) + + +class ImmutableList: + def __init__(self, iterable): + self._data = list(iterable) + + def __getitem__(self, index): + return self._data[index] + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __eq__(self, other): + return self._data == other + + def __str__(self): + return str(self._data) + + def __repr__(self): + return repr(self._data) + + +class Choices: + "A class to define allowable data types for a property" + + def __init__( + self, + *constants, + default=None, # DEPRECATED + string=False, + integer=False, + number=False, + color=False, + ): + if default is not None: + warn( + "The `default` argument to Choices.__init__ is deprecated. " + "Providing no initial value to a property using it is sufficient.", + DeprecationWarning, + stacklevel=2, + ) + + self.constants = set(constants) + + self.string = string + self.integer = integer + self.number = number + self.color = color + + self._options = sorted(str(c).lower().replace("_", "-") for c in self.constants) + if self.string: + self._options.append("") + if self.integer: + self._options.append("") + if self.number: + self._options.append("") + if self.color: + self._options.append("") + + def validate(self, value): + if self.string: + try: + return value.strip() + except AttributeError: + pass + if self.integer: + try: + return int(value) + except (ValueError, TypeError): + pass + if self.number: + try: + return float(value) + except (ValueError, TypeError): + pass + if self.color: + try: + return color(value) + except ValueError: + pass + for const in self.constants: + if value == const: + return const + + raise ValueError(f"{value!r} is not a valid value") + + def __str__(self): + return ", ".join(self._options) + + +class validated_property: + def __init__(self, choices, initial=None): + """Define a simple validated property attribute. + + :param choices: The available choices. + :param initial: The initial value for the property. + """ + self.choices = choices + self.initial = None + + try: + # If an initial value has been provided, it must be consistent with + # the choices specified. + if initial is not None: + self.initial = self.validate(initial) + except ValueError: + # Unfortunately, __set_name__ hasn't been called yet, so we don't know the + # property's name. + raise ValueError( + f"Invalid initial value {initial!r}. Available choices: {choices}" + ) + + def __set_name__(self, owner, name): + self.name = name + owner._BASE_PROPERTIES[owner].add(name) + owner._BASE_ALL_PROPERTIES[owner].add(name) + + def __get__(self, obj, objtype=None): + if obj is None: + return self + + value = getattr(obj, f"_{self.name}", None) + return self.initial if value is None else value + + def __set__(self, obj, value): + if value is self: + # This happens during autogenerated dataclass __init__ when no value is + # supplied. + return + + if value is None: + raise ValueError( + "Python `None` cannot be used as a style value; " + f"to reset a property, use del `style.{self.name}`." + ) + + value = self.validate(value) + + if value != getattr(obj, f"_{self.name}", self.initial): + setattr(obj, f"_{self.name}", value) + obj.apply(self.name, value) + + def __delete__(self, obj): + try: + delattr(obj, f"_{self.name}") + except AttributeError: + pass + else: + obj.apply(self.name, self.initial) + + @property + def _name_if_set(self, default=""): + return f" {self.name}" if hasattr(self, "name") else default + + def validate(self, value): + try: + return self.choices.validate(value) + except ValueError: + raise ValueError( + f"Invalid value {value!r} for property{self._name_if_set}; " + f"Valid values are: {self.choices}" + ) + + def is_set_on(self, obj): + return hasattr(obj, f"_{self.name}") + + +class list_property(validated_property): + def validate(self, value): + if isinstance(value, str): + value = [value] + elif not isinstance(value, Sequence): + raise TypeError( + f"Value for list property{self._name_if_set} must be a sequence." + ) + + if not value: + name = getattr(self, "name", "prop_name") + raise ValueError( + "List properties cannot be set to an empty sequence; " + f"to reset a property, use del `style.{name}`." + ) + + # This could be a comprehension, but then the error couldn't specify which value + # is at fault. + result = [] + for item in value: + try: + item = self.choices.validate(item) + except ValueError: + raise ValueError( + f"Invalid item value {item!r} for list " + f"property{self._name_if_set}; Valid values are: {self.choices}" + ) + result.append(item) + + return ImmutableList(result) + + +class directional_property: + DIRECTIONS = [TOP, RIGHT, BOTTOM, LEFT] + ASSIGNMENT_SCHEMES = { + # T R B L + 1: [0, 0, 0, 0], + 2: [0, 1, 0, 1], + 3: [0, 1, 2, 1], + 4: [0, 1, 2, 3], + } + + def __init__(self, name_format): + """Define a property that proxies for top/right/bottom/left alternatives. + + :param name_format: The format from which to generate subproperties. "{}" will + be replaced with "_top", etc. + """ + self.name_format = name_format + + def __set_name__(self, owner, name): + self.name = name + owner._BASE_ALL_PROPERTIES[owner].add(self.name) + + def format(self, direction): + return self.name_format.format(f"_{direction}") + + def __get__(self, obj, objtype=None): + if obj is None: + return self + + return tuple(obj[self.format(direction)] for direction in self.DIRECTIONS) + + def __set__(self, obj, value): + if value is self: + # This happens during autogenerated dataclass __init__ when no value is + # supplied. + return + + if not isinstance(value, tuple): + value = (value,) + + if order := self.ASSIGNMENT_SCHEMES.get(len(value)): + for direction, index in zip(self.DIRECTIONS, order): + obj[self.format(direction)] = value[index] + else: + raise ValueError( + f"Invalid value for '{self.name}'; value must be a number, or a 1-4 " + f"tuple." + ) + + def __delete__(self, obj): + for direction in self.DIRECTIONS: + del obj[self.format(direction)] + + def is_set_on(self, obj): + return any( + hasattr(obj, self.format(direction)) for direction in self.DIRECTIONS + ) + + +class BaseStyle: + """A base class for style declarations. + + Exposes a dict-like interface. Designed for subclasses to be decorated + with @dataclass(kw_only=True), which most IDEs should be able to interpret and + provide autocompletion of argument names. On Python < 3.10, init=False can be used + to still get the keyword-only behavior from the included __init__. + """ + + _BASE_PROPERTIES = defaultdict(set) + _BASE_ALL_PROPERTIES = defaultdict(set) + + def __init_subclass__(cls): + # Give the subclass a direct reference to its properties. + cls._PROPERTIES = cls._BASE_PROPERTIES[cls] + cls._ALL_PROPERTIES = cls._BASE_ALL_PROPERTIES[cls] + + # Fallback in case subclass isn't decorated as subclass (probably from using + # previous API) or for pre-3.10, before kw_only argument existed. + def __init__(self, **style): + self.update(**style) + + @property + def _applicator(self): + return getattr(self, "_assigned_applicator", None) + + @_applicator.setter + def _applicator(self, value): + self._assigned_applicator = value + + if value is not None: + try: + self.reapply() + # This is backwards compatibility for Toga, which (at least as of + # 0.4.8), assigns style and applicator before the widget's + # implementation is available. + except Exception: + warn( + "Failed to apply style when assigning applicator, or when " + "assigning a new style once applicator is present. Node should be " + "sufficiently initialized to apply its style before it is assigned " + "an applicator. This will be an exception in a future version.", + RuntimeWarning, + stacklevel=2, + ) + + ###################################################################### + # Interface that style declarations must define + ###################################################################### + + def apply(self, property, value): + raise NotImplementedError( + "Style must define an apply method" + ) # pragma: no cover + + def layout(self, viewport): + raise NotImplementedError( + "Style must define a layout method" + ) # pragma: no cover + + ###################################################################### + # Provide a dict-like interface + ###################################################################### + + def reapply(self): + for name in self._PROPERTIES: + self.apply(name, self[name]) + + def update(self, **styles): + "Set multiple styles on the style definition." + for name, value in styles.items(): + name = name.replace("-", "_") + if name not in self._ALL_PROPERTIES: + raise NameError(f"Unknown style {name}") + + self[name] = value + + def copy(self, applicator=None): + "Create a duplicate of this style declaration." + dup = self.__class__() + dup.update(**self) + + if applicator is not None: + warn( + "Providing an applicator to BaseStyle.copy() is deprecated. Set " + "applicator afterward on the returned copy.", + DeprecationWarning, + stacklevel=2, + ) + dup._applicator = applicator + + return dup + + def __getitem__(self, name): + name = name.replace("-", "_") + if name in self._ALL_PROPERTIES: + return getattr(self, name) + raise KeyError(name) + + def __setitem__(self, name, value): + name = name.replace("-", "_") + if name in self._ALL_PROPERTIES: + setattr(self, name, value) + else: + raise KeyError(name) + + def __delitem__(self, name): + name = name.replace("-", "_") + if name in self._ALL_PROPERTIES: + delattr(self, name) + else: + raise KeyError(name) + + def keys(self): + return {name for name in self._PROPERTIES if name in self} + + def items(self): + return [(name, self[name]) for name in self._PROPERTIES if name in self] + + def __len__(self): + return sum(1 for name in self._PROPERTIES if name in self) + + def __contains__(self, name): + return name in self._ALL_PROPERTIES and ( + getattr(self.__class__, name).is_set_on(self) + ) + + def __iter__(self): + yield from (name for name in self._PROPERTIES if name in self) + + def __or__(self, other): + if isinstance(other, BaseStyle): + if self.__class__ is not other.__class__: + return NotImplemented + elif not isinstance(other, Mapping): + return NotImplemented + + result = self.copy() + result.update(**other) + return result + + def __ior__(self, other): + if isinstance(other, BaseStyle): + if self.__class__ is not other.__class__: + return NotImplemented + elif not isinstance(other, Mapping): + return NotImplemented + + self.update(**other) + return self + + ###################################################################### + # Get the rendered form of the style declaration + ###################################################################### + def __str__(self): + return "; ".join( + f"{name.replace('_', '-')}: {value}" for name, value in sorted(self.items()) + ) + + ###################################################################### + # Backwards compatibility + ###################################################################### + + @classmethod + def validated_property(cls, name, choices, initial=None): + warn( + "Defining style properties with class methods is deprecated; use class " + "attributes instead.", + DeprecationWarning, + stacklevel=2, + ) + prop = validated_property(choices, initial) + setattr(cls, name, prop) + prop.__set_name__(cls, name) + + @classmethod + def directional_property(cls, name): + warn( + "Defining style properties with class methods is deprecated; use class " + "attributes instead.", + DeprecationWarning, + stacklevel=2, + ) + name_format = name % "{}" + name = name_format.format("") + prop = directional_property(name_format) + setattr(cls, name, prop) + prop.__set_name__(cls, name) diff --git a/travertino/src/travertino/fonts.py b/travertino/src/travertino/fonts.py new file mode 100644 index 0000000000..a707f887c7 --- /dev/null +++ b/travertino/src/travertino/fonts.py @@ -0,0 +1,199 @@ +from .constants import ( + BOLD, + FONT_STYLES, + FONT_VARIANTS, + FONT_WEIGHTS, + ITALIC, + NORMAL, + OBLIQUE, + SMALL_CAPS, + SYSTEM_DEFAULT_FONT_SIZE, +) + + +class Font: + def __init__(self, family, size, style=NORMAL, variant=NORMAL, weight=NORMAL): + if (family[0] == "'" and family[-1] == "'") or ( + family[0] == '"' and family[-1] == '"' + ): + self.family = family[1:-1] + else: + self.family = family + + try: + self.size = int(size) + except ValueError: + try: + if size.strip().endswith("pt"): + self.size = int(size[:-2]) + else: + raise ValueError(f"Invalid font size {size!r}") + except Exception: + raise ValueError(f"Invalid font size {size!r}") + self.style = style if style in FONT_STYLES else NORMAL + self.variant = variant if variant in FONT_VARIANTS else NORMAL + self.weight = weight if weight in FONT_WEIGHTS else NORMAL + + def __hash__(self): + return hash( + ("FONT", self.family, self.size, self.style, self.variant, self.weight) + ) + + def __repr__(self): + return "".format( + "" if self.style is NORMAL else (self.style + " "), + "" if self.variant is NORMAL else (self.variant + " "), + "" if self.weight is NORMAL else (self.weight + " "), + ( + "system default size" + if self.size == SYSTEM_DEFAULT_FONT_SIZE + else f"{self.size}pt" + ), + self.family, + ) + + def __eq__(self, other): + try: + return ( + self.family == other.family + and self.size == other.size + and self.style == other.style + and self.variant == other.variant + and self.weight == other.weight + ) + except AttributeError: + return False + + def normal_style(self): + "Generate a normal style version of this font" + return Font( + self.family, + self.size, + style=NORMAL, + variant=self.variant, + weight=self.weight, + ) + + def italic(self): + "Generate an italic version of this font" + return Font( + self.family, + self.size, + style=ITALIC, + variant=self.variant, + weight=self.weight, + ) + + def oblique(self): + "Generate an oblique version of this font" + return Font( + self.family, + self.size, + style=OBLIQUE, + variant=self.variant, + weight=self.weight, + ) + + def normal_variant(self): + "Generate a normal variant of this font" + return Font( + self.family, self.size, style=self.style, variant=NORMAL, weight=self.weight + ) + + def small_caps(self): + "Generate a small-caps variant of this font" + return Font( + self.family, + self.size, + style=self.style, + variant=SMALL_CAPS, + weight=self.weight, + ) + + def normal_weight(self): + "Generate a normal weight version of this font" + return Font( + self.family, + self.size, + style=self.style, + variant=self.variant, + weight=NORMAL, + ) + + def bold(self): + "Generate a bold version of this font" + return Font( + self.family, self.size, style=self.style, variant=self.variant, weight=BOLD + ) + + +def font(value): + """Parse a font from a string. + + Accepts: + * Font instances + + style: normal / italic / oblique + variant: normal / small-caps + weight: normal / bold + + style variant weight size family + variant weight size family + weight size family + size family + """ + + if isinstance(value, Font): + return value + + elif isinstance(value, str): + parts = value.split(" ") + + style = None + variant = None + weight = None + size = None + + while size is None: + part = parts.pop(0) + if part == NORMAL: + if style is None: + style = NORMAL + elif variant is None: + variant = NORMAL + elif weight is None: + weight = NORMAL + elif part in FONT_STYLES: + if style is not None: + raise ValueError(f"Invalid font declaration '{value}'") + style = part + elif part in FONT_VARIANTS: + if variant is not None: + raise ValueError(f"Invalid font declaration '{value}'") + if style is None: + style = NORMAL + variant = part + elif part in FONT_WEIGHTS: + if weight is not None: + raise ValueError(f"Invalid font declaration '{value}'") + if style is None: + style = NORMAL + if variant is None: + variant = NORMAL + weight = part + else: + try: + if part.endswith("pt"): + size = int(part[:-2]) + else: + size = int(part) + except ValueError: + raise ValueError(f"Invalid size in font declaration '{value}'") + + if parts[0] == "pt": + parts.pop(0) + + family = " ".join(parts) + return Font(family, size, style=style, variant=variant, weight=weight) + + raise ValueError("Unknown font '%s'" % value) diff --git a/travertino/src/travertino/layout.py b/travertino/src/travertino/layout.py new file mode 100644 index 0000000000..87e5d3ceca --- /dev/null +++ b/travertino/src/travertino/layout.py @@ -0,0 +1,170 @@ +class Viewport: + """ + A viewport is a description of surface onto which content will be + rendered. It stores the size of the surface(in pixels), plus the + pixel density of the viewport. + """ + + def __init__(self, width=0, height=0, dpi=None): + self.width = width + self.height = height + self.dpi = dpi + + +class BaseBox: + """Describe the layout of a box displaying a node. + + Stored properties + ~~~~~~~~~~~~~~~~~ + visible: The node is included in rendering, and is visible. A value of + False indicates the node takes up space, but is not rendered. + + content_width: The width of the content in the box + content_height: The height of the content in the box + content_top: The top position of the content in the box, relative to the box + content_left: The left position of the content in the box, relative to the box + content_bottom: The distance from the bottom of the content to the bottom of the box + content_right: The distance from the right of the content to the right of the box + + origin_top: The absolute position of the top of the box + origin_left: The absolute position of the left of the box + + Computed properties + ~~~~~~~~~~~~~~~~~~~ + width: The overall width of the box + height: The overall height of the box + + absolute_content_top: The absolute position of the top of the content box. + absolute_content_left: The absolute position of the left of the content box. + absolute_content_bottom: The absolute position of the bottom of the content box. + absolute_content_right: The absolute position of the right of the content box. + + """ + + def __init__(self, node): + self.node = node + self._reset() + + def __repr__(self): + return "<{} ({}x{} @ {},{})>".format( + self.__class__.__name__, + self.content_width, + self.content_height, + self.absolute_content_left, + self.absolute_content_top, + ) + + def _reset(self): + # Some properties describing whether this node exists in + # layout *at all*. + self.visible = True + + # Minimum width and height of the content box. + self.min_content_width = 0 + self.min_content_height = 0 + + # Width and height of the content box. + self.content_width = 0 + self.content_height = 0 + + # Box position, relative to the containing box + self._content_top = 0 + self._content_left = 0 + self.content_bottom = 0 + self.content_right = 0 + + self.__origin_top = 0 + self.__origin_left = 0 + + # Set the origin via properties; this forces the calculation of + # absolute positions. + self._origin_top = 0 + self._origin_left = 0 + + ###################################################################### + # Origin handling + ###################################################################### + @property + def _origin_top(self): + return self.__origin_top + + @_origin_top.setter + def _origin_top(self, value): + if value != self.__origin_top: + self.__origin_top = value + for child in self.node.children: + if child.layout: + child.layout._origin_top = self.absolute_content_top + + @property + def _origin_left(self): + return self.__origin_left + + @_origin_left.setter + def _origin_left(self, value): + if value != self.__origin_left: + self.__origin_left = value + for child in self.node.children: + if child.layout: + child.layout._origin_left = self.absolute_content_left + + @property + def width(self): + return self._content_left + self.content_width + self.content_right + + @property + def min_width(self): + return self._content_left + self.min_content_width + self.content_right + + @property + def height(self): + return self._content_top + self.content_height + self.content_bottom + + @property + def min_height(self): + return self._content_top + self.min_content_height + self.content_bottom + + ###################################################################### + # Content box properties + ###################################################################### + @property + def content_top(self): + return self._content_top + + @content_top.setter + def content_top(self, value): + self._content_top = value + for child in self.node.children: + if child.layout: + child.layout._origin_top = self.absolute_content_top + + @property + def content_left(self): + return self._content_left + + @content_left.setter + def content_left(self, value): + self._content_left = value + for child in self.node.children: + if child.layout: + child.layout._origin_left = self.absolute_content_left + + ###################################################################### + # Absolute content box position + ###################################################################### + + @property + def absolute_content_top(self): + return self.__origin_top + self._content_top + + @property + def absolute_content_right(self): + return self.__origin_left + self._content_left + self.content_width + + @property + def absolute_content_bottom(self): + return self.__origin_top + self._content_top + self.content_height + + @property + def absolute_content_left(self): + return self.__origin_left + self._content_left diff --git a/travertino/src/travertino/node.py b/travertino/src/travertino/node.py new file mode 100644 index 0000000000..d95cee6364 --- /dev/null +++ b/travertino/src/travertino/node.py @@ -0,0 +1,196 @@ +class Node: + def __init__(self, style, applicator=None, children=None): + # Parent needs to be primed before style is (potentially) applied with + # assignment of applicator. + self._parent = None + self._root = None + + # Explicitly set the internal attribute first, since the setter for style will + # access the applicator property. + self._applicator = None + + self.style = style + self.applicator = applicator + + if children is None: + self._children = None + else: + self._children = [] + for child in children: + self.add(child) + + @property + def style(self): + """The node's style. + + Assigning a style triggers an application of that style if an applicator has + already been assigned. + """ + return self._style + + @style.setter + def style(self, style): + self._style = style.copy() + self.intrinsic = self.style.IntrinsicSize() + self.layout = self.style.Box(self) + + if self.applicator: + self.style._applicator = self.applicator + + @property + def applicator(self): + """This node's applicator, which handles applying the style. + + Assigning an applicator triggers an application of the node's style. + """ + return self._applicator + + @applicator.setter + def applicator(self, applicator): + if self.applicator: + # If an existing applicator is present, clear its reference to this node. + self.applicator.node = None + + if applicator: + # This needs to happen *before* assigning the applicator to the style, + # below, because as part of receiving the applicator, the style will + # reapply itself. How this happens will vary with applicator + # implementation, but will probably need access to the node. + applicator.node = self + + self._applicator = applicator + # This triggers style.reapply(): + self.style._applicator = applicator + + @property + def root(self): + """The root of the tree containing this node. + + Returns: + The root node. Returns self if this node *is* the root node. + """ + return self._root if self._root else self + + @property + def parent(self): + """The parent of this node. + + Returns: + The parent of this node. Returns None if this node is the root node. + """ + return self._parent + + @property + def children(self): + """The children of this node. + This *always* returns a list, even if the node is a leaf + and cannot have children. + + Returns: + A list of the children for this widget. + """ + if self._children is None: + return [] + else: + return self._children + + @property + def can_have_children(self): + """Determine if the node can have children. + + This does not resolve whether there actually *are* any children; + it only confirms whether children are theoretically allowed. + """ + return self._children is not None + + def add(self, child): + """Add a node as a child of this one. + Args: + child: A node to add as a child to this node. + + Raises: + ValueError: If this node is a leaf, and cannot have children. + """ + if self._children is None: + raise ValueError("Cannot add children") + + self._children.append(child) + child._parent = self + self._set_root(child, self.root) + + def insert(self, index, child): + """Insert a node as a child of this one. + Args: + index: Index of child position. + child: A node to insert as a child to this node. + + Raises: + ValueError: If this node is a leaf, and cannot have children. + """ + if self._children is None: + raise ValueError("Cannot insert child") + + self._children.insert(index, child) + child._parent = self + self._set_root(child, self.root) + + def remove(self, child): + """Remove child from this node. + Args: + child: The child to remove from this node. + + Raises: + ValueError: If this node is a leaf, and cannot have children. + """ + if self._children is None: + raise ValueError("Cannot remove children") + + self._children.remove(child) + child._parent = None + self._set_root(child, None) + + def clear(self): + """Clear all children from this node. + + Raises: + ValueError: If this node is a leaf, and cannot have children. + """ + if self._children is None: + # This is a leaf, so do nothing. + return + + for child in self._children: + child._parent = None + self._set_root(child, None) + self._children = [] + + def refresh(self, viewport): + """Refresh the layout and appearance of the tree this node is contained in.""" + if self._root: + self._root.refresh(viewport) + else: + if self.applicator: + + ###################################################################### + # 2024-12: Backwards compatibility for Toga <= 0.4.8 + ###################################################################### + # Accommodate the earlier signature of layout(), which included the node + # as a parameter. + try: + self.style.layout(viewport) + except TypeError as error: + if "layout() missing 1 required positional argument:" in str(error): + self.style.layout(self, viewport) + else: + raise + ###################################################################### + # End backwards compatibility + ###################################################################### + + self.applicator.set_bounds() + + def _set_root(self, node, root): + # Propagate a root node change through a tree. + node._root = root + for child in node.children: + self._set_root(child, root) diff --git a/travertino/src/travertino/size.py b/travertino/src/travertino/size.py new file mode 100644 index 0000000000..1104386a17 --- /dev/null +++ b/travertino/src/travertino/size.py @@ -0,0 +1,68 @@ +class at_least: + "An annotation to wrap around a value to describe that it is a minimum bound" + + def __init__(self, value): + self.value = value + + def __repr__(self): + return f"at least {self.value}" + + def __eq__(self, other): + try: + return self.value == other.value + except AttributeError: + return False + + +class BaseIntrinsicSize: + """Representation of the intrinsic size of an object. + + width: The width of the node. + height: The height of the node. + ratio: The height between height and width. width = height * ratio + """ + + def __init__(self, width=None, height=None, ratio=None, layout=None): + self._layout = layout + self._width = width + self._height = height + + self._ratio = None + + def __repr__(self): + return f"({self.width}, {self.height})" + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + if self._width != value: + self._width = value + + if self._layout: + self._layout.dirty(intrinsic_width=value) + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + if self._height != value: + self._height = value + + if self._layout: + self._layout.dirty(intrinsic_height=value) + + @property + def ratio(self): + return self._ratio + + @ratio.setter + def ratio(self, value): + if self._ratio != value: + self._ratio = value + if self._layout: + self._layout.dirty(intrinsic_ratio=value) diff --git a/travertino/tests/__init__.py b/travertino/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/travertino/tests/colors/__init__.py b/travertino/tests/colors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/travertino/tests/colors/test_constructor.py b/travertino/tests/colors/test_constructor.py new file mode 100644 index 0000000000..7b5ea1e2db --- /dev/null +++ b/travertino/tests/colors/test_constructor.py @@ -0,0 +1,170 @@ +import pytest + +from travertino.colors import hsl, hsla, rgb, rgba + + +def assert_equal_color(actual, expected): + assert actual.rgba.r == expected.rgba.r + assert actual.rgba.g == expected.rgba.g + assert actual.rgba.b == expected.rgba.b + assert actual.rgba.a == expected.rgba.a + + +@pytest.mark.parametrize( + "constructor, value, string", + [ + (rgb, (10, 20, 30), "rgb(10, 20, 30)"), + (rgba, (10, 20, 30, 0.5), "rgba(10, 20, 30, 0.5)"), + (hsl, (10, 0.2, 0.3), "hsl(10, 0.2, 0.3)"), + (hsla, (10, 0.2, 0.3, 0.5), "hsla(10, 0.2, 0.3, 0.5)"), + ], +) +def test_repr(constructor, value, string): + assert repr(constructor(*value)) == string + + +def test_rgb_hash(): + assert hash(rgb(10, 20, 30)) == hash(rgb(10, 20, 30)) + assert hash(rgb(10, 20, 30)) != hash(rgb(30, 20, 10)) + + +def test_rgba_hash(): + assert hash(rgba(10, 20, 30, 0.5)) == hash(rgba(10, 20, 30, 0.5)) + assert hash(rgba(10, 20, 30, 1.0)) == hash(rgb(10, 20, 30)) + assert hash(rgb(10, 20, 30)) != hash(rgb(30, 20, 10)) + + +def test_hsl_hash(): + assert hash(hsl(10, 0.2, 0.3)) == hash(hsl(10, 0.2, 0.3)) + assert hash(hsl(10, 0.3, 0.2)) != hash(hsl(10, 0.2, 0.3)) + + +def test_hsla_hash(): + assert hash(hsla(10, 0.2, 0.3, 0.5)) == hash(hsla(10, 0.2, 0.3, 0.5)) + assert hash(hsla(10, 0.2, 0.3, 1.0)) == hash(hsl(10, 0.2, 0.3)) + assert hash(hsla(10, 0.3, 0.2, 0.5)) != hash(hsla(10, 0.2, 0.3, 0.5)) + assert hash(hsla(10, 0, 0, 0.5)) != hash(rgba(10, 0, 0, 0.5)) + + +@pytest.mark.parametrize( + "value, expected", + [ + # Blacks + ((0, 0.0, 0.0), (0x00, 0x00, 0x00)), + ((60, 0.0, 0.0), (0x00, 0x00, 0x00)), + ((180, 0.0, 0.0), (0x00, 0x00, 0x00)), + ((240, 0.0, 0.0), (0x00, 0x00, 0x00)), + ((360, 0.0, 0.0), (0x00, 0x00, 0x00)), + # Whites + ((0, 0.0, 1.0), (0xFF, 0xFF, 0xFF)), + ((60, 0.0, 1.0), (0xFF, 0xFF, 0xFF)), + ((180, 0.0, 1.0), (0xFF, 0xFF, 0xFF)), + ((240, 0.0, 1.0), (0xFF, 0xFF, 0xFF)), + ((360, 0.0, 1.0), (0xFF, 0xFF, 0xFF)), + # Grays + ((0, 0.0, 0.2), (0x33, 0x33, 0x33)), + ((0, 0.0, 0.4), (0x66, 0x66, 0x66)), + ((0, 0.0, 0.5), (0x80, 0x80, 0x80)), + ((0, 0.0, 0.6), (0x99, 0x99, 0x99)), + ((0, 0.0, 0.8), (0xCC, 0xCC, 0xCC)), + # Primaries + ((0, 1.0, 0.5), (0xFF, 0x00, 0x00)), + ((60, 1.0, 0.5), (0xFF, 0xFF, 0x00)), + ((120, 1.0, 0.5), (0x00, 0xFF, 0x00)), + ((180, 1.0, 0.5), (0x00, 0xFF, 0xFF)), + ((240, 1.0, 0.5), (0x00, 0x00, 0xFF)), + ((300, 1.0, 0.5), (0xFF, 0x00, 0xFF)), + ((360, 1.0, 0.5), (0xFF, 0x00, 0x00)), + # Muted + ((0, 0.25, 0.25), (0x50, 0x30, 0x30)), + ((60, 0.25, 0.25), (0x50, 0x50, 0x30)), + ((120, 0.25, 0.25), (0x30, 0x50, 0x30)), + ((180, 0.25, 0.25), (0x30, 0x50, 0x50)), + ((240, 0.25, 0.25), (0x30, 0x30, 0x50)), + ((300, 0.25, 0.25), (0x50, 0x30, 0x50)), + ((360, 0.25, 0.25), (0x50, 0x30, 0x30)), + ((0, 0.25, 0.75), (0xCF, 0xAF, 0xAF)), + ((60, 0.25, 0.75), (0xCF, 0xCF, 0xAF)), + ((120, 0.25, 0.75), (0xAF, 0xCF, 0xAF)), + ((180, 0.25, 0.75), (0xAF, 0xCF, 0xCF)), + ((240, 0.25, 0.75), (0xAF, 0xAF, 0xCF)), + ((300, 0.25, 0.75), (0xCF, 0xAF, 0xCF)), + ((360, 0.25, 0.75), (0xCF, 0xAF, 0xAF)), + ((0, 0.75, 0.75), (0xEF, 0x8F, 0x8F)), + ((60, 0.75, 0.75), (0xEF, 0xEF, 0x8F)), + ((120, 0.75, 0.75), (0x8F, 0xEF, 0x8F)), + ((180, 0.75, 0.75), (0x8F, 0xEF, 0xEF)), + ((240, 0.75, 0.75), (0x8F, 0x8F, 0xEF)), + ((300, 0.75, 0.75), (0xEF, 0x8F, 0xEF)), + ((360, 0.75, 0.75), (0xEF, 0x8F, 0x8F)), + ((0, 0.75, 0.25), (0x70, 0x10, 0x10)), + ((60, 0.75, 0.25), (0x70, 0x70, 0x10)), + ((120, 0.75, 0.25), (0x10, 0x70, 0x10)), + ((180, 0.75, 0.25), (0x10, 0x70, 0x70)), + ((240, 0.75, 0.25), (0x10, 0x10, 0x70)), + ((300, 0.75, 0.25), (0x70, 0x10, 0x70)), + ((360, 0.75, 0.25), (0x70, 0x10, 0x10)), + ], +) +def test_hsl(value, expected): + assert_equal_color(hsl(*value), rgb(*expected)) + + +@pytest.mark.parametrize( + "value, expected", + [ + ((60, 0.0, 0.0, 0.3), (0x00, 0x00, 0x00, 0.3)), + ((60, 0.0, 1.0, 0.3), (0xFF, 0xFF, 0xFF, 0.3)), + ((60, 1.0, 0.5, 0.3), (0xFF, 0xFF, 0x00, 0.3)), + ((60, 0.25, 0.25, 0.3), (0x50, 0x50, 0x30, 0.3)), + ((60, 0.25, 0.75, 0.3), (0xCF, 0xCF, 0xAF, 0.3)), + ((60, 0.75, 0.75, 0.3), (0xEF, 0xEF, 0x8F, 0.3)), + ((60, 0.75, 0.25, 0.3), (0x70, 0x70, 0x10, 0.3)), + ], +) +def test_hsl_alpha(value, expected): + assert_equal_color(hsla(*value), rgba(*expected)) + + +@pytest.mark.parametrize( + "constructor, value, name, min, max, actual", + [ + (rgb, (-1, 120, 10), "red", 0, 255, -1), + (rgb, (256, 120, 10), "red", 0, 255, 256), + (rgb, (120, -1, 10), "green", 0, 255, -1), + (rgb, (120, 256, 10), "green", 0, 255, 256), + (rgb, (120, 10, -1), "blue", 0, 255, -1), + (rgb, (120, 10, 256), "blue", 0, 255, 256), + # + (rgba, (-1, 120, 10, 0.5), "red", 0, 255, -1), + (rgba, (256, 120, 10, 0.5), "red", 0, 255, 256), + (rgba, (120, -1, 10, 0.5), "green", 0, 255, -1), + (rgba, (120, 256, 10, 0.5), "green", 0, 255, 256), + (rgba, (120, 10, -1, 0.5), "blue", 0, 255, -1), + (rgba, (120, 10, 256, 0.5), "blue", 0, 255, 256), + (rgba, (120, 10, 60, -0.5), "alpha", 0, 1, -0.5), + (rgba, (120, 10, 60, 1.1), "alpha", 0, 1, 1.1), + # + (hsl, (-1, 0.5, 0.8), "hue", 0, 360, -1), + (hsl, (361, 0.5, 0.8), "hue", 0, 360, 361), + (hsl, (120, -0.1, 0.8), "saturation", 0, 1, -0.1), + (hsl, (120, 1.1, 0.8), "saturation", 0, 1, 1.1), + (hsl, (120, 0.8, -0.1), "lightness", 0, 1, -0.1), + (hsl, (120, 0.8, 1.1), "lightness", 0, 1, 1.1), + # + (hsla, (-1, 0.5, 0.8, 0.5), "hue", 0, 360, -1), + (hsla, (361, 0.5, 0.8, 0.5), "hue", 0, 360, 361), + (hsla, (120, -0.1, 0.8, 0.5), "saturation", 0, 1, -0.1), + (hsla, (120, 1.1, 0.8, 0.5), "saturation", 0, 1, 1.1), + (hsla, (120, 0.8, -0.1, 0.5), "lightness", 0, 1, -0.1), + (hsla, (120, 0.8, 1.1, 0.5), "lightness", 0, 1, 1.1), + (hsla, (120, 0.8, 0.5, -0.1), "alpha", 0, 1, -0.1), + (hsla, (120, 0.8, 0.5, 1.1), "alpha", 0, 1, 1.1), + ], +) +def test_invalid_color_constructor(constructor, value, name, min, max, actual): + with pytest.raises( + ValueError, + match=rf"^{name} value should be between {min}-{max}\. Got {actual}$", + ): + constructor(*value) diff --git a/travertino/tests/colors/test_parsing.py b/travertino/tests/colors/test_parsing.py new file mode 100644 index 0000000000..21c18eb0df --- /dev/null +++ b/travertino/tests/colors/test_parsing.py @@ -0,0 +1,171 @@ +import pytest + +from travertino.colors import color, hsl, hsla, rgb, rgba + + +def assert_equal_hsl(value, expected): + # Nothing fancy - a color is equal if the attributes are all the same + actual = color(value) + assert actual.h == expected.h + assert actual.s == expected.s + assert actual.l == expected.l + assert actual.a == pytest.approx(expected.a, abs=0.001) + + +def assert_equal_rgb(value, expected): + # Nothing fancy - a color is equal if the attributes are all the same + actual = color(value) + assert actual.r == expected.r + assert actual.g == expected.g + assert actual.b == expected.b + assert actual.a == pytest.approx(expected.a, abs=0.001) + + +def test_noop(): + assert_equal_rgb(rgba(1, 2, 3, 0.5), rgba(1, 2, 3, 0.5)) + assert_equal_hsl(hsl(1, 0.2, 0.3), hsl(1, 0.2, 0.3)) + + +@pytest.mark.parametrize( + "value, expected", + [ + ("rgb(1,2,3)", (1, 2, 3)), + ("rgb(1, 2, 3)", (1, 2, 3)), + ("rgb( 1 , 2 , 3)", (1, 2, 3)), + ("#123", (0x11, 0x22, 0x33)), + ("#112233", (0x11, 0x22, 0x33)), + ("#abc", (0xAA, 0xBB, 0xCC)), + ("#ABC", (0xAA, 0xBB, 0xCC)), + ("#abcdef", (0xAB, 0xCD, 0xEF)), + ("#ABCDEF", (0xAB, 0xCD, 0xEF)), + ], +) +def test_rgb(value, expected): + assert_equal_rgb(value, rgb(*expected)) + + +@pytest.mark.parametrize( + "value", + [ + "10, 20", + "a, 10, 20", + "10, b, 20", + "10, 20, c", + "10, 20, 30, 0.5", + ], +) +def test_rgb_invalid(value): + with pytest.raises(ValueError): + color(f"rgb({value})") + + +@pytest.mark.parametrize( + "value, expected", + [ + ("rgba(1,2,3,0.5)", (1, 2, 3, 0.5)), + ("rgba(1, 2, 3, 0.5)", (1, 2, 3, 0.5)), + ("rgba( 1 , 2 , 3 , 0.5)", (1, 2, 3, 0.5)), + ("#1234", (0x11, 0x22, 0x33, 0.2666)), + ("#11223344", (0x11, 0x22, 0x33, 0.2666)), + ("#abcd", (0xAA, 0xBB, 0xCC, 0.8666)), + ("#ABCD", (0xAA, 0xBB, 0xCC, 0.8666)), + ("#abcdefba", (0xAB, 0xCD, 0xEF, 0.7294)), + ("#ABCDEFBA", (0xAB, 0xCD, 0xEF, 0.7294)), + ], +) +def test_rgba(value, expected): + assert_equal_rgb(value, rgba(*expected)) + + +@pytest.mark.parametrize( + "value", + [ + "10, 20, 30", + "a, 10, 20, 0.5", + "10, b, 20, 0.5", + "10, 20, c, 0.5", + "10, 20, 30, c", + "10, 20, 30, 0.5, 5", + ], +) +def test_rgba_invalid(value): + with pytest.raises(ValueError): + color(f"rgba({value})") + + +@pytest.mark.parametrize( + "value", + [ + "1,20%,30%", + "1, 20%, 30%", + "1, 20% , 30%", + ], +) +def test_hsl(value): + assert_equal_hsl(f"hsl({value})", hsl(1, 0.2, 0.3)) + + +@pytest.mark.parametrize( + "value", + [ + "1, 20%", + "a, 20%, 30%", + "1, a, 30%", + "1, 20%, a)", + "1, 20%, 30%, 0.5)", + ], +) +def test_hsl_invalid(value): + with pytest.raises(ValueError): + color(value) + + +@pytest.mark.parametrize( + "value", + [ + "1,20%,30%,0.5", + "1, 20%, 30%, 0.5", + " 1, 20% , 30% , 0.5", + ], +) +def test_hsla(value): + assert_equal_hsl(f"hsla({value})", hsla(1, 0.2, 0.3, 0.5)) + + +@pytest.mark.parametrize( + "value", + [ + "1, 20%, 30%", + "a, 20%, 30%, 0.5", + "1, a, 30%, 0.5", + "1, 20%, a, 0.5", + "1, 20%, 30%, a", + "1, 20%, 30%, 0.5, 5", + ], +) +def test_hsla_invalid(value): + with pytest.raises(ValueError): + color(f"hsla({value})") + + +@pytest.mark.parametrize( + "value, expected", + [ + ("Red", (0xFF, 0, 0)), + ("RED", (0xFF, 0, 0)), + ("red", (0xFF, 0, 0)), + ("rEd", (0xFF, 0, 0)), + ("CornflowerBlue", (0x64, 0x95, 0xED)), + ("cornflowerblue", (0x64, 0x95, 0xED)), + ("CORNFLOWERBLUE", (0x64, 0x95, 0xED)), + ("Cornflowerblue", (0x64, 0x95, 0xED)), + ("CoRnFlOwErBlUe", (0x64, 0x95, 0xED)), + ], +) +def test_named_color(value, expected): + assert_equal_rgb(value, rgb(*expected)) + + +def test_named_color_invalid(): + with pytest.raises(ValueError): + color("not a color") diff --git a/travertino/tests/fonts/__init__.py b/travertino/tests/fonts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/travertino/tests/fonts/test_constructor.py b/travertino/tests/fonts/test_constructor.py new file mode 100644 index 0000000000..8729b51524 --- /dev/null +++ b/travertino/tests/fonts/test_constructor.py @@ -0,0 +1,203 @@ +import pytest + +from travertino.constants import ( + BOLD, + ITALIC, + NORMAL, + OBLIQUE, + SMALL_CAPS, + SYSTEM_DEFAULT_FONT_SIZE, +) +from travertino.fonts import Font + + +def assert_font(font, family, size, style, variant, weight): + assert font.family == family + assert font.size == size + assert font.style == style + assert font.variant == variant + assert font.weight == weight + + +@pytest.mark.parametrize( + "font", + [ + Font("Comic Sans", "12 pt"), + Font("Comic Sans", 12), + Font("Comic Sans", 12, NORMAL, NORMAL, NORMAL), + ], +) +def test_equality(font): + assert font == Font("Comic Sans", "12 pt") + + +@pytest.mark.parametrize( + "font", + [ + Font("Comic Sans", 13), + Font("Comic Sans", 12, ITALIC), + Font("Times New Roman", 12, NORMAL, NORMAL, NORMAL), + "a string", + 5, + ], +) +def test_inqequality(font): + assert font != Font("Comic Sans", "12 pt") + + +def test_hash(): + assert hash(Font("Comic Sans", 12)) == hash(Font("Comic Sans", 12)) + + assert hash(Font("Comic Sans", 12, weight=BOLD)) != hash(Font("Comic Sans", 12)) + + +@pytest.mark.parametrize( + "size, kwargs, string", + [ + (12, {}, "12pt"), + (12, {"style": ITALIC}, "italic 12pt"), + (12, {"style": ITALIC, "variant": SMALL_CAPS}, "italic small-caps 12pt"), + ( + 12, + {"style": ITALIC, "variant": SMALL_CAPS, "weight": BOLD}, + "italic small-caps bold 12pt", + ), + (12, {"variant": SMALL_CAPS, "weight": BOLD}, "small-caps bold 12pt"), + (12, {"weight": BOLD}, "bold 12pt"), + (12, {"style": ITALIC, "weight": BOLD}, "italic bold 12pt"), + # Check system default size handling + (SYSTEM_DEFAULT_FONT_SIZE, {}, "system default size"), + (SYSTEM_DEFAULT_FONT_SIZE, {"style": ITALIC}, "italic system default size"), + ], +) +def test_repr(size, kwargs, string): + assert repr(Font("Comic Sans", size, **kwargs)) == f"" + + +@pytest.mark.parametrize("size", [12, "12", "12pt", "12 pt"]) +def test_simple_construction(size): + assert_font(Font("Comic Sans", size), "Comic Sans", 12, NORMAL, NORMAL, NORMAL) + + +def test_invalid_construction(): + with pytest.raises(ValueError): + Font("Comic Sans", "12 quatloos") + + +@pytest.mark.parametrize( + "family", + [ + "Comics Sans", + "Wingdings", + "'Comic Sans'", + '"Comic Sans"', + ], +) +def test_family(family): + normalized_family = family.replace("'", "").replace('"', "") + assert_font(Font(family, 12), normalized_family, 12, NORMAL, NORMAL, NORMAL) + + +@pytest.mark.parametrize( + "style, result_style", + [ + (ITALIC, ITALIC), + ("italic", ITALIC), + (OBLIQUE, OBLIQUE), + ("oblique", OBLIQUE), + ("something else", NORMAL), + ], +) +def test_style(style, result_style): + assert_font( + Font("Comic Sans", 12, style=style), + "Comic Sans", + 12, + result_style, + NORMAL, + NORMAL, + ) + + +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"style": ITALIC}, + ], +) +def test_make_normal_style(kwargs): + f = Font("Comic Sans", 12, **kwargs) + assert_font(f.normal_style(), "Comic Sans", 12, NORMAL, NORMAL, NORMAL) + + +@pytest.mark.parametrize( + "method, result", + [ + ("italic", ITALIC), + ("oblique", OBLIQUE), + ], +) +def test_make_slanted(method, result): + f = Font("Comic Sans", 12) + assert_font(getattr(f, method)(), "Comic Sans", 12, result, NORMAL, NORMAL) + + +@pytest.mark.parametrize( + "variant, result", + [ + (SMALL_CAPS, SMALL_CAPS), + ("small-caps", SMALL_CAPS), + ("something else", NORMAL), + ], +) +def test_variant(variant, result): + assert_font( + Font("Comic Sans", 12, variant=variant), + "Comic Sans", + 12, + NORMAL, + result, + NORMAL, + ) + + +@pytest.mark.parametrize("kwargs", [{}, {"variant": SMALL_CAPS}]) +def test_make_normal_variant(kwargs): + f = Font("Comic Sans", 12, **kwargs) + assert_font(f.normal_variant(), "Comic Sans", 12, NORMAL, NORMAL, NORMAL) + + +def test_make_small_caps(): + f = Font("Comic Sans", 12) + assert_font(f.small_caps(), "Comic Sans", 12, NORMAL, SMALL_CAPS, NORMAL) + + +@pytest.mark.parametrize( + "weight, result", + [ + (BOLD, BOLD), + ("bold", BOLD), + ("something else", NORMAL), + ], +) +def test_weight(weight, result): + assert_font( + Font("Comic Sans", 12, weight=weight), + "Comic Sans", + 12, + NORMAL, + NORMAL, + result, + ) + + +@pytest.mark.parametrize("kwargs", [{}, {"weight": BOLD}]) +def test_make_normal_weight(kwargs): + f = Font("Comic Sans", 12, **kwargs) + assert_font(f.normal_weight(), "Comic Sans", 12, NORMAL, NORMAL, NORMAL) + + +def test_make_bold(): + f = Font("Comic Sans", 12) + assert_font(f.bold(), "Comic Sans", 12, NORMAL, NORMAL, BOLD) diff --git a/travertino/tests/fonts/test_parsing.py b/travertino/tests/fonts/test_parsing.py new file mode 100644 index 0000000000..1029f8e977 --- /dev/null +++ b/travertino/tests/fonts/test_parsing.py @@ -0,0 +1,133 @@ +import pytest + +from tests.fonts.test_constructor import assert_font +from travertino.constants import ( + BOLD, + ITALIC, + NORMAL, + OBLIQUE, + SMALL_CAPS, +) +from travertino.fonts import Font, font + + +def test_font_instance(): + f = Font("Comic Sans", 12) + + parsed = font(f) + + assert f == parsed + assert f is parsed + + +@pytest.mark.parametrize( + "string, style, variant, weight", + [ + ("12pt Comic Sans", NORMAL, NORMAL, NORMAL), + ("italic 12pt Comic Sans", ITALIC, NORMAL, NORMAL), + ("italic small-caps 12pt Comic Sans", ITALIC, SMALL_CAPS, NORMAL), + ("italic small-caps bold 12pt Comic Sans", ITALIC, SMALL_CAPS, BOLD), + ("small-caps bold 12pt Comic Sans", NORMAL, SMALL_CAPS, BOLD), + ("italic bold 12 pt Comic Sans", ITALIC, NORMAL, BOLD), + ("bold 12 pt Comic Sans", NORMAL, NORMAL, BOLD), + ], +) +def test_successful_combinations(string, style, variant, weight): + assert_font(font(string), "Comic Sans", 12, style, variant, weight) + + +@pytest.mark.parametrize( + "string", + [ + "12pt Comic Sans", + "12 pt Comic Sans", + "12 Comic Sans", + ], +) +def test_font_sizes(string): + assert_font(font(string), "Comic Sans", 12, NORMAL, NORMAL, NORMAL) + + +def test_invalid_size(): + with pytest.raises(ValueError): + font("12quatloo Comic Sans") + + +@pytest.mark.parametrize("string", ["12pt 'Comic Sans'", '12pt "Comic Sans"']) +def test_font_family(string): + assert_font(font(string), "Comic Sans", 12, NORMAL, NORMAL, NORMAL) + + +@pytest.mark.parametrize( + "string, style, variant", + [ + ("normal 12pt Comic Sans", NORMAL, NORMAL), + ("italic normal 12pt Comic Sans", ITALIC, NORMAL), + ("italic small-caps normal 12pt Comic Sans", ITALIC, SMALL_CAPS), + ], +) +def test_normal(string, style, variant): + assert_font(font(string), "Comic Sans", 12, style, variant, NORMAL) + + +@pytest.mark.parametrize( + "string, style", + [ + ("italic 12pt Comic Sans", ITALIC), + ("oblique 12pt Comic Sans", OBLIQUE), + ], +) +def test_style(string, style): + assert_font(font(string), "Comic Sans", 12, style, NORMAL, NORMAL) + + +def test_invalid_style(): + with pytest.raises(ValueError): + font("wiggly small-caps bold 12pt Comic Sans") + + +def test_variant(): + assert_font( + font("italic small-caps 12pt Comic Sans"), + "Comic Sans", + 12, + ITALIC, + SMALL_CAPS, + NORMAL, + ) + + with pytest.raises(ValueError): + font("italic wiggly bold 12pt Comic Sans") + + +def test_weight(): + assert_font( + font("italic small-caps bold 12pt Comic Sans"), + "Comic Sans", + 12, + ITALIC, + SMALL_CAPS, + BOLD, + ) + + with pytest.raises(ValueError): + font("italic small-caps wiggly 12pt Comic Sans") + + +@pytest.mark.parametrize( + "string", + [ + "oblique italic 12pt Comic Sans", + "italic small-caps oblique 12pt Comic Sans", + "italic small-caps bold small-caps 12pt Comic Sans", + "bold bold 12pt Comic Sans", + ], +) +def test_duplicates(string): + with pytest.raises(ValueError): + font(string) + + +def test_invaid(): + with pytest.raises(ValueError): + font(42) diff --git a/travertino/tests/test_choices.py b/travertino/tests/test_choices.py new file mode 100644 index 0000000000..b9ef2c865f --- /dev/null +++ b/travertino/tests/test_choices.py @@ -0,0 +1,399 @@ +from __future__ import annotations + +from warnings import catch_warnings, filterwarnings + +import pytest + +from tests.utils import mock_attr, prep_style_class +from travertino.colors import NAMED_COLOR, rgb +from travertino.constants import GOLDENROD, NONE, REBECCAPURPLE, TOP +from travertino.declaration import BaseStyle, Choices, validated_property + + +@prep_style_class +class Style(BaseStyle): + none: str = validated_property(choices=Choices(NONE, REBECCAPURPLE), initial=NONE) + allow_string: str = validated_property( + choices=Choices(string=True), initial="start" + ) + allow_integer: int = validated_property(choices=Choices(integer=True), initial=0) + allow_number: float = validated_property(choices=Choices(number=True), initial=0) + allow_color: str = validated_property( + choices=Choices(color=True), initial="goldenrod" + ) + values: str = validated_property(choices=Choices("a", "b", NONE), initial="a") + multiple_choices: str | float = validated_property( + choices=Choices("a", "b", NONE, number=True, color=True), + initial=None, + ) + string_symbol: str = validated_property(choices=Choices(TOP, NONE)) + + +with catch_warnings(): + filterwarnings("ignore", category=DeprecationWarning) + + @mock_attr("apply") + class DeprecatedStyle(BaseStyle): + pass + + DeprecatedStyle.validated_property( + "none", choices=Choices(NONE, REBECCAPURPLE), initial=NONE + ) + DeprecatedStyle.validated_property( + "allow_string", choices=Choices(string=True), initial="start" + ) + DeprecatedStyle.validated_property( + "allow_integer", choices=Choices(integer=True), initial=0 + ) + DeprecatedStyle.validated_property( + "allow_number", choices=Choices(number=True), initial=0 + ) + DeprecatedStyle.validated_property( + "allow_color", choices=Choices(color=True), initial="goldenrod" + ) + DeprecatedStyle.validated_property( + "values", choices=Choices("a", "b", NONE), initial="a" + ) + DeprecatedStyle.validated_property( + "multiple_choices", + choices=Choices("a", "b", NONE, number=True, color=True), + initial=None, + ) + DeprecatedStyle.validated_property("string_symbol", choices=Choices(TOP, NONE)) + + +def assert_property(obj, name, value): + assert getattr(obj, name) == value + + obj.apply.assert_called_once_with(name, value) + obj.apply.reset_mock() + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_none(StyleClass): + style = StyleClass() + assert style.none == NONE + + with pytest.raises(ValueError): + style.none = 10 + + with pytest.raises(ValueError): + style.none = 3.14159 + + with pytest.raises(ValueError): + style.none = "#112233" + + with pytest.raises(ValueError): + style.none = "a" + + with pytest.raises(ValueError): + style.none = "b" + + # Set the property to a different explicit value + style.none = REBECCAPURPLE + assert_property(style, "none", REBECCAPURPLE) + + # A Travertino NONE is an explicit value + style.none = NONE + assert_property(style, "none", NONE) + + # Set the property to a different explicit value + style.none = REBECCAPURPLE + assert_property(style, "none", REBECCAPURPLE) + + # A Python None is invalid + with pytest.raises(ValueError): + style.none = None + + # The property can be reset + del style.none + assert_property(style, "none", NONE) + + with pytest.raises( + ValueError, + match=r"Invalid value 'invalid' for property none; Valid values are: " + r"none, rebeccapurple", + ): + style.none = "invalid" + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_allow_string(StyleClass): + style = StyleClass() + assert style.allow_string == "start" + + with pytest.raises(ValueError): + style.allow_string = 10 + + with pytest.raises(ValueError): + style.allow_string = 3.14159 + + style.allow_string = REBECCAPURPLE + assert_property(style, "allow_string", "rebeccapurple") + + style.allow_string = "#112233" + assert_property(style, "allow_string", "#112233") + + style.allow_string = "a" + assert_property(style, "allow_string", "a") + + style.allow_string = "b" + assert_property(style, "allow_string", "b") + + # A Travertino NONE is an explicit string value + style.allow_string = NONE + assert_property(style, "allow_string", NONE) + + # A Python None is invalid + with pytest.raises(ValueError): + style.allow_string = None + + # The property can be reset + del style.allow_string + assert_property(style, "allow_string", "start") + + with pytest.raises( + ValueError, + match=r"Invalid value 99 for property allow_string; Valid values are: ", + ): + style.allow_string = 99 + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_allow_integer(StyleClass): + style = StyleClass() + assert style.allow_integer == 0 + + style.allow_integer = 10 + assert_property(style, "allow_integer", 10) + + # This is an odd case; Python happily rounds floats to integers. + # It's more trouble than it's worth to correct this. + style.allow_integer = 3.14159 + assert_property(style, "allow_integer", 3) + + with pytest.raises(ValueError): + style.allow_integer = REBECCAPURPLE + + with pytest.raises(ValueError): + style.allow_integer = "#112233" + + with pytest.raises(ValueError): + style.allow_integer = "a" + + with pytest.raises(ValueError): + style.allow_integer = "b" + + # A Travertino NONE is an explicit string value + with pytest.raises(ValueError): + style.allow_integer = NONE + + # A Python None is invalid + with pytest.raises(ValueError): + style.allow_integer = None + + # The property can be reset + del style.allow_integer + assert_property(style, "allow_integer", 0) + + # Check the error message + with pytest.raises( + ValueError, + match=( + r"Invalid value 'invalid' for property allow_integer; Valid values are: " + r"" + ), + ): + style.allow_integer = "invalid" + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_allow_number(StyleClass): + style = StyleClass() + assert style.allow_number == 0 + + style.allow_number = 10 + assert_property(style, "allow_number", 10.0) + + style.allow_number = 3.14159 + assert_property(style, "allow_number", 3.14159) + + with pytest.raises(ValueError): + style.allow_number = REBECCAPURPLE + + with pytest.raises(ValueError): + style.allow_number = "#112233" + + with pytest.raises(ValueError): + style.allow_number = "a" + + with pytest.raises(ValueError): + style.allow_number = "b" + + # A Travertino NONE is an explicit string value + with pytest.raises(ValueError): + style.allow_number = NONE + + # A Python None is invalid + with pytest.raises(ValueError): + style.allow_number = None + + # The property can be reset + del style.allow_number + assert_property(style, "allow_number", 0) + + with pytest.raises( + ValueError, + match=( + r"Invalid value 'invalid' for property allow_number; Valid values are: " + r"" + ), + ): + style.allow_number = "invalid" + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_allow_color(StyleClass): + style = StyleClass() + assert style.allow_color == NAMED_COLOR[GOLDENROD] + + with pytest.raises(ValueError): + style.allow_color = 10 + + with pytest.raises(ValueError): + style.allow_color = 3.14159 + + style.allow_color = REBECCAPURPLE + assert_property(style, "allow_color", NAMED_COLOR[REBECCAPURPLE]) + + style.allow_color = "#112233" + assert_property(style, "allow_color", rgb(0x11, 0x22, 0x33)) + + with pytest.raises(ValueError): + style.allow_color = "a" + + with pytest.raises(ValueError): + style.allow_color = "b" + + # A Travertino NONE is an explicit string value + with pytest.raises(ValueError): + style.allow_color = NONE + + # A Python None is invalid + with pytest.raises(ValueError): + style.allow_color = None + + # The property can be reset + del style.allow_color + assert_property(style, "allow_color", NAMED_COLOR["goldenrod"]) + + with pytest.raises( + ValueError, + match=( + r"Invalid value 'invalid' for property allow_color; Valid values are: " + r"" + ), + ): + style.allow_color = "invalid" + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_values(StyleClass): + style = StyleClass() + assert style.values == "a" + + with pytest.raises(ValueError): + style.values = 10 + + with pytest.raises(ValueError): + style.values = 3.14159 + + with pytest.raises(ValueError): + style.values = REBECCAPURPLE + + with pytest.raises(ValueError): + style.values = "#112233" + + style.values = NONE + assert_property(style, "values", NONE) + + style.values = "b" + assert_property(style, "values", "b") + + # A Python None is invalid + with pytest.raises(ValueError): + style.values = None + + # The property can be reset + del style.values + assert_property(style, "values", "a") + + with pytest.raises( + ValueError, + match=( + r"Invalid value 'invalid' for property values; Valid values are: a, b, " + r"none" + ), + ): + style.values = "invalid" + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_multiple_choices(StyleClass): + style = StyleClass() + + style.multiple_choices = 10 + assert_property(style, "multiple_choices", 10.0) + + style.multiple_choices = 3.14159 + assert_property(style, "multiple_choices", 3.14159) + + style.multiple_choices = REBECCAPURPLE + assert_property(style, "multiple_choices", NAMED_COLOR[REBECCAPURPLE]) + + style.multiple_choices = "#112233" + assert_property(style, "multiple_choices", rgb(0x11, 0x22, 0x33)) + + style.multiple_choices = "a" + assert_property(style, "multiple_choices", "a") + + style.multiple_choices = NONE + assert_property(style, "multiple_choices", NONE) + + style.multiple_choices = "b" + assert_property(style, "multiple_choices", "b") + + # A Python None is invalid + with pytest.raises(ValueError): + style.multiple_choices = None + + # The property can be reset + # There's no initial value, so the property is None + del style.multiple_choices + assert style.multiple_choices is None + + # Check the error message + with pytest.raises( + ValueError, + match=( + r"Invalid value 'invalid' for property multiple_choices; Valid values are: " + r"a, b, none, , " + ), + ): + style.multiple_choices = "invalid" + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_string_symbol(StyleClass): + style = StyleClass() + + # Set a symbolic value using the string value of the symbol + # We can't just use the string directly, though - that would + # get optimized by the compiler. So we create a string and + # transform it into the value we want. + val = "TOP" + style.string_symbol = val.lower() + + # Both equality and instance checking should work. + assert_property(style, "string_symbol", TOP) + assert style.string_symbol is TOP diff --git a/travertino/tests/test_declaration.py b/travertino/tests/test_declaration.py new file mode 100644 index 0000000000..c2f1414630 --- /dev/null +++ b/travertino/tests/test_declaration.py @@ -0,0 +1,836 @@ +from __future__ import annotations + +from unittest.mock import call +from warnings import catch_warnings, filterwarnings + +import pytest + +from tests.utils import mock_attr, prep_style_class +from travertino.declaration import ( + BaseStyle, + Choices, + ImmutableList, + directional_property, + list_property, + validated_property, +) + +VALUE1 = "value1" +VALUE2 = "value2" +VALUE3 = "value3" +VALUE_CHOICES = Choices(VALUE1, VALUE2, VALUE3, None, integer=True) +DEFAULT_VALUE_CHOICES = Choices(VALUE1, VALUE2, VALUE3, integer=True) + + +@prep_style_class +class Style(BaseStyle): + # Some properties with explicit initial values + explicit_const: str | int = validated_property( + choices=VALUE_CHOICES, initial=VALUE1 + ) + explicit_value: str | int = validated_property(choices=VALUE_CHOICES, initial=0) + explicit_none: str | int | None = validated_property( + choices=VALUE_CHOICES, initial=None + ) + + # A property with an implicit default value. + # This usually means the default is platform specific. + implicit: str | int | None = validated_property(choices=DEFAULT_VALUE_CHOICES) + + # A set of directional properties + thing: tuple[str | int] | str | int = directional_property("thing{}") + thing_top: str | int = validated_property(choices=VALUE_CHOICES, initial=0) + thing_right: str | int = validated_property(choices=VALUE_CHOICES, initial=0) + thing_bottom: str | int = validated_property(choices=VALUE_CHOICES, initial=0) + thing_left: str | int = validated_property(choices=VALUE_CHOICES, initial=0) + + # Doesn't need to be tested in deprecated API: + list_prop: list[str] = list_property(choices=VALUE_CHOICES, initial=(VALUE2,)) + + +with catch_warnings(): + filterwarnings("ignore", category=DeprecationWarning) + + @mock_attr("apply") + class DeprecatedStyle(BaseStyle): + pass + + # Some properties with explicit initial values + DeprecatedStyle.validated_property( + "explicit_const", choices=VALUE_CHOICES, initial=VALUE1 + ) + DeprecatedStyle.validated_property( + "explicit_value", choices=VALUE_CHOICES, initial=0 + ) + DeprecatedStyle.validated_property( + "explicit_none", choices=VALUE_CHOICES, initial=None + ) + + # A property with an implicit default value. + # This usually means the default is platform specific. + DeprecatedStyle.validated_property("implicit", choices=DEFAULT_VALUE_CHOICES) + + # A set of directional properties + DeprecatedStyle.validated_property("thing_top", choices=VALUE_CHOICES, initial=0) + DeprecatedStyle.validated_property("thing_right", choices=VALUE_CHOICES, initial=0) + DeprecatedStyle.validated_property("thing_bottom", choices=VALUE_CHOICES, initial=0) + DeprecatedStyle.validated_property("thing_left", choices=VALUE_CHOICES, initial=0) + DeprecatedStyle.directional_property("thing%s") + + +class StyleSubclass(Style): + pass + + +class DeprecatedStyleSubclass(DeprecatedStyle): + pass + + +class Sibling(BaseStyle): + pass + + +@prep_style_class +@mock_attr("reapply") +class MockedReapplyStyle(BaseStyle): + pass + + +def test_invalid_style(): + with pytest.raises(ValueError): + # Define an invalid initial value on a validated property + validated_property(choices=VALUE_CHOICES, initial="something") + + with pytest.raises(ValueError): + # Same for list property + list_property(choices=VALUE_CHOICES, initial=["something"]) + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_positional_argument(StyleClass): + # Could be the subclass or inherited __init__, depending on Python version / API + # used. + with pytest.raises( + TypeError, match=r"__init__\(\) takes 1 positional argument but 2 were given" + ): + StyleClass(5) + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_create_and_copy(StyleClass): + style = StyleClass(explicit_const=VALUE2, implicit=VALUE3) + + dup = style.copy() + assert dup.explicit_const == VALUE2 + assert dup.explicit_value == 0 + assert dup.implicit == VALUE3 + + +def test_deprecated_copy(): + style = MockedReapplyStyle() + + with pytest.warns(DeprecationWarning): + style_copy = style.copy(applicator=object()) + + style_copy.reapply.assert_called_once() + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_reapply(StyleClass): + style = StyleClass(explicit_const=VALUE2, implicit=VALUE3) + + style.reapply() + style.apply.assert_has_calls( + [ + call("explicit_const", VALUE2), + call("explicit_value", 0), + call("explicit_none", None), + call("implicit", VALUE3), + call("thing_left", 0), + call("thing_top", 0), + call("thing_right", 0), + call("thing_bottom", 0), + ], + any_order=True, + ) + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_property_with_explicit_const(StyleClass): + style = StyleClass() + + # Default value is VALUE1 + assert style.explicit_const is VALUE1 + style.apply.assert_not_called() + + # Modify the value + style.explicit_const = 10 + + assert style.explicit_const == 10 + style.apply.assert_called_once_with("explicit_const", 10) + + # Clear the applicator mock + style.apply.reset_mock() + + # Set the value to the same value. + # No dirty notification is sent + style.explicit_const = 10 + assert style.explicit_const == 10 + style.apply.assert_not_called() + + # Set the value to something new + # A dirty notification is set. + style.explicit_const = 20 + assert style.explicit_const == 20 + style.apply.assert_called_once_with("explicit_const", 20) + + # Clear the applicator mock + style.apply.reset_mock() + + # Clear the property + del style.explicit_const + assert style.explicit_const is VALUE1 + style.apply.assert_called_once_with("explicit_const", VALUE1) + + # Clear the applicator mock + style.apply.reset_mock() + + # Clear the property again. + # The underlying attribute won't exist, so this + # should be a no-op. + del style.explicit_const + assert style.explicit_const is VALUE1 + style.apply.assert_not_called() + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_property_with_explicit_value(StyleClass): + style = StyleClass() + + # Default value is 0 + assert style.explicit_value == 0 + style.apply.assert_not_called() + + # Modify the value + style.explicit_value = 10 + + assert style.explicit_value == 10 + style.apply.assert_called_once_with("explicit_value", 10) + + # Clear the applicator mock + style.apply.reset_mock() + + # Set the value to the same value. + # No dirty notification is sent + style.explicit_value = 10 + assert style.explicit_value == 10 + style.apply.assert_not_called() + + # Set the value to something new + # A dirty notification is set. + style.explicit_value = 20 + assert style.explicit_value == 20 + style.apply.assert_called_once_with("explicit_value", 20) + + # Clear the applicator mock + style.apply.reset_mock() + + # Clear the property + del style.explicit_value + assert style.explicit_value == 0 + style.apply.assert_called_once_with("explicit_value", 0) + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_property_with_explicit_none(StyleClass): + style = StyleClass() + + # Default value is None + assert style.explicit_none is None + style.apply.assert_not_called() + + # Modify the value + style.explicit_none = 10 + + assert style.explicit_none == 10 + style.apply.assert_called_once_with("explicit_none", 10) + + # Clear the applicator mock + style.apply.reset_mock() + + # Set the property to the same value. + # No dirty notification is sent + style.explicit_none = 10 + assert style.explicit_none == 10 + style.apply.assert_not_called() + + # Set the property to something new + # A dirty notification is set. + style.explicit_none = 20 + assert style.explicit_none == 20 + style.apply.assert_called_once_with("explicit_none", 20) + + # Clear the applicator mock + style.apply.reset_mock() + + # Clear the property + del style.explicit_none + assert style.explicit_none is None + style.apply.assert_called_once_with("explicit_none", None) + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_property_with_implicit_default(StyleClass): + style = StyleClass() + + # Default value is None + assert style.implicit is None + style.apply.assert_not_called() + + # Modify the value + style.implicit = 10 + + assert style.implicit == 10 + style.apply.assert_called_once_with("implicit", 10) + + # Clear the applicator mock + style.apply.reset_mock() + + # Set the value to the same value. + # No dirty notification is sent + style.implicit = 10 + assert style.implicit == 10 + style.apply.assert_not_called() + + # Set the value to something new + # A dirty notification is set. + style.implicit = 20 + assert style.implicit == 20 + style.apply.assert_called_once_with("implicit", 20) + + # Clear the applicator mock + style.apply.reset_mock() + + # Clear the property + del style.implicit + assert style.implicit is None + style.apply.assert_called_once_with("implicit", None) + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_set_initial_no_apply(StyleClass): + """If a property hasn't been set, assigning it its initial value shouldn't apply.""" + style = StyleClass() + + # 0 is the initial value + style.explicit_value = 0 + + style.apply.assert_not_called() + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_directional_property(StyleClass): + style = StyleClass() + + # Default value is 0 + assert style.thing == (0, 0, 0, 0) + assert style.thing_top == 0 + assert style.thing_right == 0 + assert style.thing_bottom == 0 + assert style.thing_left == 0 + style.apply.assert_not_called() + + # Set a value in one axis + style.thing_top = 10 + + assert style.thing == (10, 0, 0, 0) + assert style.thing_top == 10 + assert style.thing_right == 0 + assert style.thing_bottom == 0 + assert style.thing_left == 0 + style.apply.assert_called_once_with("thing_top", 10) + + # Clear the applicator mock + style.apply.reset_mock() + + # Set a value directly with a single item + style.thing = (10,) + + assert style.thing == (10, 10, 10, 10) + assert style.thing_top == 10 + assert style.thing_right == 10 + assert style.thing_bottom == 10 + assert style.thing_left == 10 + style.apply.assert_has_calls( + [ + call("thing_right", 10), + call("thing_bottom", 10), + call("thing_left", 10), + ] + ) + + # Clear the applicator mock + style.apply.reset_mock() + + # Set a value directly with a single item + style.thing = 30 + + assert style.thing == (30, 30, 30, 30) + assert style.thing_top == 30 + assert style.thing_right == 30 + assert style.thing_bottom == 30 + assert style.thing_left == 30 + style.apply.assert_has_calls( + [ + call("thing_top", 30), + call("thing_right", 30), + call("thing_bottom", 30), + call("thing_left", 30), + ] + ) + + # Clear the applicator mock + style.apply.reset_mock() + + # Set a value directly with a 2 values + style.thing = (10, 20) + + assert style.thing == (10, 20, 10, 20) + assert style.thing_top == 10 + assert style.thing_right == 20 + assert style.thing_bottom == 10 + assert style.thing_left == 20 + style.apply.assert_has_calls( + [ + call("thing_top", 10), + call("thing_right", 20), + call("thing_bottom", 10), + call("thing_left", 20), + ] + ) + + # Clear the applicator mock + style.apply.reset_mock() + + # Set a value directly with a 3 values + style.thing = (10, 20, 30) + + assert style.thing == (10, 20, 30, 20) + assert style.thing_top == 10 + assert style.thing_right == 20 + assert style.thing_bottom == 30 + assert style.thing_left == 20 + style.apply.assert_called_once_with("thing_bottom", 30) + + # Clear the applicator mock + style.apply.reset_mock() + + # Set a value directly with a 4 values + style.thing = (10, 20, 30, 40) + + assert style.thing == (10, 20, 30, 40) + assert style.thing_top == 10 + assert style.thing_right == 20 + assert style.thing_bottom == 30 + assert style.thing_left == 40 + style.apply.assert_called_once_with("thing_left", 40) + + # Set a value directly with an invalid number of values + with pytest.raises(ValueError): + style.thing = () + + with pytest.raises(ValueError): + style.thing = (10, 20, 30, 40, 50) + + # Clear the applicator mock + style.apply.reset_mock() + + # Clear a value on one axis + del style.thing_top + + assert style.thing == (0, 20, 30, 40) + assert style.thing_top == 0 + assert style.thing_right == 20 + assert style.thing_bottom == 30 + assert style.thing_left == 40 + style.apply.assert_called_once_with("thing_top", 0) + + # Restore the top thing + style.thing_top = 10 + + # Clear the applicator mock + style.apply.reset_mock() + + # Clear a value directly + del style.thing + + assert style.thing == (0, 0, 0, 0) + assert style.thing_top == 0 + assert style.thing_right == 0 + assert style.thing_bottom == 0 + assert style.thing_left == 0 + style.apply.assert_has_calls( + [ + call("thing_right", 0), + call("thing_bottom", 0), + call("thing_left", 0), + ] + ) + + +@pytest.mark.parametrize( + "value, expected", + [ + ([VALUE1], [VALUE1]), + (VALUE1, [VALUE1]), + ([VALUE1, VALUE3], [VALUE1, VALUE3]), + ([VALUE2, VALUE1], [VALUE2, VALUE1]), + ([VALUE2, VALUE3, 1, 2, VALUE1], [VALUE2, VALUE3, 1, 2, VALUE1]), + # Duplicates are kept, but "normalized" via validation. + ( + [VALUE3, 1, VALUE3, "1", True, " 1", VALUE2], + [VALUE3, 1, VALUE3, 1, 1, 1, VALUE2], + ), + # Other sequences should work too. + ((VALUE1, VALUE3), [VALUE1, VALUE3]), + ], +) +def test_list_property(value, expected): + style = Style() + style.list_prop = value + assert style.list_prop == expected + + +@pytest.mark.parametrize( + "value, error, match", + [ + ( + 5, + TypeError, + r"Value for list property list_prop must be a sequence\.", + ), + ( + # Fails because it's only a generator, not a comprehension: + (i for i in [VALUE1, VALUE3]), + TypeError, + r"Value for list property list_prop must be a sequence.", + ), + ( + [VALUE3, VALUE1, "bogus"], + ValueError, + r"Invalid item value 'bogus' for list property list_prop; " + r"Valid values are: none, value1, value2, value3, ", + ), + ( + (), + ValueError, + r"List properties cannot be set to an empty sequence; " + r"to reset a property, use del `style.list_prop`\.", + ), + ( + [], + ValueError, + r"List properties cannot be set to an empty sequence; " + r"to reset a property, use del `style.list_prop`\.", + ), + ], +) +def test_list_property_invalid(value, error, match): + style = Style() + with pytest.raises(error, match=match): + style.list_prop = value + + +def test_list_property_immutable(): + style = Style() + style.list_prop = [1, 2, 3, VALUE2] + prop = style.list_prop + + with pytest.raises(TypeError, match=r"does not support item assignment"): + prop[0] = 5 + + with pytest.raises(TypeError, match=r"doesn't support item deletion"): + del prop[1] + + with pytest.raises(AttributeError): + prop.insert(2, VALUE1) + + with pytest.raises(AttributeError): + prop.append(VALUE3) + + with pytest.raises(AttributeError): + prop.clear() + + with pytest.raises(AttributeError): + prop.reverse() + + with pytest.raises(AttributeError): + prop.pop() + + with pytest.raises(AttributeError): + prop.remove(VALUE2) + + with pytest.raises(AttributeError): + prop.extend([5, 6, 7]) + + with pytest.raises(TypeError, match=r"unsupported operand type\(s\)"): + prop += [4, 3, VALUE1] + + with pytest.raises(TypeError, match=r"unsupported operand type\(s\)"): + prop += ImmutableList([4, 3, VALUE1]) + + with pytest.raises(AttributeError): + prop.sort() + + +def test_list_property_list_like(): + style = Style() + style.list_prop = [1, 2, 3, VALUE2] + prop = style.list_prop + + assert isinstance(prop, ImmutableList) + assert prop == [1, 2, 3, VALUE2] + assert prop == ImmutableList([1, 2, 3, VALUE2]) + assert str(prop) == repr(prop) == "[1, 2, 3, 'value2']" + assert len(prop) == 4 + + count = 0 + for _ in prop: + count += 1 + assert count == 4 + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_set_multiple_properties(StyleClass): + style = StyleClass() + + # Set a pair of properties + style.update(explicit_value=20, explicit_none=10) + + assert style.explicit_const is VALUE1 + assert style.explicit_none == 10 + assert style.explicit_value == 20 + style.apply.assert_has_calls( + [ + call("explicit_value", 20), + call("explicit_none", 10), + ], + any_order=True, + ) + + # Set a different pair of properties + style.update(explicit_const=VALUE2, explicit_value=30) + + assert style.explicit_const is VALUE2 + assert style.explicit_value == 30 + assert style.explicit_none == 10 + style.apply.assert_has_calls( + [ + call("explicit_const", VALUE2), + call("explicit_value", 30), + ], + any_order=True, + ) + + # Clear the applicator mock + style.apply.reset_mock() + + # Setting a non-property + with pytest.raises(NameError): + style.update(not_a_property=10) + + style.apply.assert_not_called() + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_str(StyleClass): + style = StyleClass() + + style.update( + explicit_const=VALUE2, + explicit_value=20, + thing=(30, 40, 50, 60), + ) + + assert ( + str(style) == "explicit-const: value2; " + "explicit-value: 20; " + "thing-bottom: 50; " + "thing-left: 60; " + "thing-right: 40; " + "thing-top: 30" + ) + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +def test_dict(StyleClass): + "Style declarations expose a dict-like interface" + style = StyleClass() + + style.update( + explicit_const=VALUE2, + explicit_value=20, + thing=(30, 40, 50, 60), + ) + + expected_keys = { + "explicit_const", + "explicit_value", + "thing_bottom", + "thing_left", + "thing_right", + "thing_top", + } + + assert style.keys() == expected_keys + + assert sorted(style.items()) == sorted( + [ + ("explicit_const", "value2"), + ("explicit_value", 20), + ("thing_bottom", 50), + ("thing_left", 60), + ("thing_right", 40), + ("thing_top", 30), + ] + ) + + # Properties that are set are in the keys. + for name in expected_keys: + assert name in style + + # Directional properties with one or more of the aliased properties set also count. + assert "thing" in style + + # Valid properties that haven't been set are not in the keys. + assert "implicit" not in style + assert "explicit_none" not in style + + # Neither are invalid properties. + assert "invalid_property" not in style + + # A property can be set, retrieved and cleared using the attribute name + style["thing-bottom"] = 10 + assert style["thing-bottom"] == 10 + del style["thing-bottom"] + assert style["thing-bottom"] == 0 + + # A property can be set, retrieved and cleared using the Python attribute name + style["thing_bottom"] = 10 + assert style["thing_bottom"] == 10 + del style["thing_bottom"] + assert style["thing_bottom"] == 0 + + # Property aliases can be accessed as well. + style["thing"] = 5 + assert style["thing"] == (5, 5, 5, 5) + del style["thing"] + assert style["thing"] == (0, 0, 0, 0) + + # Clearing a valid property isn't an error + del style["thing_bottom"] + assert style["thing_bottom"] == 0 + + # Non-existent properties raise KeyError + with pytest.raises(KeyError): + style["no-such-property"] = "no-such-value" + + with pytest.raises(KeyError): + style["no-such-property"] + + with pytest.raises(KeyError): + del style["no-such-property"] + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +@pytest.mark.parametrize("instantiate", [True, False]) +def test_union_operators(StyleClass, instantiate): + """Styles support | and |= with dicts and with their own class.""" + left = StyleClass(explicit_value=VALUE1, implicit=VALUE2) + + style_dict = {"thing_top": 5, "implicit": VALUE3} + right = StyleClass(**style_dict) if instantiate else style_dict + + # Standard operator + result = left | right + + # Original objects unchanged + assert left["explicit_value"] == VALUE1 + assert left["implicit"] == VALUE2 + + assert right["thing_top"] == 5 + assert right["implicit"] == VALUE3 + + # Unshared properties assigned + assert result["explicit_const"] == VALUE1 + assert result["thing_top"] == 5 + + # Common property overridden by second operand + assert result["implicit"] == VALUE3 + + # In-place version + left |= right + + # Common property updated on lefthand + assert left["explicit_value"] == VALUE1 + assert left["implicit"] == VALUE3 + + # Righthand unchanged + assert right["thing_top"] == 5 + assert right["implicit"] == VALUE3 + + +@pytest.mark.parametrize( + "StyleClass, OtherClass", + [ + (Style, StyleSubclass), + (Style, Sibling), + (Style, int), + (Style, list), + (DeprecatedStyle, DeprecatedStyleSubclass), + (DeprecatedStyle, Sibling), + (DeprecatedStyle, int), + (DeprecatedStyle, list), + ], +) +def test_union_operators_invalid_type(StyleClass, OtherClass): + """Styles do not support | or |= with other style classes or with non-mappings.""" + + left = StyleClass() + right = OtherClass() + + with pytest.raises(TypeError, match=r"unsupported operand type"): + left | right + + with pytest.raises(TypeError, match=r"unsupported operand type"): + left |= right + + +@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle]) +@pytest.mark.parametrize( + "right, error", + [ + ({"implicit": "bogus_value"}, ValueError), + ({"bogus_key": 3.12}, NameError), + ], +) +def test_union_operators_invalid_key_value(StyleClass, right, error): + """Operators will accept any mapping, but invalid keys/values are still an error.""" + left = StyleClass() + + with pytest.raises(error): + left | right + + with pytest.raises(error): + left |= right + + +def test_deprecated_class_methods(): + class OldStyle(BaseStyle): + pass + + with pytest.warns(DeprecationWarning): + OldStyle.validated_property("implicit", choices=DEFAULT_VALUE_CHOICES) + + with pytest.warns(DeprecationWarning): + OldStyle.directional_property("thing%s") diff --git a/travertino/tests/test_layout.py b/travertino/tests/test_layout.py new file mode 100644 index 0000000000..d6c417f120 --- /dev/null +++ b/travertino/tests/test_layout.py @@ -0,0 +1,407 @@ +import pytest + +from travertino.declaration import BaseStyle +from travertino.layout import BaseBox, Viewport +from travertino.node import Node +from travertino.size import BaseIntrinsicSize + + +class Style(BaseStyle): + class IntrinsicSize(BaseIntrinsicSize): + pass + + class Box(BaseBox): + pass + + +def test_viewport_default(): + viewport = Viewport() + + assert viewport.width == 0 + assert viewport.height == 0 + assert viewport.dpi is None + + +def test_viewport_constructor(): + viewport = Viewport(width=640, height=480, dpi=96) + + assert viewport.width == 640 + assert viewport.height == 480 + assert viewport.dpi == 96 + + +class TestBox: + pass + + +@pytest.fixture +def box(): + box = TestBox() + + box.maxDiff = None + + box.grandchild1_1 = Node(style=Style()) + box.grandchild1_1.layout.min_content_width = 5 + box.grandchild1_1.layout.content_width = 10 + box.grandchild1_1.layout.min_content_height = 8 + box.grandchild1_1.layout.content_height = 16 + + box.grandchild1_2 = Node(style=Style()) + + box.child1 = Node(style=Style(), children=[box.grandchild1_1, box.grandchild1_2]) + box.child1.layout.min_content_width = 5 + box.child1.layout.content_width = 10 + box.child1.layout.min_content_height = 8 + box.child1.layout.content_height = 16 + box.child2 = Node(style=Style(), children=[]) + + box.node = Node(style=Style(), children=[box.child1, box.child2]) + box.node.layout.min_content_width = 5 + box.node.layout.content_width = 10 + box.node.layout.min_content_height = 8 + box.node.layout.content_height = 16 + + return box + + +def assert_layout(box, expected): + actual = { + "origin": (box._origin_left, box._origin_top), + "min_size": (box.min_width, box.min_height), + "size": (box.width, box.height), + "content": (box.content_width, box.content_height), + "relative": ( + box.content_top, + box.content_right, + box.content_bottom, + box.content_left, + ), + "absolute": ( + box.absolute_content_top, + box.absolute_content_right, + box.absolute_content_bottom, + box.absolute_content_left, + ), + } + assert actual == expected + + +def test_repr(box): + box.node.layout._origin_top = 1 + box.node.layout._origin_left = 2 + assert repr(box.node.layout) == "" + + +def test_initial(box): + # Core attributes have been stored + assert_layout( + box.node.layout, + { + "origin": (0, 0), + "min_size": (5, 8), + "size": (10, 16), + "content": (10, 16), + "relative": (0, 0, 0, 0), + "absolute": (0, 10, 16, 0), + }, + ) + + +@pytest.mark.parametrize( + "dimension, val1, expected1, val2, expected2", + [ + ( + "content_top", + 5, + { + "origin": (0, 0), + "min_size": (5, 13), + "size": (10, 21), + "content": (10, 16), + "relative": (5, 0, 0, 0), + "absolute": (5, 10, 21, 0), + }, + 7, + { + "origin": (0, 0), + "min_size": (5, 15), + "size": (10, 23), + "content": (10, 16), + "relative": (7, 0, 0, 0), + "absolute": (7, 10, 23, 0), + }, + ), + ( + "content_left", + 5, + { + "origin": (0, 0), + "min_size": (10, 8), + "size": (15, 16), + "content": (10, 16), + "relative": (0, 0, 0, 5), + "absolute": (0, 15, 16, 5), + }, + 7, + { + "origin": (0, 0), + "min_size": (12, 8), + "size": (17, 16), + "content": (10, 16), + "relative": (0, 0, 0, 7), + "absolute": (0, 17, 16, 7), + }, + ), + ( + "min_content_width", + 8, + { + "origin": (0, 0), + "min_size": (8, 8), + "size": (10, 16), + "content": (10, 16), + "relative": (0, 0, 0, 0), + "absolute": (0, 10, 16, 0), + }, + 9, + { + "origin": (0, 0), + "min_size": (9, 8), + "size": (10, 16), + "content": (10, 16), + "relative": (0, 0, 0, 0), + "absolute": (0, 10, 16, 0), + }, + ), + ( + "content_width", + 5, + { + "origin": (0, 0), + "min_size": (5, 8), + "size": (5, 16), + "content": (5, 16), + "relative": (0, 0, 0, 0), + "absolute": (0, 5, 16, 0), + }, + 7, + { + "origin": (0, 0), + "min_size": (5, 8), + "size": (7, 16), + "content": (7, 16), + "relative": (0, 0, 0, 0), + "absolute": (0, 7, 16, 0), + }, + ), + ( + "min_content_height", + 7, + { + "origin": (0, 0), + "min_size": (5, 7), + "size": (10, 16), + "content": (10, 16), + "relative": (0, 0, 0, 0), + "absolute": (0, 10, 16, 0), + }, + 8, + { + "origin": (0, 0), + "min_size": (5, 8), + "size": (10, 16), + "content": (10, 16), + "relative": (0, 0, 0, 0), + "absolute": (0, 10, 16, 0), + }, + ), + ( + "content_height", + 10, + { + "origin": (0, 0), + "min_size": (5, 8), + "size": (10, 10), + "content": (10, 10), + "relative": (0, 0, 0, 0), + "absolute": (0, 10, 10, 0), + }, + 12, + { + "origin": (0, 0), + "min_size": (5, 8), + "size": (10, 12), + "content": (10, 12), + "relative": (0, 0, 0, 0), + "absolute": (0, 10, 12, 0), + }, + ), + ], +) +def test_set_content_dimension(box, dimension, val1, expected1, val2, expected2): + setattr(box.node.layout, dimension, val1) + assert_layout(box.node.layout, expected1) + + # Set to a new value + setattr(box.node.layout, dimension, val2) + assert_layout(box.node.layout, expected2) + + +def test_descendent_offsets(box): + box.node.layout.content_top = 7 + box.node.layout.content_left = 8 + + box.child1.layout.content_top = 9 + box.child1.layout.content_left = 10 + + box.grandchild1_1.layout.content_top = 11 + box.grandchild1_1.layout.content_left = 12 + + assert_layout( + box.node.layout, + { + "origin": (0, 0), + "min_size": (13, 15), + "size": (18, 23), + "content": (10, 16), + "relative": (7, 0, 0, 8), + "absolute": (7, 18, 23, 8), + }, + ) + + assert_layout( + box.child1.layout, + { + "origin": (8, 7), + "min_size": (15, 17), + "size": (20, 25), + "content": (10, 16), + "relative": (9, 0, 0, 10), + "absolute": (16, 28, 32, 18), + }, + ) + + assert_layout( + box.grandchild1_1.layout, + { + "origin": (18, 16), + "min_size": (17, 19), + "size": (22, 27), + "content": (10, 16), + "relative": (11, 0, 0, 12), + "absolute": (27, 40, 43, 30), + }, + ) + + # Modify the grandchild position + box.grandchild1_1.layout.content_top = 13 + box.grandchild1_1.layout.content_left = 14 + + # Only the grandchild position has changed. + assert_layout( + box.node.layout, + { + "origin": (0, 0), + "min_size": (13, 15), + "size": (18, 23), + "content": (10, 16), + "relative": (7, 0, 0, 8), + "absolute": (7, 18, 23, 8), + }, + ) + + assert_layout( + box.child1.layout, + { + "origin": (8, 7), + "min_size": (15, 17), + "size": (20, 25), + "content": (10, 16), + "relative": (9, 0, 0, 10), + "absolute": (16, 28, 32, 18), + }, + ) + + assert_layout( + box.grandchild1_1.layout, + { + "origin": (18, 16), + "min_size": (19, 21), + "size": (24, 29), + "content": (10, 16), + "relative": (13, 0, 0, 14), + "absolute": (29, 42, 45, 32), + }, + ) + + # Modify the child position + box.child1.layout.content_top = 15 + box.child1.layout.content_left = 16 + + # The child and grandchild positions have changed. + assert_layout( + box.node.layout, + { + "origin": (0, 0), + "min_size": (13, 15), + "size": (18, 23), + "content": (10, 16), + "relative": (7, 0, 0, 8), + "absolute": (7, 18, 23, 8), + }, + ) + + assert_layout( + box.child1.layout, + { + "origin": (8, 7), + "min_size": (21, 23), + "size": (26, 31), + "content": (10, 16), + "relative": (15, 0, 0, 16), + "absolute": (22, 34, 38, 24), + }, + ) + + assert_layout( + box.grandchild1_1.layout, + { + "origin": (24, 22), + "min_size": (19, 21), + "size": (24, 29), + "content": (10, 16), + "relative": (13, 0, 0, 14), + "absolute": (35, 48, 51, 38), + }, + ) + + +def test_absolute_equalities(box): + # Move the box around and set some borders. + layout = box.node.layout + + layout.origin_top = 100 + layout.origin_left = 200 + + layout.content_top = 50 + layout.content_left = 75 + layout.content_right = 42 + layout.content_bottom = 37 + + assert ( + layout.absolute_content_left + layout.content_width + == layout.absolute_content_right + ) + assert ( + layout.absolute_content_top + layout.content_height + == layout.absolute_content_bottom + ) + + assert ( + layout.content_left + layout.content_width + layout.content_right + == layout.width + ) + assert ( + layout.content_top + layout.content_height + layout.content_bottom + == layout.height + ) diff --git a/travertino/tests/test_node.py b/travertino/tests/test_node.py new file mode 100644 index 0000000000..2f0e76e42d --- /dev/null +++ b/travertino/tests/test_node.py @@ -0,0 +1,467 @@ +from unittest.mock import Mock +from warnings import catch_warnings, filterwarnings + +import pytest + +from tests.utils import mock_attr, prep_style_class +from travertino.declaration import BaseStyle, Choices, validated_property +from travertino.layout import BaseBox, Viewport +from travertino.node import Node +from travertino.size import BaseIntrinsicSize + + +@prep_style_class +@mock_attr("reapply") +class Style(BaseStyle): + int_prop: int = validated_property(Choices(integer=True)) + + class IntrinsicSize(BaseIntrinsicSize): + pass + + class Box(BaseBox): + pass + + def layout(self, viewport): + # A simple layout scheme that allocates twice the viewport size. + self._applicator.node.layout.content_width = viewport.width * 2 + self._applicator.node.layout.content_height = viewport.height * 2 + + +@prep_style_class +class OldStyle(Style): + # Uses two-argument layout(), as in Toga <= 0.4.8 + def layout(self, node, viewport): + # A simple layout scheme that allocates twice the viewport size. + super().layout(viewport) + + +@prep_style_class +class TypeErrorStyle(Style): + # Uses the correct signature, but raises an unrelated TypeError in layout + def layout(self, viewport): + raise TypeError("An unrelated TypeError has occurred somewhere in layout()") + + +@prep_style_class +class OldTypeErrorStyle(Style): + # Just to be extra safe... + def layout(self, node, viewport): + raise TypeError("An unrelated TypeError has occurred somewhere in layout()") + + +@prep_style_class +class BrokenStyle(BaseStyle): + def reapply(self): + raise AttributeError("Missing attribute, node not ready for style application") + + class IntrinsicSize(BaseIntrinsicSize): + pass + + class Box(BaseBox): + pass + + def layout(self, viewport): + # A simple layout scheme that allocates twice the viewport size. + self._applicator.node.layout.content_width = viewport.width * 2 + self._applicator.node.layout.content_height = viewport.height * 2 + + +class AttributeTestStyle(BaseStyle): + class IntrinsicSize(BaseIntrinsicSize): + pass + + class Box(BaseBox): + pass + + def reapply(self): + assert self._applicator.node.style is self + + +def test_create_leaf(): + """A leaf can be created""" + style = Style() + leaf = Node(style=style) + + assert leaf._children is None + assert leaf.children == [] + assert not leaf.can_have_children + + # An unattached leaf is a root + assert leaf.parent is None + assert leaf.root == leaf + + # A leaf can't have children + child = Node(style=style) + + with pytest.raises(ValueError): + leaf.add(child) + + +def test_create_node(): + """A node can be created with children""" + style = Style() + + child1 = Node(style=style) + child2 = Node(style=style) + child3 = Node(style=style) + + node = Node(style=style, children=[child1, child2, child3]) + + assert node.children == [child1, child2, child3] + assert node.can_have_children + + # The node is the root as well. + assert node.parent is None + assert node.root == node + + # The children all point at the node. + assert child1.parent == node + assert child1.root == node + + assert child2.parent == node + assert child2.root == node + + assert child3.parent == node + assert child3.root == node + + # Create another node + new_node = Node(style=style, children=[]) + + assert new_node.children == [] + assert new_node.can_have_children + + # Add the old node as a child of the new one. + new_node.add(node) + + # The new node is the root + assert new_node.parent is None + assert new_node.root == new_node + + # The node is the root as well. + assert node.parent == new_node + assert node.root == new_node + + # The children all point at the node. + assert child1.parent == node + assert child1.root == new_node + + assert child2.parent == node + assert child2.root == new_node + + assert child3.parent == node + assert child3.root == new_node + + +@pytest.mark.parametrize("StyleClass", [Style, OldStyle]) +def test_refresh(StyleClass): + """The layout can be refreshed, and the applicator invoked""" + + # Define an applicator that tracks the node being rendered and its size + class Applicator: + def __init__(self, node): + self.tasks = [] + self.node = node + + def set_bounds(self): + self.tasks.append( + ( + self.node, + self.node.layout.content_width, + self.node.layout.content_height, + ) + ) + + class TestNode(Node): + def __init__(self, style, children=None): + super().__init__( + style=style, applicator=Applicator(self), children=children + ) + + # Define a simple 2 level tree of nodes. + style = StyleClass() + child1 = TestNode(style=style) + child2 = TestNode(style=style) + child3 = TestNode(style=style) + + node = TestNode(style=style, children=[child1, child2, child3]) + + # Refresh the root node + node.refresh(Viewport(width=10, height=20)) + + # Check the output is as expected + assert node.applicator.tasks == [(node, 20, 40)] + assert child1.applicator.tasks == [] + assert child2.applicator.tasks == [] + assert child3.applicator.tasks == [] + + # Reset the applicator + node.applicator.tasks = [] + + # Refresh a child node + child1.refresh(Viewport(width=15, height=25)) + + # The root node was rendered, not the child. + assert node.applicator.tasks == [(node, 30, 50)] + assert child1.applicator.tasks == [] + assert child2.applicator.tasks == [] + assert child3.applicator.tasks == [] + + +@pytest.mark.parametrize("StyleClass", [TypeErrorStyle, OldTypeErrorStyle]) +def test_type_error_in_layout(StyleClass): + """The shim shouldn't hide unrelated TypeErrors.""" + + class Applicator: + def set_bounds(self): + pass + + node = Node(style=StyleClass(), applicator=Applicator()) + with pytest.raises(TypeError, match=r"unrelated TypeError"): + node.refresh(Viewport(50, 50)) + + +def test_add(): + """Nodes can be added as children to another node""" + + style = Style() + node = Node(style=style, children=[]) + + child = Node(style=style) + node.add(child) + + assert child in node.children + assert child.parent == node + assert child.root == node.root + + +def test_insert(): + """Node can be inserted at a specific position as a child""" + + style = Style() + child1 = Node(style=style) + child2 = Node(style=style) + child3 = Node(style=style) + node = Node(style=style, children=[child1, child2, child3]) + + child4 = Node(style=style) + + index = 2 + node.insert(index, child4) + + assert child4 in node.children + assert child4.parent == node + assert child4.root == node.root + + assert node.children.index(child4) == index + + +def test_remove(): + """Children can be removed from node""" + + style = Style() + child1 = Node(style=style) + child2 = Node(style=style) + child3 = Node(style=style) + node = Node(style=style, children=[child1, child2, child3]) + + node.remove(child1) + + assert child1 not in node.children + assert child1.parent is None + assert child1.root == child1 + + +def test_clear(): + """Node can be inserted at a specific position as a child""" + style = Style() + children = [Node(style=style), Node(style=style), Node(style=style)] + node = Node(style=style, children=children) + + for child in children: + assert child in node.children + assert child.parent == node + assert child.root == node + assert node.children == children + + node.clear() + + for child in children: + assert child not in node.children + assert child.parent is None + assert child.root == child + + assert node.children == [] + + +def test_create_with_no_applicator(): + """A node can be created without an applicator.""" + style = Style(int_prop=5) + node = Node(style=style) + + # Style copies on assignment. + assert isinstance(node.style, Style) + assert node.style == style + assert node.style is not style + + # Since no applicator has been assigned, style wasn't applied. + node.style.reapply.assert_not_called() + + +def test_create_with_applicator(): + """A node can be created with an applicator.""" + style = Style(int_prop=5) + applicator = Mock() + node = Node(style=style, applicator=applicator) + + # Style copies on assignment. + assert isinstance(node.style, Style) + assert node.style == style + assert node.style is not style + + # Applicator assignment does *not* copy. + assert node.applicator is applicator + # Applicator gets a reference back to its node and to the style. + assert applicator.node is node + assert node.style._applicator is applicator + + # Assigning a non-None applicator should always apply style. + node.style.reapply.assert_called_once() + + +@pytest.mark.parametrize( + "node", + [ + Node(style=Style()), + Node(style=Style(), applicator=Mock()), + ], +) +def test_assign_applicator(node): + """A node can be assigned an applicator after creation.""" + node.style.reapply.reset_mock() + + applicator = Mock() + node.applicator = applicator + + # Applicator assignment does *not* copy. + assert node.applicator is applicator + # Applicator gets a reference back to its node and to the style. + assert applicator.node is node + assert node.style._applicator is applicator + + # Assigning a non-None applicator should always apply style. + node.style.reapply.assert_called_once() + + +@pytest.mark.parametrize( + "node", + [ + Node(style=Style()), + Node(style=Style(), applicator=Mock()), + ], +) +def test_assign_applicator_none(node): + """A node can have its applicator set to None.""" + node.style.reapply.reset_mock() + + node.applicator = None + assert node.applicator is None + + # Should be updated on style as well + assert node.style._applicator is None + # Assigning None to applicator does not trigger reapply. + node.style.reapply.assert_not_called() + + +def assign_new_applicator(): + """Assigning a new applicator clears reference to node on the old applicator.""" + applicator_1 = Mock() + node = Node(style=Style(), applicator=applicator_1) + + assert applicator_1.node is node + + applicator_2 = Mock() + node.applicator = applicator_2 + + assert applicator_1.node is None + assert applicator_2.node is node + + +def assign_new_applicator_none(): + """Assigning None to applicator clears reference to node on the old applicator.""" + applicator = Mock() + node = Node(style=Style(), applicator=applicator) + + assert applicator.node is node + + node.applicator = None + + assert applicator.node is None + + +def test_assign_style_with_applicator(): + """Assigning a new style triggers a reapply if an applicator is already present.""" + style_1 = Style(int_prop=5) + node = Node(style=style_1, applicator=Mock()) + + node.style.reapply.reset_mock() + style_2 = Style(int_prop=10) + node.style = style_2 + + # Style copies on assignment. + assert isinstance(node.style, Style) + assert node.style == style_2 + assert node.style is not style_2 + + assert node.style != style_1 + + # Since an applicator has already been assigned, assigning style applies the style. + node.style.reapply.assert_called_once() + + +def test_assign_style_with_no_applicator(): + """Assigning new style doesn't trigger a reapply if an applicator isn' present.""" + style_1 = Style(int_prop=5) + node = Node(style=style_1) + + node.style.reapply.reset_mock() + style_2 = Style(int_prop=10) + node.style = style_2 + + # Style copies on assignment. + assert isinstance(node.style, Style) + assert node.style == style_2 + assert node.style is not style_2 + + assert node.style != style_1 + + # Since no applicator was present, style should not be applied. + node.style.reapply.assert_not_called() + + +def test_apply_before_node_is_ready(): + """Triggering a reapply raises a warning if the node is not ready to apply style.""" + style = BrokenStyle() + applicator = Mock() + + with pytest.warns(RuntimeWarning): + node = Node(style=style) + node.applicator = applicator + + with pytest.warns(RuntimeWarning): + node.style = BrokenStyle() + + with pytest.warns(RuntimeWarning): + Node(style=style, applicator=applicator) + + +def test_applicator_has_node_reference(): + """Applicator should have a reference to its node before style is first applied.""" + + # We can't just check it after creating the widget, because at that point the + # reapply will have already happened. AttributeTestStyle has a reapply() method + # that asserts the reference trail of style -> applicator -> node -> style is + # already intact at the point that reapply is called. + + with catch_warnings(): + filterwarnings("error", category=RuntimeWarning) + Node(style=AttributeTestStyle(), applicator=Mock()) diff --git a/travertino/tests/test_size.py b/travertino/tests/test_size.py new file mode 100644 index 0000000000..ebe4cf2b96 --- /dev/null +++ b/travertino/tests/test_size.py @@ -0,0 +1,138 @@ +from typing import NamedTuple +from unittest.mock import Mock + +import pytest + +from travertino.size import BaseIntrinsicSize, at_least + + +class Size(NamedTuple): + width: int + height: int + ratio: float + + def change(self, dimension, value): + return self._replace(**{dimension: value}) + + +BASE_SIZE = Size(width=1, height=2, ratio=0.1) + + +class TestBox: + pass + + +@pytest.fixture +def box(): + box = TestBox() + + box.maxDiff = None + + box.layout = Mock() + box.size = BaseIntrinsicSize(layout=box.layout) + box.size._width, box.size._height, box.size._ratio = BASE_SIZE + + assert_size(box.size, BASE_SIZE) + + return box + + +def assert_size(size, values): + assert (size.width, size.height, size.ratio) == values + + +def test_at_least_repr(): + assert repr(at_least(10)) == "at least 10" + + +def test_size_repr(box): + assert repr(box.size) == "(1, 2)" + box.size.width = at_least(10) + assert repr(box.size) == "(at least 10, 2)" + + +@pytest.mark.parametrize("dimension", ["width", "height"]) +def test_set_dimension(box, dimension): + setattr(box.size, dimension, 10) + assert_size(box.size, BASE_SIZE.change(dimension, 10)) + + # Layout has been dirtied. + box.layout.dirty.assert_called_once_with(**{f"intrinsic_{dimension}": 10}) + + # Clean the layout + box.layout.dirty.reset_mock() + + # Set the width to the same value + setattr(box.size, dimension, 10) + assert_size(box.size, BASE_SIZE.change(dimension, 10)) + + # Layout has NOT been dirtied. + box.layout.dirty.assert_not_called() + + # Set the width to something new + setattr(box.size, dimension, 20) + assert_size(box.size, BASE_SIZE.change(dimension, 20)) + + # Layout has been dirtied. + box.layout.dirty.assert_called_once_with(**{f"intrinsic_{dimension}": 20}) + + +@pytest.mark.parametrize("dimension", ["width", "height"]) +def test_set_dimension_at_least(box, dimension): + setattr(box.size, dimension, at_least(10)) + assert_size(box.size, BASE_SIZE.change(dimension, at_least(10))) + + # Layout has been dirtied. + box.layout.dirty.assert_called_once_with(**{f"intrinsic_{dimension}": at_least(10)}) + + # Clean the layout + box.layout.dirty.reset_mock() + + # Set the width to the same value + setattr(box.size, dimension, at_least(10)) + assert_size(box.size, BASE_SIZE.change(dimension, at_least(10))) + + # Layout has NOT been dirtied. + box.layout.dirty.assert_not_called() + + # Set the width to the same value, but not as a minimum + setattr(box.size, dimension, 10) + assert_size(box.size, BASE_SIZE.change(dimension, 10)) + + # Layout has been dirtied. + box.layout.dirty.assert_called_once_with(**{f"intrinsic_{dimension}": 10}) + + # Clean the layout + box.layout.dirty.reset_mock() + + # Set the width to something new + setattr(box.size, dimension, at_least(20)) + assert_size(box.size, BASE_SIZE.change(dimension, at_least(20))) + + # Layout has been dirtied. + box.layout.dirty.assert_called_once_with(**{f"intrinsic_{dimension}": at_least(20)}) + + +def test_set_ratio(box): + box.size.ratio = 0.5 + assert_size(box.size, (1, 2, 0.5)) + + # Layout has been dirtied. + box.layout.dirty.assert_called_once_with(intrinsic_ratio=0.5) + + # Clean the layout + box.layout.dirty.reset_mock() + + # Set the ratio to the same value + box.size.ratio = 0.5 + assert_size(box.size, (1, 2, 0.5)) + + # Layout has NOT been dirtied. + box.layout.dirty.assert_not_called() + + # Set the ratio to something else + box.size.ratio = 0.75 + assert_size(box.size, (1, 2, 0.75)) + + # Layout has been dirtied. + box.layout.dirty.assert_called_once_with(intrinsic_ratio=0.75) diff --git a/travertino/tests/utils.py b/travertino/tests/utils.py new file mode 100644 index 0000000000..0649494db8 --- /dev/null +++ b/travertino/tests/utils.py @@ -0,0 +1,29 @@ +import sys +from dataclasses import dataclass +from unittest.mock import Mock + +if sys.version_info < (3, 10): + _DATACLASS_KWARGS = {"init": False} +else: + _DATACLASS_KWARGS = {"kw_only": True} + + +def prep_style_class(cls): + """Decorator to apply dataclass and mock apply.""" + return mock_attr("apply")(dataclass(**_DATACLASS_KWARGS)(cls)) + + +def mock_attr(attr): + """Mock an arbitrary attribute of a class.""" + + def returned_decorator(cls): + orig_init = cls.__init__ + + def __init__(self, *args, **kwargs): + setattr(self, attr, Mock()) + orig_init(self, *args, **kwargs) + + cls.__init__ = __init__ + return cls + + return returned_decorator diff --git a/travertino/tox.ini b/travertino/tox.ini new file mode 100644 index 0000000000..c18af7cef6 --- /dev/null +++ b/travertino/tox.ini @@ -0,0 +1,41 @@ +# Flake8 doesn't believe in pyproject.toml, so we put the configuration here. +[flake8] +exclude= + local/*,\ + docs/*,\ + build/*,\ + dist/*,\ + .tox/*, \ + venv* +max-complexity = 25 +max-line-length = 119 +ignore = + # line break occurred before a binary operator + W503, + +[tox] +envlist = towncrier-check,pre-commit,py{39,310,311,312,313,314} +skip_missing_interpreters = true + +[testenv:pre-commit] +package = wheel +wheel_build_env = .pkg +extras = dev +commands = pre-commit run --all-files --show-diff-on-failure --color=always + +[testenv:py{,39,310,311,312,313,314}] +package = wheel +wheel_build_env = .pkg +depends = + towncrier-check + pre-commit +extras = dev +commands = python -m pytest {posargs:-vv --color yes} + +[testenv:towncrier{,-check}] +skip_install = True +deps = + towncrier==24.8.0 +commands = + check : python -m towncrier.check --compare-with origin/main + !check : python -m towncrier {posargs} From 3349fbd233eeb5394d9a56b352826b37c9579052 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 9 Jan 2025 19:19:05 -0500 Subject: [PATCH 02/53] Move tests and fix configurations --- .github/workflows/config-file-deps-bump.yml | 2 +- .github/workflows/publish.yml | 1 + .github/workflows/release.yml | 1 + core/pyproject.toml | 8 ++-- .../tests/travertino}/__init__.py | 0 .../tests/travertino}/colors/__init__.py | 0 .../travertino}/colors/test_constructor.py | 0 .../tests/travertino}/colors/test_parsing.py | 0 .../tests/travertino}/fonts/__init__.py | 0 .../travertino}/fonts/test_constructor.py | 0 .../tests/travertino}/fonts/test_parsing.py | 2 +- .../tests/travertino}/test_choices.py | 2 +- .../tests/travertino}/test_declaration.py | 2 +- .../tests/travertino}/test_layout.py | 0 .../tests/travertino}/test_node.py | 2 +- .../tests/travertino}/test_size.py | 0 .../tests => core/tests/travertino}/utils.py | 0 tox.ini | 2 +- travertino/pyproject.toml | 39 +++++++----------- travertino/tox.ini | 41 ------------------- 20 files changed, 27 insertions(+), 75 deletions(-) rename {travertino/tests => core/tests/travertino}/__init__.py (100%) rename {travertino/tests => core/tests/travertino}/colors/__init__.py (100%) rename {travertino/tests => core/tests/travertino}/colors/test_constructor.py (100%) rename {travertino/tests => core/tests/travertino}/colors/test_parsing.py (100%) rename {travertino/tests => core/tests/travertino}/fonts/__init__.py (100%) rename {travertino/tests => core/tests/travertino}/fonts/test_constructor.py (100%) rename {travertino/tests => core/tests/travertino}/fonts/test_parsing.py (98%) rename {travertino/tests => core/tests/travertino}/test_choices.py (99%) rename {travertino/tests => core/tests/travertino}/test_declaration.py (99%) rename {travertino/tests => core/tests/travertino}/test_layout.py (100%) rename {travertino/tests => core/tests/travertino}/test_node.py (99%) rename {travertino/tests => core/tests/travertino}/test_size.py (100%) rename {travertino/tests => core/tests/travertino}/utils.py (100%) delete mode 100644 travertino/tox.ini diff --git a/.github/workflows/config-file-deps-bump.yml b/.github/workflows/config-file-deps-bump.yml index f8d42de729..5ccd2b0b33 100644 --- a/.github/workflows/config-file-deps-bump.yml +++ b/.github/workflows/config-file-deps-bump.yml @@ -11,4 +11,4 @@ jobs: uses: beeware/.github/.github/workflows/dep-version-bump.yml@main secrets: inherit with: - subdirectory: . core dummy android cocoa demo gtk iOS testbed textual toga web winforms + subdirectory: . core dummy android cocoa demo gtk iOS testbed textual toga travertino web winforms diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 662300f168..a713e53b4b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,6 +27,7 @@ jobs: - "toga_iOS" - "toga_web" - "toga_winforms" + - "travertino" steps: - name: Get packages uses: dsaltares/fetch-gh-release-asset@1.1.2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c42ac3245..33e80c9c75 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,6 +70,7 @@ jobs: - "toga_textual" - "toga_web" - "toga_winforms" + - "travertino" steps: - name: Get Packages uses: actions/download-artifact@v4.1.8 diff --git a/core/pyproject.toml b/core/pyproject.toml index 85a9dff2d7..a9e9042b33 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -55,9 +55,6 @@ classifiers = [ "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets", ] -dependencies = [ - "travertino >= 0.3.0, < 0.4.0", -] [project.optional-dependencies] # Extras used by developers *of* Toga are pinned to specific versions to @@ -104,6 +101,11 @@ pil = "toga.plugins.image_formats.PILConverter" [tool.setuptools_scm] root = ".." +[tool.setuptools_dynamic_dependencies] +dependencies = [ + "travertino == {version}", +] + [tool.coverage.run] parallel = true branch = true diff --git a/travertino/tests/__init__.py b/core/tests/travertino/__init__.py similarity index 100% rename from travertino/tests/__init__.py rename to core/tests/travertino/__init__.py diff --git a/travertino/tests/colors/__init__.py b/core/tests/travertino/colors/__init__.py similarity index 100% rename from travertino/tests/colors/__init__.py rename to core/tests/travertino/colors/__init__.py diff --git a/travertino/tests/colors/test_constructor.py b/core/tests/travertino/colors/test_constructor.py similarity index 100% rename from travertino/tests/colors/test_constructor.py rename to core/tests/travertino/colors/test_constructor.py diff --git a/travertino/tests/colors/test_parsing.py b/core/tests/travertino/colors/test_parsing.py similarity index 100% rename from travertino/tests/colors/test_parsing.py rename to core/tests/travertino/colors/test_parsing.py diff --git a/travertino/tests/fonts/__init__.py b/core/tests/travertino/fonts/__init__.py similarity index 100% rename from travertino/tests/fonts/__init__.py rename to core/tests/travertino/fonts/__init__.py diff --git a/travertino/tests/fonts/test_constructor.py b/core/tests/travertino/fonts/test_constructor.py similarity index 100% rename from travertino/tests/fonts/test_constructor.py rename to core/tests/travertino/fonts/test_constructor.py diff --git a/travertino/tests/fonts/test_parsing.py b/core/tests/travertino/fonts/test_parsing.py similarity index 98% rename from travertino/tests/fonts/test_parsing.py rename to core/tests/travertino/fonts/test_parsing.py index 1029f8e977..516d08b8ec 100644 --- a/travertino/tests/fonts/test_parsing.py +++ b/core/tests/travertino/fonts/test_parsing.py @@ -1,6 +1,6 @@ import pytest +from tests.travertino.fonts.test_constructor import assert_font -from tests.fonts.test_constructor import assert_font from travertino.constants import ( BOLD, ITALIC, diff --git a/travertino/tests/test_choices.py b/core/tests/travertino/test_choices.py similarity index 99% rename from travertino/tests/test_choices.py rename to core/tests/travertino/test_choices.py index b9ef2c865f..6928b6284e 100644 --- a/travertino/tests/test_choices.py +++ b/core/tests/travertino/test_choices.py @@ -3,8 +3,8 @@ from warnings import catch_warnings, filterwarnings import pytest +from tests.travertino.utils import mock_attr, prep_style_class -from tests.utils import mock_attr, prep_style_class from travertino.colors import NAMED_COLOR, rgb from travertino.constants import GOLDENROD, NONE, REBECCAPURPLE, TOP from travertino.declaration import BaseStyle, Choices, validated_property diff --git a/travertino/tests/test_declaration.py b/core/tests/travertino/test_declaration.py similarity index 99% rename from travertino/tests/test_declaration.py rename to core/tests/travertino/test_declaration.py index c2f1414630..57fa7cef12 100644 --- a/travertino/tests/test_declaration.py +++ b/core/tests/travertino/test_declaration.py @@ -4,8 +4,8 @@ from warnings import catch_warnings, filterwarnings import pytest +from tests.travertino.utils import mock_attr, prep_style_class -from tests.utils import mock_attr, prep_style_class from travertino.declaration import ( BaseStyle, Choices, diff --git a/travertino/tests/test_layout.py b/core/tests/travertino/test_layout.py similarity index 100% rename from travertino/tests/test_layout.py rename to core/tests/travertino/test_layout.py diff --git a/travertino/tests/test_node.py b/core/tests/travertino/test_node.py similarity index 99% rename from travertino/tests/test_node.py rename to core/tests/travertino/test_node.py index 2f0e76e42d..f04bc0dccc 100644 --- a/travertino/tests/test_node.py +++ b/core/tests/travertino/test_node.py @@ -2,8 +2,8 @@ from warnings import catch_warnings, filterwarnings import pytest +from tests.travertino.utils import mock_attr, prep_style_class -from tests.utils import mock_attr, prep_style_class from travertino.declaration import BaseStyle, Choices, validated_property from travertino.layout import BaseBox, Viewport from travertino.node import Node diff --git a/travertino/tests/test_size.py b/core/tests/travertino/test_size.py similarity index 100% rename from travertino/tests/test_size.py rename to core/tests/travertino/test_size.py diff --git a/travertino/tests/utils.py b/core/tests/travertino/utils.py similarity index 100% rename from travertino/tests/utils.py rename to core/tests/travertino/utils.py diff --git a/tox.ini b/tox.ini index 847c4a8107..5893746451 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ allowlist_externals = commands = # TOGA_INSTALL_COMMAND is set to a bash command by the CI workflow # Install as editable so parallel runs don't clobber the build directory for each other - {env:TOGA_INSTALL_COMMAND:python -m pip install {tox_root}{/}core[dev] {tox_root}{/}dummy} + {env:TOGA_INSTALL_COMMAND:python -m pip install {tox_root}{/}core[dev] {tox_root}{/}dummy {tox_root}{/}travertino} !cov: python -m pytest {posargs:-vv --color yes} cov : python -m coverage run -m pytest {posargs:-vv --color yes} diff --git a/travertino/pyproject.toml b/travertino/pyproject.toml index 55a918f88a..cb87c00b89 100644 --- a/travertino/pyproject.toml +++ b/travertino/pyproject.toml @@ -40,16 +40,6 @@ classifiers = [ "Topic :: Software Development :: User Interfaces", ] -[project.optional-dependencies] -# Extras used by developers *of* Travertino are pinned to specific versions to -# ensure environment consistency. -dev = [ - "pre-commit == 4.0.1", - "pytest == 8.3.4", - "setuptools_scm == 8.1.0", - "tox == 4.23.2", -] - [project.urls] Homepage = "https://beeware.org/travertino" Funding = "https://beeware.org/contributing/membership/" @@ -63,20 +53,19 @@ split_on_trailing_comma = true combine_as_imports = true [tool.setuptools_scm] -# To enable SCM versioning, we need an empty tool configuration for setuptools_scm +root = ".." + +[tool.coverage.run] +parallel = true +branch = true +relative_files = true + +# See notes in the root pyproject.toml file. +source = ["src"] +source_pkgs = ["travertino"] -[tool.towncrier] -directory = "changes" -package = "travertino" -package_dir = "src" -filename = "CHANGELOG.rst" -title_format = "{version} ({project_date})" -issue_format = "`#{issue} `_" -template = "changes/template.rst" -type = [ - { directory = "feature", name = "Features", showcontent = true }, - { directory = "bugfix", name = "Bugfixes", showcontent = true }, - { directory = "removal", name = "Backward Incompatible Changes", showcontent = true }, - { directory = "doc", name = "Documentation", showcontent = true }, - { directory = "misc", name = "Misc", showcontent = false }, +[tool.coverage.paths] +source = [ + "src/travertino", + "**/travertino", ] diff --git a/travertino/tox.ini b/travertino/tox.ini deleted file mode 100644 index c18af7cef6..0000000000 --- a/travertino/tox.ini +++ /dev/null @@ -1,41 +0,0 @@ -# Flake8 doesn't believe in pyproject.toml, so we put the configuration here. -[flake8] -exclude= - local/*,\ - docs/*,\ - build/*,\ - dist/*,\ - .tox/*, \ - venv* -max-complexity = 25 -max-line-length = 119 -ignore = - # line break occurred before a binary operator - W503, - -[tox] -envlist = towncrier-check,pre-commit,py{39,310,311,312,313,314} -skip_missing_interpreters = true - -[testenv:pre-commit] -package = wheel -wheel_build_env = .pkg -extras = dev -commands = pre-commit run --all-files --show-diff-on-failure --color=always - -[testenv:py{,39,310,311,312,313,314}] -package = wheel -wheel_build_env = .pkg -depends = - towncrier-check - pre-commit -extras = dev -commands = python -m pytest {posargs:-vv --color yes} - -[testenv:towncrier{,-check}] -skip_install = True -deps = - towncrier==24.8.0 -commands = - check : python -m towncrier.check --compare-with origin/main - !check : python -m towncrier {posargs} From c1eace4d97b869c0d660afdff1745d5bcd1fc06d Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 9 Jan 2025 19:30:52 -0500 Subject: [PATCH 03/53] Switch to dataclass style properties --- core/src/toga/style/pack.py | 112 ++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index eafeed4e37..5638aa039f 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -1,8 +1,19 @@ from __future__ import annotations import warnings -from typing import Any +from typing import TYPE_CHECKING, Any +if TYPE_CHECKING: + from travertino.colors import rgb, hsl + +from toga.fonts import ( + FONT_STYLES, + FONT_VARIANTS, + FONT_WEIGHTS, + SYSTEM_DEFAULT_FONT_SIZE, + SYSTEM_DEFAULT_FONTS, + Font, +) from travertino.constants import ( # noqa: F401 BOLD, BOTTOM, @@ -30,19 +41,15 @@ TRANSPARENT, VISIBLE, ) -from travertino.declaration import BaseStyle, Choices +from travertino.declaration import ( + BaseStyle, + Choices, + directional_property, + validated_property, +) from travertino.layout import BaseBox from travertino.size import BaseIntrinsicSize -from toga.fonts import ( - FONT_STYLES, - FONT_VARIANTS, - FONT_WEIGHTS, - SYSTEM_DEFAULT_FONT_SIZE, - SYSTEM_DEFAULT_FONTS, - Font, -) - # Make sure deprecation warnings are shown by default warnings.filterwarnings("default", category=DeprecationWarning) @@ -99,6 +106,49 @@ class IntrinsicSize(BaseIntrinsicSize): _depth = -1 + display: str = validated_property(choices=DISPLAY_CHOICES, initial=PACK) + visibility: str = validated_property(choices=VISIBILITY_CHOICES, initial=VISIBLE) + direction: str = validated_property(choices=DIRECTION_CHOICES, initial=ROW) + align_items: str | None = validated_property(choices=ALIGN_ITEMS_CHOICES) + alignment: str | None = validated_property(choices=ALIGNMENT_CHOICES) # Deprecated + justify_content: str | None = validated_property( + choices=JUSTIFY_CONTENT_CHOICES, initial=START + ) + gap: int = validated_property(choices=GAP_CHOICES, initial=0) + + width: str | int = validated_property(choices=SIZE_CHOICES, initial=NONE) + height: str | int = validated_property(choices=SIZE_CHOICES, initial=NONE) + flex: float = validated_property(choices=FLEX_CHOICES, initial=0) + + margin: int | tuple[int] = directional_property("margin{}") + margin_top: int = validated_property(choices=MARGIN_CHOICES, initial=0) + margin_right: int = validated_property(choices=MARGIN_CHOICES, initial=0) + margin_bottom: int = validated_property(choices=MARGIN_CHOICES, initial=0) + margin_left: int = validated_property(choices=MARGIN_CHOICES, initial=0) + + color: rgb | hsl | str | None = validated_property(choices=COLOR_CHOICES) + background_color: rgb | hsl | str | None = validated_property( + choices=BACKGROUND_COLOR_CHOICES + ) + + text_align: str | None = validated_property(choices=TEXT_ALIGN_CHOICES) + text_direction: str | None = validated_property( + choices=TEXT_DIRECTION_CHOICES, initial=LTR + ) + + font_family: str = validated_property(choices=FONT_FAMILY_CHOICES, initial=SYSTEM) + # Pack.list_property('font_family', choices=FONT_FAMILY_CHOICES) + font_style: str = validated_property(choices=FONT_STYLE_CHOICES, initial=NORMAL) + font_variant: str = validated_property(choices=FONT_VARIANT_CHOICES, initial=NORMAL) + font_weight: str = validated_property(choices=FONT_WEIGHT_CHOICES, initial=NORMAL) + font_size: int = validated_property( + choices=FONT_SIZE_CHOICES, initial=SYSTEM_DEFAULT_FONT_SIZE + ) + # Pack.composite_property([ + # 'font_family', 'font_style', 'font_variant', 'font_weight', 'font_size' + # FONT_CHOICES + # ]) + @classmethod def _debug(cls, *args: str) -> None: # pragma: no cover print(" " * cls._depth, *args) @@ -937,43 +987,3 @@ def __css__(self) -> str: css.append(f"font-variant: {self.font_variant};") return " ".join(css) - - -Pack.validated_property("display", choices=DISPLAY_CHOICES, initial=PACK) -Pack.validated_property("visibility", choices=VISIBILITY_CHOICES, initial=VISIBLE) -Pack.validated_property("direction", choices=DIRECTION_CHOICES, initial=ROW) -Pack.validated_property("align_items", choices=ALIGN_ITEMS_CHOICES) -Pack.validated_property("alignment", choices=ALIGNMENT_CHOICES) # Deprecated -Pack.validated_property( - "justify_content", choices=JUSTIFY_CONTENT_CHOICES, initial=START -) -Pack.validated_property("gap", choices=GAP_CHOICES, initial=0) - -Pack.validated_property("width", choices=SIZE_CHOICES, initial=NONE) -Pack.validated_property("height", choices=SIZE_CHOICES, initial=NONE) -Pack.validated_property("flex", choices=FLEX_CHOICES, initial=0) - -Pack.validated_property("margin_top", choices=MARGIN_CHOICES, initial=0) -Pack.validated_property("margin_right", choices=MARGIN_CHOICES, initial=0) -Pack.validated_property("margin_bottom", choices=MARGIN_CHOICES, initial=0) -Pack.validated_property("margin_left", choices=MARGIN_CHOICES, initial=0) -Pack.directional_property("margin%s") - -Pack.validated_property("color", choices=COLOR_CHOICES) -Pack.validated_property("background_color", choices=BACKGROUND_COLOR_CHOICES) - -Pack.validated_property("text_align", choices=TEXT_ALIGN_CHOICES) -Pack.validated_property("text_direction", choices=TEXT_DIRECTION_CHOICES, initial=LTR) - -Pack.validated_property("font_family", choices=FONT_FAMILY_CHOICES, initial=SYSTEM) -# Pack.list_property('font_family', choices=FONT_FAMILY_CHOICES) -Pack.validated_property("font_style", choices=FONT_STYLE_CHOICES, initial=NORMAL) -Pack.validated_property("font_variant", choices=FONT_VARIANT_CHOICES, initial=NORMAL) -Pack.validated_property("font_weight", choices=FONT_WEIGHT_CHOICES, initial=NORMAL) -Pack.validated_property( - "font_size", choices=FONT_SIZE_CHOICES, initial=SYSTEM_DEFAULT_FONT_SIZE -) -# Pack.composite_property([ -# 'font_family', 'font_style', 'font_variant', 'font_weight', 'font_size' -# FONT_CHOICES -# ]) From 7cd22b2d1a570d6f734d791ab87bbd29532d66ff Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 9 Jan 2025 19:38:15 -0500 Subject: [PATCH 04/53] Remove Travertino 0.3.0 compat code in Toga --- core/src/toga/style/pack.py | 14 +--------- core/src/toga/widgets/base.py | 26 ++----------------- .../style/pack/layout/test_justify_content.py | 6 ++--- core/tests/style/pack/utils.py | 16 +----------- dummy/src/toga_dummy/utils.py | 11 +++----- 5 files changed, 10 insertions(+), 63 deletions(-) diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 5638aa039f..577f04f9f7 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -344,19 +344,7 @@ def apply(self, prop: str, value: object) -> None: # so perform a refresh. self._applicator.refresh() - def layout(self, viewport: Any, _deprecated_usage=None) -> None: - ###################################################################### - # 2024-12: Backwards compatibility for Travertino 0.3.0 - ###################################################################### - - if _deprecated_usage is not None: - # Was called with (self, viewport) - viewport = _deprecated_usage - - ###################################################################### - # End backwards compatibility - ###################################################################### - + def layout(self, viewport: Any) -> None: # self._debug("=" * 80) # self._debug( # f"Layout root {node}, available {viewport.width}x{viewport.height}" diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 1df9d789c5..e1098f052c 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -4,11 +4,10 @@ from typing import TYPE_CHECKING, Any, TypeVar from warnings import warn -from travertino.declaration import BaseStyle -from travertino.node import Node - from toga.platform import get_platform_factory from toga.style import Pack, TogaApplicator +from travertino.declaration import BaseStyle +from travertino.node import Node if TYPE_CHECKING: from toga.app import App @@ -65,27 +64,6 @@ def __init__( self.applicator = TogaApplicator() - ############################################## - # Backwards compatibility for Travertino 0.3.0 - ############################################## - - # The below if block will execute when using Travertino 0.3.0. For future - # versions of Travertino, these assignments (and the reapply) will already have - # been handled "automatically" by assigning the applicator above; in that case, - # we want to avoid doing a second, redundant style reapplication. - - # This whole section can be removed as soon as there's a newer version of - # Travertino to set as Toga's minimum requirement. - - if not hasattr(self.applicator, "node"): # pragma: no cover - self.applicator.node = self - self.style._applicator = self.applicator - self.style.reapply() - - ############################# - # End backwards compatibility - ############################# - def _create(self) -> Any: """Create a platform-specific implementation of this widget. diff --git a/core/tests/style/pack/layout/test_justify_content.py b/core/tests/style/pack/layout/test_justify_content.py index a84a1c6c01..2876eaa48e 100644 --- a/core/tests/style/pack/layout/test_justify_content.py +++ b/core/tests/style/pack/layout/test_justify_content.py @@ -1,7 +1,7 @@ import pytest -from travertino.size import at_least from toga.style.pack import Pack +from travertino.size import at_least from ..utils import ExampleNode, ExampleViewport, assert_layout @@ -52,7 +52,7 @@ def test_justify_content( if text_direction: root.style.text_direction = text_direction - root.style.layout(root, viewport) + root.style.layout(viewport) assert_layout( root, (240, 120) if direction == "row" else (120, 240), @@ -96,7 +96,7 @@ def test_justify_content_flex( delattr(child_style, "width" if direction == "row" else "height") child_style.flex = 1 - root.style.layout(root, viewport) + root.style.layout(viewport) assert_layout( root, (140, 120) if direction == "row" else (120, 140), diff --git a/core/tests/style/pack/utils.py b/core/tests/style/pack/utils.py index 86c3f1f8d9..6e0c7cb2eb 100644 --- a/core/tests/style/pack/utils.py +++ b/core/tests/style/pack/utils.py @@ -1,8 +1,7 @@ from unittest.mock import Mock -from travertino.node import Node - from toga.style.applicator import TogaApplicator +from travertino.node import Node class ExampleNode(Node): @@ -12,19 +11,6 @@ def __init__(self, name, style, size=None, children=None): super().__init__(style=style, children=children, applicator=TogaApplicator()) - ############################################## - # Backwards compatibility for Travertino 0.3.0 - ############################################## - - if not hasattr(self.applicator, "node"): - self.applicator.node = self - self.style._applicator = self.applicator - self.style.reapply() - - ############################# - # End backwards compatibility - ############################# - self.name = name self._impl = Mock() if size: diff --git a/dummy/src/toga_dummy/utils.py b/dummy/src/toga_dummy/utils.py index d5ab2e5f32..c29b99f265 100644 --- a/dummy/src/toga_dummy/utils.py +++ b/dummy/src/toga_dummy/utils.py @@ -1,6 +1,7 @@ from unittest.mock import Mock import pytest + from travertino.declaration import BaseStyle from travertino.layout import BaseBox from travertino.size import BaseIntrinsicSize @@ -371,15 +372,9 @@ def assert_action_performed_with(_widget, _action, **test_data): found = False except AttributeError: # No raw attribute; use the provided value as-is - try: - if data[key] != value: - found = False - ######################################################## - # Backwards compatibility for Travertino 0.3.0 - # Font.__eq__ throws an AttributeError against non-Fonts - ######################################################## - except AttributeError: + if data[key] != value: found = False + except KeyError: found = False From 105afca6ec1a3c2fb0d91d59fd0ce2ccebefb387 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 9 Jan 2025 19:46:53 -0500 Subject: [PATCH 05/53] Added changenote --- changes/3086.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/3086.misc.rst diff --git a/changes/3086.misc.rst b/changes/3086.misc.rst new file mode 100644 index 0000000000..5a8418326c --- /dev/null +++ b/changes/3086.misc.rst @@ -0,0 +1 @@ +Travertino has moved from its own repository to Toga's and is now released to coincide with each version release of Toga. From 30b270d0c713ea8dd2f55230f1ccb2c9a2d29652 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 9 Jan 2025 19:49:51 -0500 Subject: [PATCH 06/53] Ran pre-commit to reorder imports across Toga --- android/src/toga_android/colors.py | 1 + android/src/toga_android/widgets/base.py | 2 +- android/src/toga_android/widgets/button.py | 1 + android/src/toga_android/widgets/divider.py | 1 + android/src/toga_android/widgets/internal/pickers.py | 1 + android/src/toga_android/widgets/label.py | 2 +- android/src/toga_android/widgets/multilinetextinput.py | 1 + android/src/toga_android/widgets/optioncontainer.py | 2 +- android/src/toga_android/widgets/switch.py | 1 + cocoa/src/toga_cocoa/widgets/box.py | 3 +-- cocoa/src/toga_cocoa/widgets/button.py | 2 +- cocoa/src/toga_cocoa/widgets/canvas.py | 2 +- cocoa/src/toga_cocoa/widgets/detailedlist.py | 2 +- cocoa/src/toga_cocoa/widgets/divider.py | 3 +-- cocoa/src/toga_cocoa/widgets/label.py | 3 +-- cocoa/src/toga_cocoa/widgets/mapview.py | 2 +- cocoa/src/toga_cocoa/widgets/multilinetextinput.py | 2 +- cocoa/src/toga_cocoa/widgets/numberinput.py | 2 +- cocoa/src/toga_cocoa/widgets/optioncontainer.py | 2 +- cocoa/src/toga_cocoa/widgets/progressbar.py | 3 +-- cocoa/src/toga_cocoa/widgets/scrollcontainer.py | 2 +- cocoa/src/toga_cocoa/widgets/selection.py | 3 +-- cocoa/src/toga_cocoa/widgets/slider.py | 3 +-- cocoa/src/toga_cocoa/widgets/splitcontainer.py | 2 +- cocoa/src/toga_cocoa/widgets/switch.py | 3 +-- cocoa/src/toga_cocoa/widgets/table.py | 2 +- cocoa/src/toga_cocoa/widgets/textinput.py | 3 +-- cocoa/src/toga_cocoa/widgets/tree.py | 2 +- cocoa/src/toga_cocoa/widgets/webview.py | 2 +- core/src/toga/fonts.py | 6 +++--- core/src/toga/widgets/canvas/canvas.py | 3 +-- core/src/toga/widgets/canvas/context.py | 3 +-- core/src/toga/widgets/canvas/drawingobject.py | 3 +-- core/src/toga/widgets/imageview.py | 3 +-- core/tests/style/pack/layout/test_beeliza.py | 3 +-- core/tests/style/pack/layout/test_fixed.py | 3 +-- core/tests/style/pack/layout/test_flex.py | 3 +-- core/tests/style/pack/layout/test_gap.py | 3 +-- core/tests/style/pack/layout/test_tutorial0.py | 3 +-- core/tests/style/pack/layout/test_tutorial1.py | 3 +-- core/tests/style/pack/layout/test_tutorial3.py | 3 +-- core/tests/widgets/test_imageview.py | 2 +- examples/canvas/canvas/app.py | 3 +-- examples/colors/colors/app.py | 3 +-- examples/focus/focus/app.py | 3 +-- gtk/src/toga_gtk/widgets/button.py | 3 +-- gtk/src/toga_gtk/widgets/canvas.py | 3 +-- gtk/src/toga_gtk/widgets/detailedlist.py | 3 +-- gtk/src/toga_gtk/widgets/mapview.py | 3 +-- gtk/src/toga_gtk/widgets/numberinput.py | 3 +-- gtk/src/toga_gtk/widgets/slider.py | 3 +-- gtk/src/toga_gtk/widgets/table.py | 3 +-- gtk/src/toga_gtk/widgets/textinput.py | 3 +-- gtk/src/toga_gtk/widgets/webview.py | 3 +-- iOS/src/toga_iOS/widgets/box.py | 2 +- iOS/src/toga_iOS/widgets/button.py | 2 +- iOS/src/toga_iOS/widgets/canvas.py | 2 +- iOS/src/toga_iOS/widgets/detailedlist.py | 2 +- iOS/src/toga_iOS/widgets/divider.py | 3 +-- iOS/src/toga_iOS/widgets/label.py | 2 +- iOS/src/toga_iOS/widgets/mapview.py | 2 +- iOS/src/toga_iOS/widgets/multilinetextinput.py | 2 +- iOS/src/toga_iOS/widgets/numberinput.py | 2 +- iOS/src/toga_iOS/widgets/optioncontainer.py | 2 +- iOS/src/toga_iOS/widgets/progressbar.py | 3 +-- iOS/src/toga_iOS/widgets/scrollcontainer.py | 2 +- iOS/src/toga_iOS/widgets/selection.py | 2 +- iOS/src/toga_iOS/widgets/slider.py | 2 +- iOS/src/toga_iOS/widgets/switch.py | 2 +- iOS/src/toga_iOS/widgets/textinput.py | 2 +- iOS/src/toga_iOS/widgets/webview.py | 2 +- textual/src/toga_textual/widgets/base.py | 3 +-- textual/src/toga_textual/widgets/box.py | 3 +-- textual/src/toga_textual/widgets/button.py | 3 +-- textual/src/toga_textual/widgets/label.py | 3 +-- textual/src/toga_textual/widgets/textinput.py | 3 +-- winforms/src/toga_winforms/colors.py | 1 + winforms/src/toga_winforms/widgets/base.py | 2 +- winforms/src/toga_winforms/widgets/button.py | 1 + winforms/src/toga_winforms/widgets/divider.py | 1 + winforms/src/toga_winforms/widgets/label.py | 2 +- winforms/src/toga_winforms/widgets/multilinetextinput.py | 2 +- winforms/src/toga_winforms/widgets/scrollcontainer.py | 2 +- winforms/src/toga_winforms/widgets/switch.py | 1 + 84 files changed, 86 insertions(+), 114 deletions(-) diff --git a/android/src/toga_android/colors.py b/android/src/toga_android/colors.py index ffd8a71e30..0c23dce56c 100644 --- a/android/src/toga_android/colors.py +++ b/android/src/toga_android/colors.py @@ -1,4 +1,5 @@ from android.graphics import Color + from travertino.colors import TRANSPARENT CACHE = {TRANSPARENT: Color.TRANSPARENT} diff --git a/android/src/toga_android/widgets/base.py b/android/src/toga_android/widgets/base.py index 3d25006bb1..6a3d1e02d1 100644 --- a/android/src/toga_android/widgets/base.py +++ b/android/src/toga_android/widgets/base.py @@ -6,9 +6,9 @@ from android.view import Gravity, View from android.widget import RelativeLayout from org.beeware.android import MainActivity -from travertino.size import at_least from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT, TRANSPARENT +from travertino.size import at_least from ..colors import native_color diff --git a/android/src/toga_android/widgets/button.py b/android/src/toga_android/widgets/button.py index a095f9b07e..9207b8c9cb 100644 --- a/android/src/toga_android/widgets/button.py +++ b/android/src/toga_android/widgets/button.py @@ -3,6 +3,7 @@ from android.view import View from android.widget import Button as A_Button from java import dynamic_proxy + from travertino.size import at_least from .label import TextViewWidget diff --git a/android/src/toga_android/widgets/divider.py b/android/src/toga_android/widgets/divider.py index 3c856abac2..9f9db1e989 100644 --- a/android/src/toga_android/widgets/divider.py +++ b/android/src/toga_android/widgets/divider.py @@ -1,6 +1,7 @@ from android.graphics import Color from android.view import View from android.widget import LinearLayout + from travertino.size import at_least from .base import Widget diff --git a/android/src/toga_android/widgets/internal/pickers.py b/android/src/toga_android/widgets/internal/pickers.py index 61e00a1d46..0a020bc0d5 100644 --- a/android/src/toga_android/widgets/internal/pickers.py +++ b/android/src/toga_android/widgets/internal/pickers.py @@ -4,6 +4,7 @@ from android.view import View from android.widget import EditText from java import dynamic_proxy + from travertino.size import at_least from ..label import TextViewWidget diff --git a/android/src/toga_android/widgets/label.py b/android/src/toga_android/widgets/label.py index 0aaa149e20..3ba18337f3 100644 --- a/android/src/toga_android/widgets/label.py +++ b/android/src/toga_android/widgets/label.py @@ -5,10 +5,10 @@ from android.util import TypedValue from android.view import Gravity, View from android.widget import TextView -from travertino.size import at_least from toga.constants import JUSTIFY from toga_android.colors import native_color +from travertino.size import at_least from .base import Widget, android_text_align diff --git a/android/src/toga_android/widgets/multilinetextinput.py b/android/src/toga_android/widgets/multilinetextinput.py index 545a65127f..2dac2fb7c8 100644 --- a/android/src/toga_android/widgets/multilinetextinput.py +++ b/android/src/toga_android/widgets/multilinetextinput.py @@ -1,5 +1,6 @@ from android.text import InputType from android.view import Gravity + from travertino.size import at_least from .textinput import TextInput diff --git a/android/src/toga_android/widgets/optioncontainer.py b/android/src/toga_android/widgets/optioncontainer.py index c44f09cf94..694a309861 100644 --- a/android/src/toga_android/widgets/optioncontainer.py +++ b/android/src/toga_android/widgets/optioncontainer.py @@ -16,9 +16,9 @@ NavigationBarView = None from java import dynamic_proxy -from travertino.size import at_least import toga +from travertino.size import at_least from ..container import Container from .base import Widget diff --git a/android/src/toga_android/widgets/switch.py b/android/src/toga_android/widgets/switch.py index d1202171e1..888f6daf75 100644 --- a/android/src/toga_android/widgets/switch.py +++ b/android/src/toga_android/widgets/switch.py @@ -3,6 +3,7 @@ from android.view import View from android.widget import CompoundButton, Switch as A_Switch from java import dynamic_proxy + from travertino.size import at_least from .label import TextViewWidget diff --git a/cocoa/src/toga_cocoa/widgets/box.py b/cocoa/src/toga_cocoa/widgets/box.py index de3273ecd8..6b7b3bd410 100644 --- a/cocoa/src/toga_cocoa/widgets/box.py +++ b/cocoa/src/toga_cocoa/widgets/box.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga_cocoa.container import TogaView +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/button.py b/cocoa/src/toga_cocoa/widgets/button.py index eadb8e2b92..52c96aca1c 100644 --- a/cocoa/src/toga_cocoa/widgets/button.py +++ b/cocoa/src/toga_cocoa/widgets/button.py @@ -1,5 +1,4 @@ from rubicon.objc import SEL, objc_method, objc_property -from travertino.size import at_least from toga.colors import TRANSPARENT from toga.fonts import SYSTEM_DEFAULT_FONT_SIZE @@ -10,6 +9,7 @@ NSButton, NSMomentaryPushInButton, ) +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/canvas.py b/cocoa/src/toga_cocoa/widgets/canvas.py index 2572ad2a31..946dbf0249 100644 --- a/cocoa/src/toga_cocoa/widgets/canvas.py +++ b/cocoa/src/toga_cocoa/widgets/canvas.py @@ -1,7 +1,6 @@ from math import ceil from rubicon.objc import CGSize, objc_method, objc_property -from travertino.size import at_least from toga.colors import BLACK, TRANSPARENT, color from toga.constants import Baseline, FillRule @@ -27,6 +26,7 @@ kCGPathFill, kCGPathStroke, ) +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/detailedlist.py b/cocoa/src/toga_cocoa/widgets/detailedlist.py index d1a17875ca..f1c3ddc253 100644 --- a/cocoa/src/toga_cocoa/widgets/detailedlist.py +++ b/cocoa/src/toga_cocoa/widgets/detailedlist.py @@ -1,5 +1,4 @@ from rubicon.objc import SEL, objc_method, objc_property -from travertino.size import at_least from toga_cocoa.libs import ( NSIndexSet, @@ -12,6 +11,7 @@ from toga_cocoa.widgets.internal.cells import TogaDetailedCell from toga_cocoa.widgets.internal.data import TogaData from toga_cocoa.widgets.internal.refresh import RefreshableScrollView +from travertino.size import at_least class TogaList(NSTableView): diff --git a/cocoa/src/toga_cocoa/widgets/divider.py b/cocoa/src/toga_cocoa/widgets/divider.py index 0d983b9198..60b5eaea9e 100644 --- a/cocoa/src/toga_cocoa/widgets/divider.py +++ b/cocoa/src/toga_cocoa/widgets/divider.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga_cocoa.libs import NSBox, NSBoxType +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/label.py b/cocoa/src/toga_cocoa/widgets/label.py index 2c1dfd9c65..1eb3d68401 100644 --- a/cocoa/src/toga_cocoa/widgets/label.py +++ b/cocoa/src/toga_cocoa/widgets/label.py @@ -1,7 +1,6 @@ -from travertino.size import at_least - from toga_cocoa.colors import native_color from toga_cocoa.libs import NSTextAlignment, NSTextField +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/mapview.py b/cocoa/src/toga_cocoa/widgets/mapview.py index 2dd8470240..a873d62226 100644 --- a/cocoa/src/toga_cocoa/widgets/mapview.py +++ b/cocoa/src/toga_cocoa/widgets/mapview.py @@ -1,9 +1,9 @@ import math from rubicon.objc import objc_method, objc_property -from travertino.size import at_least from toga.types import LatLng +from travertino.size import at_least from ..libs import ( CLLocationCoordinate2D, diff --git a/cocoa/src/toga_cocoa/widgets/multilinetextinput.py b/cocoa/src/toga_cocoa/widgets/multilinetextinput.py index 4935060bd2..aaa7850c3d 100644 --- a/cocoa/src/toga_cocoa/widgets/multilinetextinput.py +++ b/cocoa/src/toga_cocoa/widgets/multilinetextinput.py @@ -1,5 +1,4 @@ from rubicon.objc import objc_method, objc_property -from travertino.size import at_least from toga.colors import TRANSPARENT from toga_cocoa.colors import native_color @@ -11,6 +10,7 @@ NSViewHeightSizable, NSViewWidthSizable, ) +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/numberinput.py b/cocoa/src/toga_cocoa/widgets/numberinput.py index 64b956e884..f3ad3110cd 100644 --- a/cocoa/src/toga_cocoa/widgets/numberinput.py +++ b/cocoa/src/toga_cocoa/widgets/numberinput.py @@ -2,7 +2,6 @@ from decimal import Decimal, InvalidOperation from rubicon.objc import SEL, objc_method, objc_property -from travertino.size import at_least from toga.colors import TRANSPARENT from toga.widgets.numberinput import _clean_decimal, _clean_decimal_str @@ -22,6 +21,7 @@ NSTextView, NSView, ) +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/optioncontainer.py b/cocoa/src/toga_cocoa/widgets/optioncontainer.py index ccf1f16c74..55b3929a15 100644 --- a/cocoa/src/toga_cocoa/widgets/optioncontainer.py +++ b/cocoa/src/toga_cocoa/widgets/optioncontainer.py @@ -1,10 +1,10 @@ import warnings from rubicon.objc import SEL, objc_method -from travertino.size import at_least from toga_cocoa.container import Container from toga_cocoa.libs import NSTabView, NSTabViewItem +from travertino.size import at_least from ..libs import objc_property from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/progressbar.py b/cocoa/src/toga_cocoa/widgets/progressbar.py index 691d1e488a..808ab5bdc0 100644 --- a/cocoa/src/toga_cocoa/widgets/progressbar.py +++ b/cocoa/src/toga_cocoa/widgets/progressbar.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga_cocoa.libs import NSProgressIndicator, NSProgressIndicatorBarStyle +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/scrollcontainer.py b/cocoa/src/toga_cocoa/widgets/scrollcontainer.py index 40721bdeee..5e1d91b93a 100644 --- a/cocoa/src/toga_cocoa/widgets/scrollcontainer.py +++ b/cocoa/src/toga_cocoa/widgets/scrollcontainer.py @@ -1,5 +1,4 @@ from rubicon.objc import SEL, objc_method, objc_property -from travertino.size import at_least from toga_cocoa.container import Container from toga_cocoa.libs import ( @@ -12,6 +11,7 @@ NSScrollViewDidEndLiveScrollNotification, NSScrollViewDidLiveScrollNotification, ) +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/selection.py b/cocoa/src/toga_cocoa/widgets/selection.py index 3bc09fdd47..8347504415 100644 --- a/cocoa/src/toga_cocoa/widgets/selection.py +++ b/cocoa/src/toga_cocoa/widgets/selection.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga_cocoa.libs import SEL, NSPopUpButton, objc_method, objc_property +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/slider.py b/cocoa/src/toga_cocoa/widgets/slider.py index 3154c674c7..ce56155b18 100644 --- a/cocoa/src/toga_cocoa/widgets/slider.py +++ b/cocoa/src/toga_cocoa/widgets/slider.py @@ -1,5 +1,3 @@ -from travertino.size import at_least - from toga.widgets.slider import SliderImpl from toga_cocoa.libs import ( SEL, @@ -8,6 +6,7 @@ objc_method, objc_property, ) +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/splitcontainer.py b/cocoa/src/toga_cocoa/widgets/splitcontainer.py index 8ce5296d3f..b120f76ef7 100644 --- a/cocoa/src/toga_cocoa/widgets/splitcontainer.py +++ b/cocoa/src/toga_cocoa/widgets/splitcontainer.py @@ -1,9 +1,9 @@ from rubicon.objc import SEL, objc_method, objc_property -from travertino.size import at_least from toga.constants import Direction from toga_cocoa.container import Container from toga_cocoa.libs import NSSplitView +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/switch.py b/cocoa/src/toga_cocoa/widgets/switch.py index 15f92bc6aa..0f451772ee 100644 --- a/cocoa/src/toga_cocoa/widgets/switch.py +++ b/cocoa/src/toga_cocoa/widgets/switch.py @@ -1,5 +1,3 @@ -from travertino.size import at_least - from toga_cocoa.libs import ( SEL, NSBezelStyle, @@ -10,6 +8,7 @@ objc_method, objc_property, ) +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/table.py b/cocoa/src/toga_cocoa/widgets/table.py index 76e71c4f07..2ddca44005 100644 --- a/cocoa/src/toga_cocoa/widgets/table.py +++ b/cocoa/src/toga_cocoa/widgets/table.py @@ -1,5 +1,4 @@ from rubicon.objc import SEL, NSPoint, at, objc_method, objc_property -from travertino.size import at_least import toga from toga_cocoa.libs import ( @@ -12,6 +11,7 @@ NSTableViewAnimation, NSTableViewColumnAutoresizingStyle, ) +from travertino.size import at_least from .base import Widget from .internal.cells import TogaIconView diff --git a/cocoa/src/toga_cocoa/widgets/textinput.py b/cocoa/src/toga_cocoa/widgets/textinput.py index e3e5995b06..adaaf698f8 100644 --- a/cocoa/src/toga_cocoa/widgets/textinput.py +++ b/cocoa/src/toga_cocoa/widgets/textinput.py @@ -1,5 +1,3 @@ -from travertino.size import at_least - from toga.colors import TRANSPARENT from toga.constants import LEFT, RIGHT from toga_cocoa.colors import native_color @@ -20,6 +18,7 @@ objc_property, send_super, ) +from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/tree.py b/cocoa/src/toga_cocoa/widgets/tree.py index d6729a6995..6dac318538 100644 --- a/cocoa/src/toga_cocoa/widgets/tree.py +++ b/cocoa/src/toga_cocoa/widgets/tree.py @@ -1,5 +1,4 @@ from rubicon.objc import SEL, at, objc_method, objc_property -from travertino.size import at_least import toga from toga_cocoa.libs import ( @@ -14,6 +13,7 @@ from toga_cocoa.widgets.base import Widget from toga_cocoa.widgets.internal.cells import TogaIconView from toga_cocoa.widgets.internal.data import TogaData +from travertino.size import at_least def node_impl(node): diff --git a/cocoa/src/toga_cocoa/widgets/webview.py b/cocoa/src/toga_cocoa/widgets/webview.py index eb58ede63f..4dc8c494b6 100644 --- a/cocoa/src/toga_cocoa/widgets/webview.py +++ b/cocoa/src/toga_cocoa/widgets/webview.py @@ -1,9 +1,9 @@ from http.cookiejar import Cookie, CookieJar from rubicon.objc import objc_id, objc_method, objc_property, py_from_ns -from travertino.size import at_least from toga.widgets.webview import CookiesResult, JavaScriptResult +from travertino.size import at_least from ..libs import NSURL, NSURLRequest, WKWebView from .base import Widget diff --git a/core/src/toga/fonts.py b/core/src/toga/fonts.py index f2a7016d49..8476c3f981 100644 --- a/core/src/toga/fonts.py +++ b/core/src/toga/fonts.py @@ -2,6 +2,9 @@ from pathlib import Path +import toga +from toga.platform import get_platform_factory + # Use the Travertino font definitions as-is from travertino import constants from travertino.constants import ( @@ -20,9 +23,6 @@ ) from travertino.fonts import Font as BaseFont -import toga -from toga.platform import get_platform_factory - SYSTEM_DEFAULT_FONTS = {SYSTEM, MESSAGE, SERIF, SANS_SERIF, CURSIVE, FANTASY, MONOSPACE} SYSTEM_DEFAULT_FONT_SIZE = -1 FONT_WEIGHTS = {NORMAL, BOLD} diff --git a/core/src/toga/widgets/canvas/canvas.py b/core/src/toga/widgets/canvas/canvas.py index e23913299a..d70e289a8f 100644 --- a/core/src/toga/widgets/canvas/canvas.py +++ b/core/src/toga/widgets/canvas/canvas.py @@ -8,8 +8,6 @@ Protocol, ) -from travertino.colors import Color - import toga from toga.colors import BLACK from toga.constants import FillRule @@ -19,6 +17,7 @@ Font, ) from toga.handlers import wrapped_handler +from travertino.colors import Color from ..base import StyleT, Widget from .context import ClosedPathContext, Context, FillContext, StrokeContext diff --git a/core/src/toga/widgets/canvas/context.py b/core/src/toga/widgets/canvas/context.py index f779795149..c6c7806bd0 100644 --- a/core/src/toga/widgets/canvas/context.py +++ b/core/src/toga/widgets/canvas/context.py @@ -5,12 +5,11 @@ from math import pi from typing import TYPE_CHECKING, Any -from travertino.colors import Color - import toga from toga.colors import BLACK, color as parse_color from toga.constants import Baseline, FillRule from toga.fonts import Font +from travertino.colors import Color from .drawingobject import ( Arc, diff --git a/core/src/toga/widgets/canvas/drawingobject.py b/core/src/toga/widgets/canvas/drawingobject.py index 69c5457f8e..dd211db07c 100644 --- a/core/src/toga/widgets/canvas/drawingobject.py +++ b/core/src/toga/widgets/canvas/drawingobject.py @@ -4,8 +4,6 @@ from math import pi from typing import Any -from travertino.colors import Color - from toga.colors import BLACK, color as parse_color from toga.constants import Baseline, FillRule from toga.fonts import ( @@ -13,6 +11,7 @@ SYSTEM_DEFAULT_FONT_SIZE, Font, ) +from travertino.colors import Color class DrawingObject(ABC): diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 7011e059ab..c9cb47bdb4 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -2,11 +2,10 @@ from typing import TYPE_CHECKING, Any, Literal -from travertino.size import at_least - import toga from toga.style.pack import NONE from toga.widgets.base import StyleT, Widget +from travertino.size import at_least if TYPE_CHECKING: from toga.images import ImageContentT, ImageT diff --git a/core/tests/style/pack/layout/test_beeliza.py b/core/tests/style/pack/layout/test_beeliza.py index 93499ebbb1..064b1df0e7 100644 --- a/core/tests/style/pack/layout/test_beeliza.py +++ b/core/tests/style/pack/layout/test_beeliza.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga.style.pack import COLUMN, ROW, Pack +from travertino.size import at_least from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_fixed.py b/core/tests/style/pack/layout/test_fixed.py index 29a19034cd..b4d322fe3f 100644 --- a/core/tests/style/pack/layout/test_fixed.py +++ b/core/tests/style/pack/layout/test_fixed.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga.style.pack import COLUMN, ROW, Pack +from travertino.size import at_least from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_flex.py b/core/tests/style/pack/layout/test_flex.py index 0563ab0f20..f4a92a06c7 100644 --- a/core/tests/style/pack/layout/test_flex.py +++ b/core/tests/style/pack/layout/test_flex.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga.style.pack import COLUMN, ROW, Pack +from travertino.size import at_least from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_gap.py b/core/tests/style/pack/layout/test_gap.py index e79a4387ad..a098afb4a4 100644 --- a/core/tests/style/pack/layout/test_gap.py +++ b/core/tests/style/pack/layout/test_gap.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga.style.pack import Pack +from travertino.size import at_least from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_tutorial0.py b/core/tests/style/pack/layout/test_tutorial0.py index ec9d1588a7..41805b3aba 100644 --- a/core/tests/style/pack/layout/test_tutorial0.py +++ b/core/tests/style/pack/layout/test_tutorial0.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga.style.pack import COLUMN, Pack +from travertino.size import at_least from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_tutorial1.py b/core/tests/style/pack/layout/test_tutorial1.py index 24e7199fd1..a0eb2d3f31 100644 --- a/core/tests/style/pack/layout/test_tutorial1.py +++ b/core/tests/style/pack/layout/test_tutorial1.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga.style.pack import COLUMN, ROW, Pack +from travertino.size import at_least from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_tutorial3.py b/core/tests/style/pack/layout/test_tutorial3.py index 8d1c5d8302..1b7982f6dd 100644 --- a/core/tests/style/pack/layout/test_tutorial3.py +++ b/core/tests/style/pack/layout/test_tutorial3.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga.style.pack import COLUMN, Pack +from travertino.size import at_least from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/widgets/test_imageview.py b/core/tests/widgets/test_imageview.py index 2b423ccac3..6facd398f5 100644 --- a/core/tests/widgets/test_imageview.py +++ b/core/tests/widgets/test_imageview.py @@ -3,7 +3,6 @@ import PIL.Image import pytest -from travertino.size import at_least import toga from toga.style.pack import Pack @@ -13,6 +12,7 @@ assert_action_performed, assert_action_performed_with, ) +from travertino.size import at_least @pytest.fixture diff --git a/examples/canvas/canvas/app.py b/examples/canvas/canvas/app.py index 311774889d..73a5075d2e 100644 --- a/examples/canvas/canvas/app.py +++ b/examples/canvas/canvas/app.py @@ -1,13 +1,12 @@ import math import sys -from travertino.constants import BLACK, BLUE, BOLD, GREEN, ITALIC, NORMAL, RED, YELLOW - import toga from toga.constants import Baseline, FillRule from toga.fonts import CURSIVE, FANTASY, MESSAGE, MONOSPACE, SANS_SERIF, SERIF, SYSTEM from toga.style import Pack from toga.style.pack import COLUMN, ROW +from travertino.constants import BLACK, BLUE, BOLD, GREEN, ITALIC, NORMAL, RED, YELLOW MOVE_STEP = 5 diff --git a/examples/colors/colors/app.py b/examples/colors/colors/app.py index 203057e388..8f4f04dfbf 100644 --- a/examples/colors/colors/app.py +++ b/examples/colors/colors/app.py @@ -1,8 +1,7 @@ -from travertino import colors - import toga from toga.constants import COLUMN, ROW from toga.style import Pack +from travertino import colors class ColorsApp(toga.App): diff --git a/examples/focus/focus/app.py b/examples/focus/focus/app.py index 8b024f7b9a..1b7dd40448 100644 --- a/examples/focus/focus/app.py +++ b/examples/focus/focus/app.py @@ -1,9 +1,8 @@ import random -from travertino.constants import COLUMN - import toga from toga.style import Pack +from travertino.constants import COLUMN WIDGETS_GROUP = toga.Group("Widgets", order=2) FOCUS_ORDER_GROUP = toga.Group("Focus Order", order=3) diff --git a/gtk/src/toga_gtk/widgets/button.py b/gtk/src/toga_gtk/widgets/button.py index 48f9b323ed..4e3015ad5f 100644 --- a/gtk/src/toga_gtk/widgets/button.py +++ b/gtk/src/toga_gtk/widgets/button.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga.colors import TRANSPARENT +from travertino.size import at_least from ..libs import Gtk from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/canvas.py b/gtk/src/toga_gtk/widgets/canvas.py index fe05c28e13..20d51a6443 100644 --- a/gtk/src/toga_gtk/widgets/canvas.py +++ b/gtk/src/toga_gtk/widgets/canvas.py @@ -2,13 +2,12 @@ from io import BytesIO from math import ceil -from travertino.size import at_least - from toga import Font from toga.constants import Baseline, FillRule from toga.fonts import SYSTEM_DEFAULT_FONT_SIZE from toga_gtk.colors import native_color from toga_gtk.libs import Gdk, Gtk, Pango, PangoCairo, cairo +from travertino.size import at_least from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/detailedlist.py b/gtk/src/toga_gtk/widgets/detailedlist.py index d9466266a5..43fd929225 100644 --- a/gtk/src/toga_gtk/widgets/detailedlist.py +++ b/gtk/src/toga_gtk/widgets/detailedlist.py @@ -1,8 +1,7 @@ import html -from travertino.size import at_least - from toga_gtk.libs import Gdk, Gio, Gtk, Pango +from travertino.size import at_least from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/mapview.py b/gtk/src/toga_gtk/widgets/mapview.py index 9b77c18076..cf9dc3dcfa 100644 --- a/gtk/src/toga_gtk/widgets/mapview.py +++ b/gtk/src/toga_gtk/widgets/mapview.py @@ -1,8 +1,7 @@ from concurrent.futures import Future -from travertino.size import at_least - from toga.types import LatLng +from travertino.size import at_least from ..libs import Gtk, WebKit2 from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/numberinput.py b/gtk/src/toga_gtk/widgets/numberinput.py index 59fc4a3217..f8001ac2e3 100644 --- a/gtk/src/toga_gtk/widgets/numberinput.py +++ b/gtk/src/toga_gtk/widgets/numberinput.py @@ -1,9 +1,8 @@ import sys from decimal import InvalidOperation -from travertino.size import at_least - from toga.widgets.numberinput import _clean_decimal +from travertino.size import at_least from ..libs import Gtk, gtk_text_align from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/slider.py b/gtk/src/toga_gtk/widgets/slider.py index 33e78b5037..b6a268d2ce 100644 --- a/gtk/src/toga_gtk/widgets/slider.py +++ b/gtk/src/toga_gtk/widgets/slider.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from toga.widgets.slider import SliderImpl +from travertino.size import at_least from ..libs import Gtk from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/table.py b/gtk/src/toga_gtk/widgets/table.py index b50b2ac95d..679da83c89 100644 --- a/gtk/src/toga_gtk/widgets/table.py +++ b/gtk/src/toga_gtk/widgets/table.py @@ -1,8 +1,7 @@ import warnings -from travertino.size import at_least - import toga +from travertino.size import at_least from ..libs import GdkPixbuf, GObject, Gtk from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/textinput.py b/gtk/src/toga_gtk/widgets/textinput.py index 141ec5de54..7d6ce8e468 100644 --- a/gtk/src/toga_gtk/widgets/textinput.py +++ b/gtk/src/toga_gtk/widgets/textinput.py @@ -1,7 +1,6 @@ -from travertino.size import at_least - from toga.keys import Key from toga_gtk.keys import toga_key +from travertino.size import at_least from ..libs import Gtk, gtk_text_align from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/webview.py b/gtk/src/toga_gtk/widgets/webview.py index 94c066621e..0937b2e2e8 100644 --- a/gtk/src/toga_gtk/widgets/webview.py +++ b/gtk/src/toga_gtk/widgets/webview.py @@ -1,8 +1,7 @@ from http.cookiejar import CookieJar -from travertino.size import at_least - from toga.widgets.webview import CookiesResult, JavaScriptResult +from travertino.size import at_least from ..libs import GLib, WebKit2 from .base import Widget diff --git a/iOS/src/toga_iOS/widgets/box.py b/iOS/src/toga_iOS/widgets/box.py index e0c55847fa..010c4e78f6 100644 --- a/iOS/src/toga_iOS/widgets/box.py +++ b/iOS/src/toga_iOS/widgets/box.py @@ -1,8 +1,8 @@ from rubicon.objc import objc_property -from travertino.size import at_least from toga_iOS.libs import UIView from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaView(UIView): diff --git a/iOS/src/toga_iOS/widgets/button.py b/iOS/src/toga_iOS/widgets/button.py index 2d050a12ea..0244a5602a 100644 --- a/iOS/src/toga_iOS/widgets/button.py +++ b/iOS/src/toga_iOS/widgets/button.py @@ -1,5 +1,4 @@ from rubicon.objc import SEL, CGSize, objc_method, objc_property -from travertino.size import at_least from toga.colors import TRANSPARENT from toga_iOS.colors import native_color @@ -11,6 +10,7 @@ UIControlStateNormal, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaButton(UIButton): diff --git a/iOS/src/toga_iOS/widgets/canvas.py b/iOS/src/toga_iOS/widgets/canvas.py index 9fc213f304..ea665db80d 100644 --- a/iOS/src/toga_iOS/widgets/canvas.py +++ b/iOS/src/toga_iOS/widgets/canvas.py @@ -13,7 +13,6 @@ objc_method, objc_property, ) -from travertino.size import at_least from toga.colors import BLACK, TRANSPARENT, color from toga.constants import Baseline, FillRule @@ -37,6 +36,7 @@ uikit, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaCanvas(UIView): diff --git a/iOS/src/toga_iOS/widgets/detailedlist.py b/iOS/src/toga_iOS/widgets/detailedlist.py index 5dd9b8a51a..1dcbf8e804 100644 --- a/iOS/src/toga_iOS/widgets/detailedlist.py +++ b/iOS/src/toga_iOS/widgets/detailedlist.py @@ -5,7 +5,6 @@ objc_method, objc_property, ) -from travertino.size import at_least from toga_iOS.libs import ( NSIndexPath, @@ -21,6 +20,7 @@ UITableViewScrollPositionNone, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaTableViewController(UITableViewController): diff --git a/iOS/src/toga_iOS/widgets/divider.py b/iOS/src/toga_iOS/widgets/divider.py index cd785d3396..1f53fed4bb 100644 --- a/iOS/src/toga_iOS/widgets/divider.py +++ b/iOS/src/toga_iOS/widgets/divider.py @@ -1,7 +1,6 @@ -from travertino.size import at_least - from toga_iOS.libs import UIColor from toga_iOS.widgets.base import Widget +from travertino.size import at_least from .box import TogaView diff --git a/iOS/src/toga_iOS/widgets/label.py b/iOS/src/toga_iOS/widgets/label.py index fb0de83e1a..bb96e8f513 100644 --- a/iOS/src/toga_iOS/widgets/label.py +++ b/iOS/src/toga_iOS/widgets/label.py @@ -1,7 +1,6 @@ from math import ceil from rubicon.objc import CGRect, NSInteger, NSMakeRect, objc_method, send_super -from travertino.size import at_least from toga_iOS.colors import native_color from toga_iOS.libs import ( @@ -10,6 +9,7 @@ UILabel, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaLabel(UILabel): diff --git a/iOS/src/toga_iOS/widgets/mapview.py b/iOS/src/toga_iOS/widgets/mapview.py index 2dd8470240..a873d62226 100644 --- a/iOS/src/toga_iOS/widgets/mapview.py +++ b/iOS/src/toga_iOS/widgets/mapview.py @@ -1,9 +1,9 @@ import math from rubicon.objc import objc_method, objc_property -from travertino.size import at_least from toga.types import LatLng +from travertino.size import at_least from ..libs import ( CLLocationCoordinate2D, diff --git a/iOS/src/toga_iOS/widgets/multilinetextinput.py b/iOS/src/toga_iOS/widgets/multilinetextinput.py index 6d97b38fed..01d55aaa0f 100644 --- a/iOS/src/toga_iOS/widgets/multilinetextinput.py +++ b/iOS/src/toga_iOS/widgets/multilinetextinput.py @@ -1,7 +1,6 @@ from ctypes import c_void_p from rubicon.objc import CGPoint, NSRange, objc_method, objc_property, send_super -from travertino.size import at_least from toga_iOS.colors import native_color from toga_iOS.libs import ( @@ -17,6 +16,7 @@ UITextView, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaMultilineTextView(UITextView, protocols=[UIKeyInput]): diff --git a/iOS/src/toga_iOS/widgets/numberinput.py b/iOS/src/toga_iOS/widgets/numberinput.py index af1cf8f43f..e80a822401 100644 --- a/iOS/src/toga_iOS/widgets/numberinput.py +++ b/iOS/src/toga_iOS/widgets/numberinput.py @@ -2,7 +2,6 @@ from decimal import InvalidOperation from rubicon.objc import SEL, CGSize, NSRange, objc_method, objc_property, send_message -from travertino.size import at_least from toga.widgets.numberinput import _clean_decimal from toga_iOS.colors import native_color @@ -14,6 +13,7 @@ UITextField, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaNumericTextField(UITextField): diff --git a/iOS/src/toga_iOS/widgets/optioncontainer.py b/iOS/src/toga_iOS/widgets/optioncontainer.py index 783c501225..55d5204857 100644 --- a/iOS/src/toga_iOS/widgets/optioncontainer.py +++ b/iOS/src/toga_iOS/widgets/optioncontainer.py @@ -1,10 +1,10 @@ from rubicon.objc import SEL, objc_method, objc_property -from travertino.size import at_least import toga from toga_iOS.container import ControlledContainer from toga_iOS.libs import UITabBarController, UITabBarItem from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaTabBarController(UITabBarController): diff --git a/iOS/src/toga_iOS/widgets/progressbar.py b/iOS/src/toga_iOS/widgets/progressbar.py index 3cbb80c03b..cd332d3162 100644 --- a/iOS/src/toga_iOS/widgets/progressbar.py +++ b/iOS/src/toga_iOS/widgets/progressbar.py @@ -1,9 +1,8 @@ import asyncio -from travertino.size import at_least - from toga_iOS.libs import CGSize, UIProgressView, UIProgressViewStyle from toga_iOS.widgets.base import Widget +from travertino.size import at_least # Implementation notes # ==================== diff --git a/iOS/src/toga_iOS/widgets/scrollcontainer.py b/iOS/src/toga_iOS/widgets/scrollcontainer.py index a95b9296f0..38a883b03f 100644 --- a/iOS/src/toga_iOS/widgets/scrollcontainer.py +++ b/iOS/src/toga_iOS/widgets/scrollcontainer.py @@ -1,9 +1,9 @@ from rubicon.objc import SEL, NSMakePoint, NSMakeSize, objc_method, objc_property -from travertino.size import at_least from toga_iOS.container import Container from toga_iOS.libs import UIScrollView from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaScrollView(UIScrollView): diff --git a/iOS/src/toga_iOS/widgets/selection.py b/iOS/src/toga_iOS/widgets/selection.py index 3aa05c2441..8cb2011117 100644 --- a/iOS/src/toga_iOS/widgets/selection.py +++ b/iOS/src/toga_iOS/widgets/selection.py @@ -1,5 +1,4 @@ from rubicon.objc import CGSize, objc_method, objc_property -from travertino.size import at_least from toga_iOS.colors import native_color from toga_iOS.libs import ( @@ -10,6 +9,7 @@ UITextField, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaBaseTextField(UITextField): diff --git a/iOS/src/toga_iOS/widgets/slider.py b/iOS/src/toga_iOS/widgets/slider.py index 6db9758378..471d3b2f85 100644 --- a/iOS/src/toga_iOS/widgets/slider.py +++ b/iOS/src/toga_iOS/widgets/slider.py @@ -1,5 +1,4 @@ from rubicon.objc import SEL, CGSize, objc_method, objc_property -from travertino.size import at_least from toga_iOS.libs import ( UIControlEventTouchCancel, @@ -10,6 +9,7 @@ UISlider, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least # Implementation notes # ==================== diff --git a/iOS/src/toga_iOS/widgets/switch.py b/iOS/src/toga_iOS/widgets/switch.py index 89cc6cdb3b..2bbeb1bf00 100644 --- a/iOS/src/toga_iOS/widgets/switch.py +++ b/iOS/src/toga_iOS/widgets/switch.py @@ -1,5 +1,4 @@ from rubicon.objc import SEL, CGSize, objc_method, objc_property -from travertino.size import at_least from toga_iOS.colors import native_color from toga_iOS.libs import ( @@ -11,6 +10,7 @@ UISwitch, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaStackView(UIStackView): diff --git a/iOS/src/toga_iOS/widgets/textinput.py b/iOS/src/toga_iOS/widgets/textinput.py index 2400ca210c..8ac81047b9 100644 --- a/iOS/src/toga_iOS/widgets/textinput.py +++ b/iOS/src/toga_iOS/widgets/textinput.py @@ -1,5 +1,4 @@ from rubicon.objc import SEL, CGSize, objc_method, objc_property -from travertino.size import at_least from toga.constants import LEFT, RIGHT from toga_iOS.colors import native_color @@ -17,6 +16,7 @@ UITextField, ) from toga_iOS.widgets.base import Widget +from travertino.size import at_least class TogaTextField(UITextField): diff --git a/iOS/src/toga_iOS/widgets/webview.py b/iOS/src/toga_iOS/widgets/webview.py index 0f4e7cfa9b..ab22fe56d8 100644 --- a/iOS/src/toga_iOS/widgets/webview.py +++ b/iOS/src/toga_iOS/widgets/webview.py @@ -1,11 +1,11 @@ from http.cookiejar import Cookie, CookieJar from rubicon.objc import objc_id, objc_method, objc_property, py_from_ns -from travertino.size import at_least from toga.widgets.webview import CookiesResult, JavaScriptResult from toga_iOS.libs import NSURL, NSURLRequest, WKWebView from toga_iOS.widgets.base import Widget +from travertino.size import at_least def js_completion_handler(result): diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index 91713cffce..785a2c658e 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -1,7 +1,6 @@ -from travertino.size import at_least - from toga.style.pack import ROW from toga.types import Size +from travertino.size import at_least # We assume a terminal is 800x600 pixels, mapping to 80x25 characters. diff --git a/textual/src/toga_textual/widgets/box.py b/textual/src/toga_textual/widgets/box.py index 01a363ad33..6ac5a5435c 100644 --- a/textual/src/toga_textual/widgets/box.py +++ b/textual/src/toga_textual/widgets/box.py @@ -1,7 +1,6 @@ -from travertino.size import at_least - from textual.containers import Container as TextualContainer from toga.style.pack import ROW +from travertino.size import at_least from .base import Widget diff --git a/textual/src/toga_textual/widgets/button.py b/textual/src/toga_textual/widgets/button.py index ec86c2ad33..7708eec995 100644 --- a/textual/src/toga_textual/widgets/button.py +++ b/textual/src/toga_textual/widgets/button.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from textual.widgets import Button as TextualButton +from travertino.size import at_least from .base import Widget diff --git a/textual/src/toga_textual/widgets/label.py b/textual/src/toga_textual/widgets/label.py index 424d12e071..10c071db00 100644 --- a/textual/src/toga_textual/widgets/label.py +++ b/textual/src/toga_textual/widgets/label.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from textual.widgets import Label as TextualLabel +from travertino.size import at_least from .base import Widget diff --git a/textual/src/toga_textual/widgets/textinput.py b/textual/src/toga_textual/widgets/textinput.py index df2bbcd0b2..f65ceb618b 100644 --- a/textual/src/toga_textual/widgets/textinput.py +++ b/textual/src/toga_textual/widgets/textinput.py @@ -1,6 +1,5 @@ -from travertino.size import at_least - from textual.widgets import Input as TextualInput +from travertino.size import at_least from .base import Widget diff --git a/winforms/src/toga_winforms/colors.py b/winforms/src/toga_winforms/colors.py index 4a7b89d08f..c96ae2cd31 100644 --- a/winforms/src/toga_winforms/colors.py +++ b/winforms/src/toga_winforms/colors.py @@ -1,4 +1,5 @@ from System.Drawing import Color + from travertino.colors import TRANSPARENT CACHE = {TRANSPARENT: Color.Transparent} diff --git a/winforms/src/toga_winforms/widgets/base.py b/winforms/src/toga_winforms/widgets/base.py index cf772f5e5e..1385675d97 100644 --- a/winforms/src/toga_winforms/widgets/base.py +++ b/winforms/src/toga_winforms/widgets/base.py @@ -7,10 +7,10 @@ Size, SystemColors, ) -from travertino.size import at_least from toga.colors import TRANSPARENT from toga_winforms.colors import native_color +from travertino.size import at_least class Scalable(ABC): diff --git a/winforms/src/toga_winforms/widgets/button.py b/winforms/src/toga_winforms/widgets/button.py index 34839a7344..61018f7a40 100644 --- a/winforms/src/toga_winforms/widgets/button.py +++ b/winforms/src/toga_winforms/widgets/button.py @@ -1,6 +1,7 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms + from travertino.size import at_least from ..libs.wrapper import WeakrefCallable diff --git a/winforms/src/toga_winforms/widgets/divider.py b/winforms/src/toga_winforms/widgets/divider.py index 86379a121a..cc176b297f 100644 --- a/winforms/src/toga_winforms/widgets/divider.py +++ b/winforms/src/toga_winforms/widgets/divider.py @@ -1,6 +1,7 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms + from travertino.size import at_least from .base import Widget diff --git a/winforms/src/toga_winforms/widgets/label.py b/winforms/src/toga_winforms/widgets/label.py index bab8c7e001..907bdfdbca 100644 --- a/winforms/src/toga_winforms/widgets/label.py +++ b/winforms/src/toga_winforms/widgets/label.py @@ -1,9 +1,9 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms -from travertino.size import at_least from toga_winforms.libs.fonts import TextAlignment +from travertino.size import at_least from .base import Widget diff --git a/winforms/src/toga_winforms/widgets/multilinetextinput.py b/winforms/src/toga_winforms/widgets/multilinetextinput.py index db12269b6c..edeb6637b6 100644 --- a/winforms/src/toga_winforms/widgets/multilinetextinput.py +++ b/winforms/src/toga_winforms/widgets/multilinetextinput.py @@ -1,9 +1,9 @@ import System.Windows.Forms as WinForms from System.Drawing import SystemColors -from travertino.size import at_least from toga_winforms.colors import native_color from toga_winforms.libs.fonts import HorizontalTextAlignment +from travertino.size import at_least from ..libs.wrapper import WeakrefCallable from .textinput import TextInput diff --git a/winforms/src/toga_winforms/widgets/scrollcontainer.py b/winforms/src/toga_winforms/widgets/scrollcontainer.py index 415a149484..ff022b2c4c 100644 --- a/winforms/src/toga_winforms/widgets/scrollcontainer.py +++ b/winforms/src/toga_winforms/widgets/scrollcontainer.py @@ -2,9 +2,9 @@ from System.Drawing import Point from System.Windows.Forms import Panel, SystemInformation -from travertino.node import Node from toga_winforms.container import Container +from travertino.node import Node from ..libs.wrapper import WeakrefCallable from .base import Widget diff --git a/winforms/src/toga_winforms/widgets/switch.py b/winforms/src/toga_winforms/widgets/switch.py index 16792bb01e..4a56861827 100644 --- a/winforms/src/toga_winforms/widgets/switch.py +++ b/winforms/src/toga_winforms/widgets/switch.py @@ -1,6 +1,7 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms + from travertino.size import at_least from ..libs.wrapper import WeakrefCallable From 3a0fda422094a09013d29c4795d5c701049c60ac Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 22:13:17 -0500 Subject: [PATCH 07/53] Moved Travertino tests back; altered tox and CI --- .github/workflows/ci.yml | 58 ++++++++++++------- tox.ini | 36 +++++++----- travertino/pyproject.toml | 3 + .../tests}/__init__.py | 0 .../tests}/colors/__init__.py | 0 .../tests}/colors/test_constructor.py | 0 .../tests}/colors/test_parsing.py | 0 .../tests}/fonts/__init__.py | 0 .../tests}/fonts/test_constructor.py | 0 .../tests}/fonts/test_parsing.py | 2 +- .../tests}/test_choices.py | 2 +- .../tests}/test_declaration.py | 2 +- .../tests}/test_layout.py | 0 .../tests}/test_node.py | 2 +- .../tests}/test_size.py | 0 .../travertino => travertino/tests}/utils.py | 0 16 files changed, 67 insertions(+), 38 deletions(-) rename {core/tests/travertino => travertino/tests}/__init__.py (100%) rename {core/tests/travertino => travertino/tests}/colors/__init__.py (100%) rename {core/tests/travertino => travertino/tests}/colors/test_constructor.py (100%) rename {core/tests/travertino => travertino/tests}/colors/test_parsing.py (100%) rename {core/tests/travertino => travertino/tests}/fonts/__init__.py (100%) rename {core/tests/travertino => travertino/tests}/fonts/test_constructor.py (100%) rename {core/tests/travertino => travertino/tests}/fonts/test_parsing.py (98%) rename {core/tests/travertino => travertino/tests}/test_choices.py (99%) rename {core/tests/travertino => travertino/tests}/test_declaration.py (99%) rename {core/tests/travertino => travertino/tests}/test_layout.py (100%) rename {core/tests/travertino => travertino/tests}/test_node.py (99%) rename {core/tests/travertino => travertino/tests}/test_size.py (100%) rename {core/tests/travertino => travertino/tests}/utils.py (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cdbda0508..f787531716 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,7 @@ jobs: - "gtk" - "iOS" - "toga" + - "travertino" - "textual" - "web" - "winforms" @@ -67,8 +68,8 @@ jobs: build-subdirectory: ${{ matrix.subdir }} attest: ${{ inputs.attest-package }} - core: - name: Test core + test-core-and-travertino: + name: Test ${{ matrix.package }} runs-on: ${{ matrix.platform }} needs: [ pre-commit, towncrier, package ] continue-on-error: ${{ matrix.experimental }} @@ -77,8 +78,13 @@ jobs: matrix: platform: [ "macos-latest", "ubuntu-latest", "windows-latest" ] python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + package: ["core", "travertino"] include: - experimental: false + - package: "core" + env_suffix: "" + - package: "travertino" + env_suffix: "-trav" steps: - name: Checkout @@ -110,16 +116,16 @@ jobs: run: | # The $(ls ...) shell expansion is done in the Github environment; # the value of TOGA_INSTALL_COMMAND will be a literal string without any shell expansions to perform - TOGA_INSTALL_COMMAND="python -m pip install ../$(ls dist/toga_core-*.whl)[dev] ../$(ls dist/toga_dummy-*.whl)" \ - tox -e py-cov - tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}") - mv core/.coverage core/.coverage.${{ matrix.platform }}.${{ matrix.python-version }} + TOGA_INSTALL_COMMAND="python -m pip install ../$(ls dist/toga_core-*.whl)[dev] ../$(ls dist/toga_dummy-*.whl) ../$(ls dist/travertino-*.whl)" + tox -e py-cov${{ matrix.env_suffix }} + tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}"){{ matrix.env_suffix }} + mv ${{ matrix.package }}/.coverage ${{ matrix.package }}/.coverage.${{ matrix.platform }}.${{ matrix.python-version }} - name: Store Coverage Data uses: actions/upload-artifact@v4.5.0 with: - name: core-coverage-data-${{ matrix.platform }}-${{ matrix.python-version }} - path: "core/.coverage.*" + name: ${{ matrix.package }}-coverage-data-${{ matrix.platform }}-${{ matrix.python-version }} + path: "$${{ matrix.coverage }}/.coverage.*" if-no-files-found: error include-hidden-files: true @@ -159,15 +165,21 @@ jobs: with: python-version: "3.13" - - name: Get Packages + - name: Get Core Package uses: actions/download-artifact@v4.1.8 with: name: Packages-toga-core path: dist + - name: Get Travertino Package + uses: actions/download-artifact@v4.1.8 + with: + name: Packages-toga-travertino + path: dist + - name: Test run: | - pip install dist/toga_core-*.whl + pip install dist/toga_core-*.whl dist/travertino-*.whl site_packages=$(python -c ' import sys print([path for path in sys.path if "site-packages" in path][0]) @@ -176,17 +188,21 @@ jobs: cd core export MICROPYPATH="$site_packages:.frozen" - echo "Stable Travertino" - ${{ steps.micropython.outputs.executable }} micropython_check.py - - echo "Development Travertino" - pip install git+https://github.com/beeware/travertino ${{ steps.micropython.outputs.executable }} micropython_check.py - core-coverage: - name: Coverage + core-and-travertino-coverage: + name: "Coverage: ${{ matrix.package }}" needs: core runs-on: ubuntu-latest + matrix: + package: ["core", "travertino"] + include: + - package: "core" + env_suffix: "" + fail: "-fail" + - package: "travertino" + env_suffix: "-trav" + fail: "" steps: - name: Checkout uses: actions/checkout@v4.2.2 @@ -210,19 +226,19 @@ jobs: - name: Retrieve Coverage Data uses: actions/download-artifact@v4.1.8 with: - pattern: core-coverage-data-* - path: core + pattern: ${{ matrix.package }}-coverage-data-* + path: ${{ matrix.package }} merge-multiple: true - name: Generate Coverage Report - run: tox -e coverage-html-fail-platform + run: tox -e coverage-html${{ matrix.fail }}platform${{ matrix.env_suffix }} - name: Upload HTML Coverage Report uses: actions/upload-artifact@v4.5.0 if: failure() with: name: html-coverage-report - path: core/htmlcov + path: ${{ matrix.package }}/htmlcov testbed: name: Testbed diff --git a/tox.ini b/tox.ini index 5893746451..69d3fe6818 100644 --- a/tox.ini +++ b/tox.ini @@ -9,13 +9,13 @@ extend-ignore = [tox] envlist = py{39,310,311,312,313}-cov,coverage labels = - test = py-cov,coverage - test39 = py39-cov,coverage39 - test310 = py310-cov,coverage310 - test311 = py311-cov,coverage311 - test312 = py312-cov,coverage312 - test313 = py313-cov,coverage313 - ci = towncrier-check,docs-lint,pre-commit,py{39,310,311,312,313}-cov,coverage-fail-platform + test = py-cov{,-trav},coverage{,-trav} + test39 = py39-cov{,-trav},coverage39{,-trav} + test310 = py310-cov{,-trav},coverage310{,-trav} + test311 = py311-cov{,-trav},coverage311{,-trav} + test312 = py312-cov{,-trav},coverage312{,-trav} + test313 = py313-cov{,-trav},coverage313{,-trav} + ci = towncrier-check,docs-lint,pre-commit,py313-cov{,-trav},coverage{,-trav}-fail-platform skip_missing_interpreters = True [testenv:pre-commit] @@ -25,9 +25,11 @@ deps = commands = pre-commit run --all-files --show-diff-on-failure --color=always # The leading comma generates the "py" environment -[testenv:py{,39,310,311,312,313}{,-cov}] +[testenv:py{,39,310,311,312,313}{,-cov}{,-trav}] depends = pre-commit -changedir = core +changedir = + !trav: core + trav: travertino skip_install = True setenv = TOGA_BACKEND = toga_dummy @@ -35,14 +37,17 @@ allowlist_externals = bash commands = # TOGA_INSTALL_COMMAND is set to a bash command by the CI workflow - # Install as editable so parallel runs don't clobber the build directory for each other {env:TOGA_INSTALL_COMMAND:python -m pip install {tox_root}{/}core[dev] {tox_root}{/}dummy {tox_root}{/}travertino} !cov: python -m pytest {posargs:-vv --color yes} cov : python -m coverage run -m pytest {posargs:-vv --color yes} -[testenv:coverage{,39,310,311,312,313}{,-html}{,-keep}{,-fail}{,-platform}] -depends = pre-commit,py{,39,310,311,312,313}{,-cov} -changedir = core +[testenv:coverage{,39,310,311,312,313}{,-trav}{,-html}{,-keep}{,-fail}{,-platform}] +depends = + !trav: pre-commit,py{,39,310,311,312,313}{,-cov} + trav: pre-commit,py{,39,310,311,312,313}{,-trav}{,-cov} +changedir = + !trav: core + trav: travertino skip_install = True # by default, coverage should run on oldest supported Python for testing platform coverage. # however, coverage for a particular Python version should match the version used for pytest. @@ -53,10 +58,14 @@ base_python = coverage312: py312 coverage313: py313 deps = + # Installing core[dev] even when just testing Travertino ensures the same versions + # of testing tools. {tox_root}{/}core[dev] setenv = keep: COMBINE_KEEP = --keep fail: REPORT_FAIL_COND = --fail-under=100 + # Even when run with "fail", cut Travertino some slack. + trav: REPORT_FAIL_COND = CORE_RCFILE = --rcfile {tox_root}{/}core{/}pyproject.toml PROJECT_RCFILE = --rcfile {tox_root}{/}pyproject.toml # disable conditional coverage exclusions for Python version @@ -90,6 +99,7 @@ suicide_timeout = 1 deps = # editable install so docstrings can be updated for 'all' and 'live' -e {tox_root}{/}core[docs] + {tox_root}{/}travertino passenv = # On macOS M1, you need to manually set the location of the PyEnchant # library: diff --git a/travertino/pyproject.toml b/travertino/pyproject.toml index cb87c00b89..29d85c1a52 100644 --- a/travertino/pyproject.toml +++ b/travertino/pyproject.toml @@ -69,3 +69,6 @@ source = [ "src/travertino", "**/travertino", ] + +[tool.pytest.ini_options] +asyncio_default_fixture_loop_scope = "function" diff --git a/core/tests/travertino/__init__.py b/travertino/tests/__init__.py similarity index 100% rename from core/tests/travertino/__init__.py rename to travertino/tests/__init__.py diff --git a/core/tests/travertino/colors/__init__.py b/travertino/tests/colors/__init__.py similarity index 100% rename from core/tests/travertino/colors/__init__.py rename to travertino/tests/colors/__init__.py diff --git a/core/tests/travertino/colors/test_constructor.py b/travertino/tests/colors/test_constructor.py similarity index 100% rename from core/tests/travertino/colors/test_constructor.py rename to travertino/tests/colors/test_constructor.py diff --git a/core/tests/travertino/colors/test_parsing.py b/travertino/tests/colors/test_parsing.py similarity index 100% rename from core/tests/travertino/colors/test_parsing.py rename to travertino/tests/colors/test_parsing.py diff --git a/core/tests/travertino/fonts/__init__.py b/travertino/tests/fonts/__init__.py similarity index 100% rename from core/tests/travertino/fonts/__init__.py rename to travertino/tests/fonts/__init__.py diff --git a/core/tests/travertino/fonts/test_constructor.py b/travertino/tests/fonts/test_constructor.py similarity index 100% rename from core/tests/travertino/fonts/test_constructor.py rename to travertino/tests/fonts/test_constructor.py diff --git a/core/tests/travertino/fonts/test_parsing.py b/travertino/tests/fonts/test_parsing.py similarity index 98% rename from core/tests/travertino/fonts/test_parsing.py rename to travertino/tests/fonts/test_parsing.py index 516d08b8ec..1029f8e977 100644 --- a/core/tests/travertino/fonts/test_parsing.py +++ b/travertino/tests/fonts/test_parsing.py @@ -1,6 +1,6 @@ import pytest -from tests.travertino.fonts.test_constructor import assert_font +from tests.fonts.test_constructor import assert_font from travertino.constants import ( BOLD, ITALIC, diff --git a/core/tests/travertino/test_choices.py b/travertino/tests/test_choices.py similarity index 99% rename from core/tests/travertino/test_choices.py rename to travertino/tests/test_choices.py index 6928b6284e..b9ef2c865f 100644 --- a/core/tests/travertino/test_choices.py +++ b/travertino/tests/test_choices.py @@ -3,8 +3,8 @@ from warnings import catch_warnings, filterwarnings import pytest -from tests.travertino.utils import mock_attr, prep_style_class +from tests.utils import mock_attr, prep_style_class from travertino.colors import NAMED_COLOR, rgb from travertino.constants import GOLDENROD, NONE, REBECCAPURPLE, TOP from travertino.declaration import BaseStyle, Choices, validated_property diff --git a/core/tests/travertino/test_declaration.py b/travertino/tests/test_declaration.py similarity index 99% rename from core/tests/travertino/test_declaration.py rename to travertino/tests/test_declaration.py index 57fa7cef12..c2f1414630 100644 --- a/core/tests/travertino/test_declaration.py +++ b/travertino/tests/test_declaration.py @@ -4,8 +4,8 @@ from warnings import catch_warnings, filterwarnings import pytest -from tests.travertino.utils import mock_attr, prep_style_class +from tests.utils import mock_attr, prep_style_class from travertino.declaration import ( BaseStyle, Choices, diff --git a/core/tests/travertino/test_layout.py b/travertino/tests/test_layout.py similarity index 100% rename from core/tests/travertino/test_layout.py rename to travertino/tests/test_layout.py diff --git a/core/tests/travertino/test_node.py b/travertino/tests/test_node.py similarity index 99% rename from core/tests/travertino/test_node.py rename to travertino/tests/test_node.py index f04bc0dccc..2f0e76e42d 100644 --- a/core/tests/travertino/test_node.py +++ b/travertino/tests/test_node.py @@ -2,8 +2,8 @@ from warnings import catch_warnings, filterwarnings import pytest -from tests.travertino.utils import mock_attr, prep_style_class +from tests.utils import mock_attr, prep_style_class from travertino.declaration import BaseStyle, Choices, validated_property from travertino.layout import BaseBox, Viewport from travertino.node import Node diff --git a/core/tests/travertino/test_size.py b/travertino/tests/test_size.py similarity index 100% rename from core/tests/travertino/test_size.py rename to travertino/tests/test_size.py diff --git a/core/tests/travertino/utils.py b/travertino/tests/utils.py similarity index 100% rename from core/tests/travertino/utils.py rename to travertino/tests/utils.py From 46959b62d49ce34163127d729c68868c88e06c1b Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 22:19:53 -0500 Subject: [PATCH 08/53] Add Travertino to RTD install --- .readthedocs.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 4be7c3148e..cb574a449c 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -35,3 +35,5 @@ python: extra_requirements: - dev - docs + - method: pip + path: travertino From e604846f824758bf7dcc77f74535646fdb564f7b Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 22:36:25 -0500 Subject: [PATCH 09/53] Fixed test imports, and fail handling in CI coverage --- .github/workflows/ci.yml | 5 ++--- travertino/tests/fonts/test_parsing.py | 3 ++- travertino/tests/test_choices.py | 3 ++- travertino/tests/test_declaration.py | 3 ++- travertino/tests/test_node.py | 3 ++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36ece8b2c9..bb15292ba9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -199,10 +199,8 @@ jobs: include: - package: "core" env_suffix: "" - fail: "-fail" - package: "travertino" env_suffix: "-trav" - fail: "" steps: - name: Checkout uses: actions/checkout@v4.2.2 @@ -231,7 +229,8 @@ jobs: merge-multiple: true - name: Generate Coverage Report - run: tox -e coverage-html${{ matrix.fail }}platform${{ matrix.env_suffix }} + # Even with "fail" on, Travertino will accept <100%. + run: tox -e coverage-html-fail-platform${{ matrix.env_suffix }} - name: Upload HTML Coverage Report uses: actions/upload-artifact@v4.6.0 diff --git a/travertino/tests/fonts/test_parsing.py b/travertino/tests/fonts/test_parsing.py index 1029f8e977..95094b45fa 100644 --- a/travertino/tests/fonts/test_parsing.py +++ b/travertino/tests/fonts/test_parsing.py @@ -1,6 +1,5 @@ import pytest -from tests.fonts.test_constructor import assert_font from travertino.constants import ( BOLD, ITALIC, @@ -10,6 +9,8 @@ ) from travertino.fonts import Font, font +from .test_constructor import assert_font + def test_font_instance(): f = Font("Comic Sans", 12) diff --git a/travertino/tests/test_choices.py b/travertino/tests/test_choices.py index b9ef2c865f..ff642bf44f 100644 --- a/travertino/tests/test_choices.py +++ b/travertino/tests/test_choices.py @@ -4,11 +4,12 @@ import pytest -from tests.utils import mock_attr, prep_style_class from travertino.colors import NAMED_COLOR, rgb from travertino.constants import GOLDENROD, NONE, REBECCAPURPLE, TOP from travertino.declaration import BaseStyle, Choices, validated_property +from .utils import mock_attr, prep_style_class + @prep_style_class class Style(BaseStyle): diff --git a/travertino/tests/test_declaration.py b/travertino/tests/test_declaration.py index c2f1414630..cf8a0b0450 100644 --- a/travertino/tests/test_declaration.py +++ b/travertino/tests/test_declaration.py @@ -5,7 +5,6 @@ import pytest -from tests.utils import mock_attr, prep_style_class from travertino.declaration import ( BaseStyle, Choices, @@ -15,6 +14,8 @@ validated_property, ) +from .utils import mock_attr, prep_style_class + VALUE1 = "value1" VALUE2 = "value2" VALUE3 = "value3" diff --git a/travertino/tests/test_node.py b/travertino/tests/test_node.py index 2f0e76e42d..5eb92c8f30 100644 --- a/travertino/tests/test_node.py +++ b/travertino/tests/test_node.py @@ -3,12 +3,13 @@ import pytest -from tests.utils import mock_attr, prep_style_class from travertino.declaration import BaseStyle, Choices, validated_property from travertino.layout import BaseBox, Viewport from travertino.node import Node from travertino.size import BaseIntrinsicSize +from .utils import mock_attr, prep_style_class + @prep_style_class @mock_attr("reapply") From 48667e8138f3ec38ad885b4c32996ca38e7a947d Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 22:38:33 -0500 Subject: [PATCH 10/53] Fixed matrix location in CI --- .github/workflows/ci.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb15292ba9..9627e18b25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,13 +194,14 @@ jobs: name: "Coverage: ${{ matrix.package }}" needs: core runs-on: ubuntu-latest - matrix: - package: ["core", "travertino"] - include: - - package: "core" - env_suffix: "" - - package: "travertino" - env_suffix: "-trav" + strategy: + matrix: + package: ["core", "travertino"] + include: + - package: "core" + env_suffix: "" + - package: "travertino" + env_suffix: "-trav" steps: - name: Checkout uses: actions/checkout@v4.2.2 From cac6e91805ccd337c98ce1a9b0af0fd35968bf3f Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 22:40:24 -0500 Subject: [PATCH 11/53] Fixed job needs names --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9627e18b25..071c3545ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,7 @@ jobs: build-subdirectory: ${{ matrix.subdir }} attest: ${{ inputs.attest-package }} - test-core-and-travertino: + core-and-travertino: name: Test ${{ matrix.package }} runs-on: ${{ matrix.platform }} needs: [ pre-commit, towncrier, package ] @@ -192,7 +192,7 @@ jobs: core-and-travertino-coverage: name: "Coverage: ${{ matrix.package }}" - needs: core + needs: core-and-travertino runs-on: ubuntu-latest strategy: matrix: @@ -242,7 +242,7 @@ jobs: testbed: name: Testbed - needs: core + needs: core-and-travertino runs-on: ${{ matrix.runs-on }} strategy: fail-fast: false From 36db68d548fd537165f4c05f5712dd801aaba077 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 23:07:19 -0500 Subject: [PATCH 12/53] Fixed style mixin with validated_property --- .github/workflows/ci.yml | 16 ++++++++-------- core/src/toga/style/mixin.py | 7 ++++++- travertino/src/travertino/declaration.py | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 071c3545ac..a542990a96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: attest: ${{ inputs.attest-package }} core-and-travertino: - name: Test ${{ matrix.package }} + name: Test ${{ matrix.package }} (${{ matrix.platform }}, ${{ matrix.python-version }}) runs-on: ${{ matrix.platform }} needs: [ pre-commit, towncrier, package ] continue-on-error: ${{ matrix.experimental }} @@ -82,9 +82,9 @@ jobs: include: - experimental: false - package: "core" - env_suffix: "" + env-suffix: "" - package: "travertino" - env_suffix: "-trav" + env-suffix: "-trav" steps: - name: Checkout @@ -117,8 +117,8 @@ jobs: # The $(ls ...) shell expansion is done in the Github environment; # the value of TOGA_INSTALL_COMMAND will be a literal string without any shell expansions to perform TOGA_INSTALL_COMMAND="python -m pip install ../$(ls dist/toga_core-*.whl)[dev] ../$(ls dist/toga_dummy-*.whl) ../$(ls dist/travertino-*.whl)" - tox -e py-cov${{ matrix.env_suffix }} - tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}"){{ matrix.env_suffix }} + tox -e py-cov${{ matrix.env-suffix }} + tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}"){{ matrix.env-suffix }} mv ${{ matrix.package }}/.coverage ${{ matrix.package }}/.coverage.${{ matrix.platform }}.${{ matrix.python-version }} - name: Store Coverage Data @@ -199,9 +199,9 @@ jobs: package: ["core", "travertino"] include: - package: "core" - env_suffix: "" + env-suffix: "" - package: "travertino" - env_suffix: "-trav" + env-suffix: "-trav" steps: - name: Checkout uses: actions/checkout@v4.2.2 @@ -231,7 +231,7 @@ jobs: - name: Generate Coverage Report # Even with "fail" on, Travertino will accept <100%. - run: tox -e coverage-html-fail-platform${{ matrix.env_suffix }} + run: tox -e coverage-html-fail-platform${{ matrix.env-suffix }} - name: Upload HTML Coverage Report uses: actions/upload-artifact@v4.6.0 diff --git a/core/src/toga/style/mixin.py b/core/src/toga/style/mixin.py index 92aff56235..7618d59b66 100644 --- a/core/src/toga/style/mixin.py +++ b/core/src/toga/style/mixin.py @@ -1,3 +1,6 @@ +from travertino.declaration import validated_property + + class StyleProperty: def __set_name__(self, mixin_cls, name): self.name = name @@ -22,7 +25,9 @@ def style_mixin(style_cls): } for name in dir(style_cls): - if not name.startswith("_") and isinstance(getattr(style_cls, name), property): + if not name.startswith("_") and isinstance( + getattr(style_cls, name), validated_property + ): mixin_dict[name] = StyleProperty() return type(style_cls.__name__ + "Mixin", (), mixin_dict) diff --git a/travertino/src/travertino/declaration.py b/travertino/src/travertino/declaration.py index c44a35a794..9e5b3f460a 100644 --- a/travertino/src/travertino/declaration.py +++ b/travertino/src/travertino/declaration.py @@ -341,7 +341,7 @@ def update(self, **styles): for name, value in styles.items(): name = name.replace("-", "_") if name not in self._ALL_PROPERTIES: - raise NameError(f"Unknown style {name}") + raise NameError(f"Unknown style '{name}'") self[name] = value From 0cb994eed975afdf73d4b2142fbeb81738165ad7 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 23:10:48 -0500 Subject: [PATCH 13/53] Missing $ --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a542990a96..1dd7eb4234 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,7 @@ jobs: # the value of TOGA_INSTALL_COMMAND will be a literal string without any shell expansions to perform TOGA_INSTALL_COMMAND="python -m pip install ../$(ls dist/toga_core-*.whl)[dev] ../$(ls dist/toga_dummy-*.whl) ../$(ls dist/travertino-*.whl)" tox -e py-cov${{ matrix.env-suffix }} - tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}"){{ matrix.env-suffix }} + tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}")${{ matrix.env-suffix }} mv ${{ matrix.package }}/.coverage ${{ matrix.package }}/.coverage.${{ matrix.platform }}.${{ matrix.python-version }} - name: Store Coverage Data From c2f461ed784ce3daaff48737eaa3281ed1884c9b Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 23:16:27 -0500 Subject: [PATCH 14/53] Fix coverage file name --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1dd7eb4234..479fc342f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,14 +118,14 @@ jobs: # the value of TOGA_INSTALL_COMMAND will be a literal string without any shell expansions to perform TOGA_INSTALL_COMMAND="python -m pip install ../$(ls dist/toga_core-*.whl)[dev] ../$(ls dist/toga_dummy-*.whl) ../$(ls dist/travertino-*.whl)" tox -e py-cov${{ matrix.env-suffix }} - tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}")${{ matrix.env-suffix }} + tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}") mv ${{ matrix.package }}/.coverage ${{ matrix.package }}/.coverage.${{ matrix.platform }}.${{ matrix.python-version }} - name: Store Coverage Data uses: actions/upload-artifact@v4.6.0 with: name: ${{ matrix.package }}-coverage-data-${{ matrix.platform }}-${{ matrix.python-version }} - path: "$${{ matrix.coverage }}/.coverage.*" + path: "$${{ matrix.package }}/.coverage.*" if-no-files-found: error include-hidden-files: true From 71f4c95e72bb3fb5794095247d7814de4f3a08bb Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 23:20:50 -0500 Subject: [PATCH 15/53] Fix it *more* --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 479fc342f5..c4eba03adf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,7 +125,7 @@ jobs: uses: actions/upload-artifact@v4.6.0 with: name: ${{ matrix.package }}-coverage-data-${{ matrix.platform }}-${{ matrix.python-version }} - path: "$${{ matrix.package }}/.coverage.*" + path: "${{ matrix.package }}/.coverage.*" if-no-files-found: error include-hidden-files: true From 8e89176e74e067793e593ef8b859e97e33f52da7 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 17 Jan 2025 23:40:57 -0500 Subject: [PATCH 16/53] Add -trav to tox call --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4eba03adf..47ee7e8e49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,7 @@ jobs: # the value of TOGA_INSTALL_COMMAND will be a literal string without any shell expansions to perform TOGA_INSTALL_COMMAND="python -m pip install ../$(ls dist/toga_core-*.whl)[dev] ../$(ls dist/toga_dummy-*.whl) ../$(ls dist/travertino-*.whl)" tox -e py-cov${{ matrix.env-suffix }} - tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}") + tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}")${{ matrix.env-suffix }} mv ${{ matrix.package }}/.coverage ${{ matrix.package }}/.coverage.${{ matrix.platform }}.${{ matrix.python-version }} - name: Store Coverage Data From b839a5e7417fc21b47859586218b7fadf6377b50 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sat, 18 Jan 2025 00:04:33 -0500 Subject: [PATCH 17/53] Add Travertino to Testbed pyproject; set project root for tox in CI --- .github/workflows/ci.yml | 4 ++-- testbed/pyproject.toml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47ee7e8e49..3e9d036685 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: with: requirements: tox extra: dev - project-root: core + project-root: $${{ matrix.package }} - name: Get Packages uses: actions/download-artifact@v4.1.8 @@ -220,7 +220,7 @@ jobs: with: requirements: tox extra: dev - project-root: core + project-root: ${{ matrix.package }} - name: Retrieve Coverage Data uses: actions/download-artifact@v4.1.8 diff --git a/testbed/pyproject.toml b/testbed/pyproject.toml index e4099d6ed1..355fd7cd97 100644 --- a/testbed/pyproject.toml +++ b/testbed/pyproject.toml @@ -35,6 +35,7 @@ test_sources = [ ] requires = [ "../core", + "../travertino", ] permission.camera = "The testbed needs to exercise Camera APIs" From 6855af7b34243710b8b9573ec038fb10f373167c Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sat, 18 Jan 2025 00:06:22 -0500 Subject: [PATCH 18/53] Revert tox root --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e9d036685..47ee7e8e49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: with: requirements: tox extra: dev - project-root: $${{ matrix.package }} + project-root: core - name: Get Packages uses: actions/download-artifact@v4.1.8 @@ -220,7 +220,7 @@ jobs: with: requirements: tox extra: dev - project-root: ${{ matrix.package }} + project-root: core - name: Retrieve Coverage Data uses: actions/download-artifact@v4.1.8 From f92e866a292a90d7815ecce97c371babd8cc7d19 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sat, 18 Jan 2025 00:20:23 -0500 Subject: [PATCH 19/53] Add Travertino to textual testbed --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47ee7e8e49..cd14417f70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -337,7 +337,7 @@ jobs: platform: "linux" runs-on: "ubuntu-latest" setup-python: false # Use the system Python packages - briefcase-run-args: --config 'requires=["../core","../textual"]' --config 'console_app=true' + briefcase-run-args: --config 'requires=["../core","../textual", "../travertino"]' --config 'console_app=true' app-user-data-path: "$HOME/.local/share/testbed" # install the meta-package build-essential since Briefcase explicitly checks for it pre-command: sudo apt update -y && sudo apt install -y build-essential @@ -345,13 +345,13 @@ jobs: - backend: "textual-macOS" platform: "macOS" runs-on: "macos-latest" - briefcase-run-args: --config 'requires=["../core","../textual"]' --config 'console_app=true' + briefcase-run-args: --config 'requires=["../core","../textual", "../travertino"]' --config 'console_app=true' app-user-data-path: "$HOME/Library/Application Support/org.beeware.toga.testbed" - backend: "textual-windows" platform: "windows" runs-on: "windows-latest" - briefcase-run-args: --config 'requires=["../core","../textual"]' --config 'console_app=true' + briefcase-run-args: --config 'requires=["../core","../textual", "../travertino"]' --config 'console_app=true' app-user-data-path: '$HOME\AppData\Local\Tiberius Yak\Toga Testbed\Data' - backend: "windows" From c0027d208cc0ab029daa07d72b31bb58b606e828 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sat, 18 Jan 2025 01:27:26 -0500 Subject: [PATCH 20/53] Separate isort settings so Toga reverts to treating Travertino as third-party --- .pre-commit-config.yaml | 3 +++ android/src/toga_android/colors.py | 1 - android/src/toga_android/widgets/base.py | 2 +- android/src/toga_android/widgets/button.py | 1 - android/src/toga_android/widgets/divider.py | 1 - .../toga_android/widgets/internal/pickers.py | 1 - android/src/toga_android/widgets/label.py | 2 +- .../toga_android/widgets/multilinetextinput.py | 1 - .../src/toga_android/widgets/optioncontainer.py | 2 +- android/src/toga_android/widgets/switch.py | 1 - cocoa/src/toga_cocoa/widgets/box.py | 3 ++- cocoa/src/toga_cocoa/widgets/button.py | 2 +- cocoa/src/toga_cocoa/widgets/canvas.py | 2 +- cocoa/src/toga_cocoa/widgets/detailedlist.py | 2 +- cocoa/src/toga_cocoa/widgets/divider.py | 3 ++- cocoa/src/toga_cocoa/widgets/label.py | 3 ++- cocoa/src/toga_cocoa/widgets/mapview.py | 2 +- .../toga_cocoa/widgets/multilinetextinput.py | 2 +- cocoa/src/toga_cocoa/widgets/numberinput.py | 2 +- cocoa/src/toga_cocoa/widgets/optioncontainer.py | 2 +- cocoa/src/toga_cocoa/widgets/progressbar.py | 3 ++- cocoa/src/toga_cocoa/widgets/scrollcontainer.py | 2 +- cocoa/src/toga_cocoa/widgets/selection.py | 3 ++- cocoa/src/toga_cocoa/widgets/slider.py | 3 ++- cocoa/src/toga_cocoa/widgets/splitcontainer.py | 2 +- cocoa/src/toga_cocoa/widgets/switch.py | 3 ++- cocoa/src/toga_cocoa/widgets/table.py | 2 +- cocoa/src/toga_cocoa/widgets/textinput.py | 3 ++- cocoa/src/toga_cocoa/widgets/tree.py | 2 +- cocoa/src/toga_cocoa/widgets/webview.py | 2 +- core/src/toga/fonts.py | 6 +++--- core/src/toga/style/pack.py | 17 +++++++++-------- core/src/toga/widgets/base.py | 5 +++-- core/src/toga/widgets/canvas/canvas.py | 3 ++- core/src/toga/widgets/canvas/context.py | 3 ++- core/src/toga/widgets/canvas/drawingobject.py | 3 ++- core/src/toga/widgets/imageview.py | 3 ++- core/tests/style/pack/layout/test_beeliza.py | 3 ++- core/tests/style/pack/layout/test_fixed.py | 3 ++- core/tests/style/pack/layout/test_flex.py | 3 ++- core/tests/style/pack/layout/test_gap.py | 3 ++- .../style/pack/layout/test_justify_content.py | 2 +- core/tests/style/pack/layout/test_tutorial0.py | 3 ++- core/tests/style/pack/layout/test_tutorial1.py | 3 ++- core/tests/style/pack/layout/test_tutorial3.py | 3 ++- core/tests/style/pack/utils.py | 3 ++- core/tests/widgets/test_imageview.py | 2 +- dummy/src/toga_dummy/utils.py | 1 - examples/canvas/canvas/app.py | 3 ++- examples/colors/colors/app.py | 3 ++- examples/focus/focus/app.py | 3 ++- gtk/src/toga_gtk/widgets/button.py | 3 ++- gtk/src/toga_gtk/widgets/canvas.py | 3 ++- gtk/src/toga_gtk/widgets/detailedlist.py | 3 ++- gtk/src/toga_gtk/widgets/mapview.py | 3 ++- gtk/src/toga_gtk/widgets/numberinput.py | 3 ++- gtk/src/toga_gtk/widgets/slider.py | 3 ++- gtk/src/toga_gtk/widgets/table.py | 3 ++- gtk/src/toga_gtk/widgets/textinput.py | 3 ++- gtk/src/toga_gtk/widgets/webview.py | 3 ++- iOS/src/toga_iOS/widgets/box.py | 2 +- iOS/src/toga_iOS/widgets/button.py | 2 +- iOS/src/toga_iOS/widgets/canvas.py | 2 +- iOS/src/toga_iOS/widgets/detailedlist.py | 2 +- iOS/src/toga_iOS/widgets/divider.py | 3 ++- iOS/src/toga_iOS/widgets/label.py | 2 +- iOS/src/toga_iOS/widgets/mapview.py | 2 +- iOS/src/toga_iOS/widgets/multilinetextinput.py | 2 +- iOS/src/toga_iOS/widgets/numberinput.py | 2 +- iOS/src/toga_iOS/widgets/optioncontainer.py | 2 +- iOS/src/toga_iOS/widgets/progressbar.py | 3 ++- iOS/src/toga_iOS/widgets/scrollcontainer.py | 2 +- iOS/src/toga_iOS/widgets/selection.py | 2 +- iOS/src/toga_iOS/widgets/slider.py | 2 +- iOS/src/toga_iOS/widgets/switch.py | 2 +- iOS/src/toga_iOS/widgets/textinput.py | 2 +- iOS/src/toga_iOS/widgets/webview.py | 2 +- pyproject.toml | 2 ++ textual/src/toga_textual/widgets/base.py | 3 ++- textual/src/toga_textual/widgets/box.py | 3 ++- textual/src/toga_textual/widgets/button.py | 3 ++- textual/src/toga_textual/widgets/label.py | 3 ++- textual/src/toga_textual/widgets/textinput.py | 3 ++- winforms/src/toga_winforms/colors.py | 1 - winforms/src/toga_winforms/widgets/base.py | 2 +- winforms/src/toga_winforms/widgets/button.py | 1 - winforms/src/toga_winforms/widgets/divider.py | 1 - winforms/src/toga_winforms/widgets/label.py | 2 +- .../toga_winforms/widgets/multilinetextinput.py | 2 +- .../toga_winforms/widgets/scrollcontainer.py | 2 +- winforms/src/toga_winforms/widgets/switch.py | 1 - 91 files changed, 134 insertions(+), 99 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5834611e1..8c35492180 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,6 +18,9 @@ repos: rev: 5.13.2 hooks: - id: isort + args: [--settings-path=travertino] + files: travertino + - id: isort - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.10.0 hooks: diff --git a/android/src/toga_android/colors.py b/android/src/toga_android/colors.py index 0c23dce56c..ffd8a71e30 100644 --- a/android/src/toga_android/colors.py +++ b/android/src/toga_android/colors.py @@ -1,5 +1,4 @@ from android.graphics import Color - from travertino.colors import TRANSPARENT CACHE = {TRANSPARENT: Color.TRANSPARENT} diff --git a/android/src/toga_android/widgets/base.py b/android/src/toga_android/widgets/base.py index 6a3d1e02d1..3d25006bb1 100644 --- a/android/src/toga_android/widgets/base.py +++ b/android/src/toga_android/widgets/base.py @@ -6,9 +6,9 @@ from android.view import Gravity, View from android.widget import RelativeLayout from org.beeware.android import MainActivity +from travertino.size import at_least from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT, TRANSPARENT -from travertino.size import at_least from ..colors import native_color diff --git a/android/src/toga_android/widgets/button.py b/android/src/toga_android/widgets/button.py index 9207b8c9cb..a095f9b07e 100644 --- a/android/src/toga_android/widgets/button.py +++ b/android/src/toga_android/widgets/button.py @@ -3,7 +3,6 @@ from android.view import View from android.widget import Button as A_Button from java import dynamic_proxy - from travertino.size import at_least from .label import TextViewWidget diff --git a/android/src/toga_android/widgets/divider.py b/android/src/toga_android/widgets/divider.py index 9f9db1e989..3c856abac2 100644 --- a/android/src/toga_android/widgets/divider.py +++ b/android/src/toga_android/widgets/divider.py @@ -1,7 +1,6 @@ from android.graphics import Color from android.view import View from android.widget import LinearLayout - from travertino.size import at_least from .base import Widget diff --git a/android/src/toga_android/widgets/internal/pickers.py b/android/src/toga_android/widgets/internal/pickers.py index 0a020bc0d5..61e00a1d46 100644 --- a/android/src/toga_android/widgets/internal/pickers.py +++ b/android/src/toga_android/widgets/internal/pickers.py @@ -4,7 +4,6 @@ from android.view import View from android.widget import EditText from java import dynamic_proxy - from travertino.size import at_least from ..label import TextViewWidget diff --git a/android/src/toga_android/widgets/label.py b/android/src/toga_android/widgets/label.py index 3ba18337f3..0aaa149e20 100644 --- a/android/src/toga_android/widgets/label.py +++ b/android/src/toga_android/widgets/label.py @@ -5,10 +5,10 @@ from android.util import TypedValue from android.view import Gravity, View from android.widget import TextView +from travertino.size import at_least from toga.constants import JUSTIFY from toga_android.colors import native_color -from travertino.size import at_least from .base import Widget, android_text_align diff --git a/android/src/toga_android/widgets/multilinetextinput.py b/android/src/toga_android/widgets/multilinetextinput.py index 2dac2fb7c8..545a65127f 100644 --- a/android/src/toga_android/widgets/multilinetextinput.py +++ b/android/src/toga_android/widgets/multilinetextinput.py @@ -1,6 +1,5 @@ from android.text import InputType from android.view import Gravity - from travertino.size import at_least from .textinput import TextInput diff --git a/android/src/toga_android/widgets/optioncontainer.py b/android/src/toga_android/widgets/optioncontainer.py index 694a309861..c44f09cf94 100644 --- a/android/src/toga_android/widgets/optioncontainer.py +++ b/android/src/toga_android/widgets/optioncontainer.py @@ -16,9 +16,9 @@ NavigationBarView = None from java import dynamic_proxy +from travertino.size import at_least import toga -from travertino.size import at_least from ..container import Container from .base import Widget diff --git a/android/src/toga_android/widgets/switch.py b/android/src/toga_android/widgets/switch.py index 888f6daf75..d1202171e1 100644 --- a/android/src/toga_android/widgets/switch.py +++ b/android/src/toga_android/widgets/switch.py @@ -3,7 +3,6 @@ from android.view import View from android.widget import CompoundButton, Switch as A_Switch from java import dynamic_proxy - from travertino.size import at_least from .label import TextViewWidget diff --git a/cocoa/src/toga_cocoa/widgets/box.py b/cocoa/src/toga_cocoa/widgets/box.py index 6b7b3bd410..de3273ecd8 100644 --- a/cocoa/src/toga_cocoa/widgets/box.py +++ b/cocoa/src/toga_cocoa/widgets/box.py @@ -1,6 +1,7 @@ -from toga_cocoa.container import TogaView from travertino.size import at_least +from toga_cocoa.container import TogaView + from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/button.py b/cocoa/src/toga_cocoa/widgets/button.py index 52c96aca1c..eadb8e2b92 100644 --- a/cocoa/src/toga_cocoa/widgets/button.py +++ b/cocoa/src/toga_cocoa/widgets/button.py @@ -1,4 +1,5 @@ from rubicon.objc import SEL, objc_method, objc_property +from travertino.size import at_least from toga.colors import TRANSPARENT from toga.fonts import SYSTEM_DEFAULT_FONT_SIZE @@ -9,7 +10,6 @@ NSButton, NSMomentaryPushInButton, ) -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/canvas.py b/cocoa/src/toga_cocoa/widgets/canvas.py index 946dbf0249..2572ad2a31 100644 --- a/cocoa/src/toga_cocoa/widgets/canvas.py +++ b/cocoa/src/toga_cocoa/widgets/canvas.py @@ -1,6 +1,7 @@ from math import ceil from rubicon.objc import CGSize, objc_method, objc_property +from travertino.size import at_least from toga.colors import BLACK, TRANSPARENT, color from toga.constants import Baseline, FillRule @@ -26,7 +27,6 @@ kCGPathFill, kCGPathStroke, ) -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/detailedlist.py b/cocoa/src/toga_cocoa/widgets/detailedlist.py index f1c3ddc253..d1a17875ca 100644 --- a/cocoa/src/toga_cocoa/widgets/detailedlist.py +++ b/cocoa/src/toga_cocoa/widgets/detailedlist.py @@ -1,4 +1,5 @@ from rubicon.objc import SEL, objc_method, objc_property +from travertino.size import at_least from toga_cocoa.libs import ( NSIndexSet, @@ -11,7 +12,6 @@ from toga_cocoa.widgets.internal.cells import TogaDetailedCell from toga_cocoa.widgets.internal.data import TogaData from toga_cocoa.widgets.internal.refresh import RefreshableScrollView -from travertino.size import at_least class TogaList(NSTableView): diff --git a/cocoa/src/toga_cocoa/widgets/divider.py b/cocoa/src/toga_cocoa/widgets/divider.py index 60b5eaea9e..0d983b9198 100644 --- a/cocoa/src/toga_cocoa/widgets/divider.py +++ b/cocoa/src/toga_cocoa/widgets/divider.py @@ -1,6 +1,7 @@ -from toga_cocoa.libs import NSBox, NSBoxType from travertino.size import at_least +from toga_cocoa.libs import NSBox, NSBoxType + from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/label.py b/cocoa/src/toga_cocoa/widgets/label.py index 1eb3d68401..2c1dfd9c65 100644 --- a/cocoa/src/toga_cocoa/widgets/label.py +++ b/cocoa/src/toga_cocoa/widgets/label.py @@ -1,6 +1,7 @@ +from travertino.size import at_least + from toga_cocoa.colors import native_color from toga_cocoa.libs import NSTextAlignment, NSTextField -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/mapview.py b/cocoa/src/toga_cocoa/widgets/mapview.py index a873d62226..2dd8470240 100644 --- a/cocoa/src/toga_cocoa/widgets/mapview.py +++ b/cocoa/src/toga_cocoa/widgets/mapview.py @@ -1,9 +1,9 @@ import math from rubicon.objc import objc_method, objc_property +from travertino.size import at_least from toga.types import LatLng -from travertino.size import at_least from ..libs import ( CLLocationCoordinate2D, diff --git a/cocoa/src/toga_cocoa/widgets/multilinetextinput.py b/cocoa/src/toga_cocoa/widgets/multilinetextinput.py index aaa7850c3d..4935060bd2 100644 --- a/cocoa/src/toga_cocoa/widgets/multilinetextinput.py +++ b/cocoa/src/toga_cocoa/widgets/multilinetextinput.py @@ -1,4 +1,5 @@ from rubicon.objc import objc_method, objc_property +from travertino.size import at_least from toga.colors import TRANSPARENT from toga_cocoa.colors import native_color @@ -10,7 +11,6 @@ NSViewHeightSizable, NSViewWidthSizable, ) -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/numberinput.py b/cocoa/src/toga_cocoa/widgets/numberinput.py index f3ad3110cd..64b956e884 100644 --- a/cocoa/src/toga_cocoa/widgets/numberinput.py +++ b/cocoa/src/toga_cocoa/widgets/numberinput.py @@ -2,6 +2,7 @@ from decimal import Decimal, InvalidOperation from rubicon.objc import SEL, objc_method, objc_property +from travertino.size import at_least from toga.colors import TRANSPARENT from toga.widgets.numberinput import _clean_decimal, _clean_decimal_str @@ -21,7 +22,6 @@ NSTextView, NSView, ) -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/optioncontainer.py b/cocoa/src/toga_cocoa/widgets/optioncontainer.py index 55b3929a15..ccf1f16c74 100644 --- a/cocoa/src/toga_cocoa/widgets/optioncontainer.py +++ b/cocoa/src/toga_cocoa/widgets/optioncontainer.py @@ -1,10 +1,10 @@ import warnings from rubicon.objc import SEL, objc_method +from travertino.size import at_least from toga_cocoa.container import Container from toga_cocoa.libs import NSTabView, NSTabViewItem -from travertino.size import at_least from ..libs import objc_property from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/progressbar.py b/cocoa/src/toga_cocoa/widgets/progressbar.py index 808ab5bdc0..691d1e488a 100644 --- a/cocoa/src/toga_cocoa/widgets/progressbar.py +++ b/cocoa/src/toga_cocoa/widgets/progressbar.py @@ -1,6 +1,7 @@ -from toga_cocoa.libs import NSProgressIndicator, NSProgressIndicatorBarStyle from travertino.size import at_least +from toga_cocoa.libs import NSProgressIndicator, NSProgressIndicatorBarStyle + from .base import Widget # Implementation notes diff --git a/cocoa/src/toga_cocoa/widgets/scrollcontainer.py b/cocoa/src/toga_cocoa/widgets/scrollcontainer.py index 5e1d91b93a..40721bdeee 100644 --- a/cocoa/src/toga_cocoa/widgets/scrollcontainer.py +++ b/cocoa/src/toga_cocoa/widgets/scrollcontainer.py @@ -1,4 +1,5 @@ from rubicon.objc import SEL, objc_method, objc_property +from travertino.size import at_least from toga_cocoa.container import Container from toga_cocoa.libs import ( @@ -11,7 +12,6 @@ NSScrollViewDidEndLiveScrollNotification, NSScrollViewDidLiveScrollNotification, ) -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/selection.py b/cocoa/src/toga_cocoa/widgets/selection.py index 8347504415..3bc09fdd47 100644 --- a/cocoa/src/toga_cocoa/widgets/selection.py +++ b/cocoa/src/toga_cocoa/widgets/selection.py @@ -1,6 +1,7 @@ -from toga_cocoa.libs import SEL, NSPopUpButton, objc_method, objc_property from travertino.size import at_least +from toga_cocoa.libs import SEL, NSPopUpButton, objc_method, objc_property + from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/slider.py b/cocoa/src/toga_cocoa/widgets/slider.py index ce56155b18..3154c674c7 100644 --- a/cocoa/src/toga_cocoa/widgets/slider.py +++ b/cocoa/src/toga_cocoa/widgets/slider.py @@ -1,3 +1,5 @@ +from travertino.size import at_least + from toga.widgets.slider import SliderImpl from toga_cocoa.libs import ( SEL, @@ -6,7 +8,6 @@ objc_method, objc_property, ) -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/splitcontainer.py b/cocoa/src/toga_cocoa/widgets/splitcontainer.py index b120f76ef7..8ce5296d3f 100644 --- a/cocoa/src/toga_cocoa/widgets/splitcontainer.py +++ b/cocoa/src/toga_cocoa/widgets/splitcontainer.py @@ -1,9 +1,9 @@ from rubicon.objc import SEL, objc_method, objc_property +from travertino.size import at_least from toga.constants import Direction from toga_cocoa.container import Container from toga_cocoa.libs import NSSplitView -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/switch.py b/cocoa/src/toga_cocoa/widgets/switch.py index 0f451772ee..15f92bc6aa 100644 --- a/cocoa/src/toga_cocoa/widgets/switch.py +++ b/cocoa/src/toga_cocoa/widgets/switch.py @@ -1,3 +1,5 @@ +from travertino.size import at_least + from toga_cocoa.libs import ( SEL, NSBezelStyle, @@ -8,7 +10,6 @@ objc_method, objc_property, ) -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/table.py b/cocoa/src/toga_cocoa/widgets/table.py index 2ddca44005..76e71c4f07 100644 --- a/cocoa/src/toga_cocoa/widgets/table.py +++ b/cocoa/src/toga_cocoa/widgets/table.py @@ -1,4 +1,5 @@ from rubicon.objc import SEL, NSPoint, at, objc_method, objc_property +from travertino.size import at_least import toga from toga_cocoa.libs import ( @@ -11,7 +12,6 @@ NSTableViewAnimation, NSTableViewColumnAutoresizingStyle, ) -from travertino.size import at_least from .base import Widget from .internal.cells import TogaIconView diff --git a/cocoa/src/toga_cocoa/widgets/textinput.py b/cocoa/src/toga_cocoa/widgets/textinput.py index adaaf698f8..e3e5995b06 100644 --- a/cocoa/src/toga_cocoa/widgets/textinput.py +++ b/cocoa/src/toga_cocoa/widgets/textinput.py @@ -1,3 +1,5 @@ +from travertino.size import at_least + from toga.colors import TRANSPARENT from toga.constants import LEFT, RIGHT from toga_cocoa.colors import native_color @@ -18,7 +20,6 @@ objc_property, send_super, ) -from travertino.size import at_least from .base import Widget diff --git a/cocoa/src/toga_cocoa/widgets/tree.py b/cocoa/src/toga_cocoa/widgets/tree.py index 6dac318538..d6729a6995 100644 --- a/cocoa/src/toga_cocoa/widgets/tree.py +++ b/cocoa/src/toga_cocoa/widgets/tree.py @@ -1,4 +1,5 @@ from rubicon.objc import SEL, at, objc_method, objc_property +from travertino.size import at_least import toga from toga_cocoa.libs import ( @@ -13,7 +14,6 @@ from toga_cocoa.widgets.base import Widget from toga_cocoa.widgets.internal.cells import TogaIconView from toga_cocoa.widgets.internal.data import TogaData -from travertino.size import at_least def node_impl(node): diff --git a/cocoa/src/toga_cocoa/widgets/webview.py b/cocoa/src/toga_cocoa/widgets/webview.py index 4dc8c494b6..eb58ede63f 100644 --- a/cocoa/src/toga_cocoa/widgets/webview.py +++ b/cocoa/src/toga_cocoa/widgets/webview.py @@ -1,9 +1,9 @@ from http.cookiejar import Cookie, CookieJar from rubicon.objc import objc_id, objc_method, objc_property, py_from_ns +from travertino.size import at_least from toga.widgets.webview import CookiesResult, JavaScriptResult -from travertino.size import at_least from ..libs import NSURL, NSURLRequest, WKWebView from .base import Widget diff --git a/core/src/toga/fonts.py b/core/src/toga/fonts.py index 8476c3f981..f2a7016d49 100644 --- a/core/src/toga/fonts.py +++ b/core/src/toga/fonts.py @@ -2,9 +2,6 @@ from pathlib import Path -import toga -from toga.platform import get_platform_factory - # Use the Travertino font definitions as-is from travertino import constants from travertino.constants import ( @@ -23,6 +20,9 @@ ) from travertino.fonts import Font as BaseFont +import toga +from toga.platform import get_platform_factory + SYSTEM_DEFAULT_FONTS = {SYSTEM, MESSAGE, SERIF, SANS_SERIF, CURSIVE, FANTASY, MONOSPACE} SYSTEM_DEFAULT_FONT_SIZE = -1 FONT_WEIGHTS = {NORMAL, BOLD} diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 72fdfa993d..e801e87cda 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -6,14 +6,6 @@ if TYPE_CHECKING: from travertino.colors import rgb, hsl -from toga.fonts import ( - FONT_STYLES, - FONT_VARIANTS, - FONT_WEIGHTS, - SYSTEM_DEFAULT_FONT_SIZE, - SYSTEM_DEFAULT_FONTS, - Font, -) from travertino.constants import ( # noqa: F401 BOLD, BOTTOM, @@ -50,6 +42,15 @@ from travertino.layout import BaseBox from travertino.size import BaseIntrinsicSize +from toga.fonts import ( + FONT_STYLES, + FONT_VARIANTS, + FONT_WEIGHTS, + SYSTEM_DEFAULT_FONT_SIZE, + SYSTEM_DEFAULT_FONTS, + Font, +) + # Make sure deprecation warnings are shown by default warnings.filterwarnings("default", category=DeprecationWarning) diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index d8b000abfc..447845cff9 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -4,11 +4,12 @@ from typing import TYPE_CHECKING, Any, TypeVar from warnings import warn +from travertino.declaration import BaseStyle +from travertino.node import Node + from toga.platform import get_platform_factory from toga.style import Pack, TogaApplicator from toga.style.mixin import style_mixin -from travertino.declaration import BaseStyle -from travertino.node import Node if TYPE_CHECKING: from toga.app import App diff --git a/core/src/toga/widgets/canvas/canvas.py b/core/src/toga/widgets/canvas/canvas.py index d685e1d32f..d5955683d6 100644 --- a/core/src/toga/widgets/canvas/canvas.py +++ b/core/src/toga/widgets/canvas/canvas.py @@ -8,6 +8,8 @@ Protocol, ) +from travertino.colors import Color + import toga from toga.colors import BLACK from toga.constants import FillRule @@ -17,7 +19,6 @@ Font, ) from toga.handlers import wrapped_handler -from travertino.colors import Color from ..base import StyleT, Widget from .context import ClosedPathContext, Context, FillContext, StrokeContext diff --git a/core/src/toga/widgets/canvas/context.py b/core/src/toga/widgets/canvas/context.py index c6c7806bd0..f779795149 100644 --- a/core/src/toga/widgets/canvas/context.py +++ b/core/src/toga/widgets/canvas/context.py @@ -5,11 +5,12 @@ from math import pi from typing import TYPE_CHECKING, Any +from travertino.colors import Color + import toga from toga.colors import BLACK, color as parse_color from toga.constants import Baseline, FillRule from toga.fonts import Font -from travertino.colors import Color from .drawingobject import ( Arc, diff --git a/core/src/toga/widgets/canvas/drawingobject.py b/core/src/toga/widgets/canvas/drawingobject.py index dd211db07c..69c5457f8e 100644 --- a/core/src/toga/widgets/canvas/drawingobject.py +++ b/core/src/toga/widgets/canvas/drawingobject.py @@ -4,6 +4,8 @@ from math import pi from typing import Any +from travertino.colors import Color + from toga.colors import BLACK, color as parse_color from toga.constants import Baseline, FillRule from toga.fonts import ( @@ -11,7 +13,6 @@ SYSTEM_DEFAULT_FONT_SIZE, Font, ) -from travertino.colors import Color class DrawingObject(ABC): diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index cce0807e8c..59f9005a72 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -2,10 +2,11 @@ from typing import TYPE_CHECKING, Any, Literal +from travertino.size import at_least + import toga from toga.style.pack import NONE from toga.widgets.base import StyleT, Widget -from travertino.size import at_least if TYPE_CHECKING: from toga.images import ImageContentT, ImageT diff --git a/core/tests/style/pack/layout/test_beeliza.py b/core/tests/style/pack/layout/test_beeliza.py index 064b1df0e7..93499ebbb1 100644 --- a/core/tests/style/pack/layout/test_beeliza.py +++ b/core/tests/style/pack/layout/test_beeliza.py @@ -1,6 +1,7 @@ -from toga.style.pack import COLUMN, ROW, Pack from travertino.size import at_least +from toga.style.pack import COLUMN, ROW, Pack + from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_fixed.py b/core/tests/style/pack/layout/test_fixed.py index b4d322fe3f..29a19034cd 100644 --- a/core/tests/style/pack/layout/test_fixed.py +++ b/core/tests/style/pack/layout/test_fixed.py @@ -1,6 +1,7 @@ -from toga.style.pack import COLUMN, ROW, Pack from travertino.size import at_least +from toga.style.pack import COLUMN, ROW, Pack + from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_flex.py b/core/tests/style/pack/layout/test_flex.py index f4a92a06c7..0563ab0f20 100644 --- a/core/tests/style/pack/layout/test_flex.py +++ b/core/tests/style/pack/layout/test_flex.py @@ -1,6 +1,7 @@ -from toga.style.pack import COLUMN, ROW, Pack from travertino.size import at_least +from toga.style.pack import COLUMN, ROW, Pack + from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_gap.py b/core/tests/style/pack/layout/test_gap.py index a098afb4a4..e79a4387ad 100644 --- a/core/tests/style/pack/layout/test_gap.py +++ b/core/tests/style/pack/layout/test_gap.py @@ -1,6 +1,7 @@ -from toga.style.pack import Pack from travertino.size import at_least +from toga.style.pack import Pack + from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_justify_content.py b/core/tests/style/pack/layout/test_justify_content.py index 2876eaa48e..5d876ccc8f 100644 --- a/core/tests/style/pack/layout/test_justify_content.py +++ b/core/tests/style/pack/layout/test_justify_content.py @@ -1,7 +1,7 @@ import pytest +from travertino.size import at_least from toga.style.pack import Pack -from travertino.size import at_least from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_tutorial0.py b/core/tests/style/pack/layout/test_tutorial0.py index 41805b3aba..ec9d1588a7 100644 --- a/core/tests/style/pack/layout/test_tutorial0.py +++ b/core/tests/style/pack/layout/test_tutorial0.py @@ -1,6 +1,7 @@ -from toga.style.pack import COLUMN, Pack from travertino.size import at_least +from toga.style.pack import COLUMN, Pack + from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_tutorial1.py b/core/tests/style/pack/layout/test_tutorial1.py index a0eb2d3f31..24e7199fd1 100644 --- a/core/tests/style/pack/layout/test_tutorial1.py +++ b/core/tests/style/pack/layout/test_tutorial1.py @@ -1,6 +1,7 @@ -from toga.style.pack import COLUMN, ROW, Pack from travertino.size import at_least +from toga.style.pack import COLUMN, ROW, Pack + from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/layout/test_tutorial3.py b/core/tests/style/pack/layout/test_tutorial3.py index 1b7982f6dd..8d1c5d8302 100644 --- a/core/tests/style/pack/layout/test_tutorial3.py +++ b/core/tests/style/pack/layout/test_tutorial3.py @@ -1,6 +1,7 @@ -from toga.style.pack import COLUMN, Pack from travertino.size import at_least +from toga.style.pack import COLUMN, Pack + from ..utils import ExampleNode, ExampleViewport, assert_layout diff --git a/core/tests/style/pack/utils.py b/core/tests/style/pack/utils.py index 6e0c7cb2eb..0dabc6ef11 100644 --- a/core/tests/style/pack/utils.py +++ b/core/tests/style/pack/utils.py @@ -1,8 +1,9 @@ from unittest.mock import Mock -from toga.style.applicator import TogaApplicator from travertino.node import Node +from toga.style.applicator import TogaApplicator + class ExampleNode(Node): def __init__(self, name, style, size=None, children=None): diff --git a/core/tests/widgets/test_imageview.py b/core/tests/widgets/test_imageview.py index 0e6bb3e864..a7036e2895 100644 --- a/core/tests/widgets/test_imageview.py +++ b/core/tests/widgets/test_imageview.py @@ -3,6 +3,7 @@ import PIL.Image import pytest +from travertino.size import at_least import toga from toga.style.pack import Pack @@ -12,7 +13,6 @@ assert_action_performed, assert_action_performed_with, ) -from travertino.size import at_least @pytest.fixture diff --git a/dummy/src/toga_dummy/utils.py b/dummy/src/toga_dummy/utils.py index c29b99f265..9dce87b62f 100644 --- a/dummy/src/toga_dummy/utils.py +++ b/dummy/src/toga_dummy/utils.py @@ -1,7 +1,6 @@ from unittest.mock import Mock import pytest - from travertino.declaration import BaseStyle from travertino.layout import BaseBox from travertino.size import BaseIntrinsicSize diff --git a/examples/canvas/canvas/app.py b/examples/canvas/canvas/app.py index 73a5075d2e..311774889d 100644 --- a/examples/canvas/canvas/app.py +++ b/examples/canvas/canvas/app.py @@ -1,12 +1,13 @@ import math import sys +from travertino.constants import BLACK, BLUE, BOLD, GREEN, ITALIC, NORMAL, RED, YELLOW + import toga from toga.constants import Baseline, FillRule from toga.fonts import CURSIVE, FANTASY, MESSAGE, MONOSPACE, SANS_SERIF, SERIF, SYSTEM from toga.style import Pack from toga.style.pack import COLUMN, ROW -from travertino.constants import BLACK, BLUE, BOLD, GREEN, ITALIC, NORMAL, RED, YELLOW MOVE_STEP = 5 diff --git a/examples/colors/colors/app.py b/examples/colors/colors/app.py index 8f4f04dfbf..203057e388 100644 --- a/examples/colors/colors/app.py +++ b/examples/colors/colors/app.py @@ -1,7 +1,8 @@ +from travertino import colors + import toga from toga.constants import COLUMN, ROW from toga.style import Pack -from travertino import colors class ColorsApp(toga.App): diff --git a/examples/focus/focus/app.py b/examples/focus/focus/app.py index 1b7dd40448..8b024f7b9a 100644 --- a/examples/focus/focus/app.py +++ b/examples/focus/focus/app.py @@ -1,8 +1,9 @@ import random +from travertino.constants import COLUMN + import toga from toga.style import Pack -from travertino.constants import COLUMN WIDGETS_GROUP = toga.Group("Widgets", order=2) FOCUS_ORDER_GROUP = toga.Group("Focus Order", order=3) diff --git a/gtk/src/toga_gtk/widgets/button.py b/gtk/src/toga_gtk/widgets/button.py index 4e3015ad5f..48f9b323ed 100644 --- a/gtk/src/toga_gtk/widgets/button.py +++ b/gtk/src/toga_gtk/widgets/button.py @@ -1,6 +1,7 @@ -from toga.colors import TRANSPARENT from travertino.size import at_least +from toga.colors import TRANSPARENT + from ..libs import Gtk from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/canvas.py b/gtk/src/toga_gtk/widgets/canvas.py index 20d51a6443..fe05c28e13 100644 --- a/gtk/src/toga_gtk/widgets/canvas.py +++ b/gtk/src/toga_gtk/widgets/canvas.py @@ -2,12 +2,13 @@ from io import BytesIO from math import ceil +from travertino.size import at_least + from toga import Font from toga.constants import Baseline, FillRule from toga.fonts import SYSTEM_DEFAULT_FONT_SIZE from toga_gtk.colors import native_color from toga_gtk.libs import Gdk, Gtk, Pango, PangoCairo, cairo -from travertino.size import at_least from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/detailedlist.py b/gtk/src/toga_gtk/widgets/detailedlist.py index 43fd929225..d9466266a5 100644 --- a/gtk/src/toga_gtk/widgets/detailedlist.py +++ b/gtk/src/toga_gtk/widgets/detailedlist.py @@ -1,8 +1,9 @@ import html -from toga_gtk.libs import Gdk, Gio, Gtk, Pango from travertino.size import at_least +from toga_gtk.libs import Gdk, Gio, Gtk, Pango + from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/mapview.py b/gtk/src/toga_gtk/widgets/mapview.py index cf9dc3dcfa..9b77c18076 100644 --- a/gtk/src/toga_gtk/widgets/mapview.py +++ b/gtk/src/toga_gtk/widgets/mapview.py @@ -1,8 +1,9 @@ from concurrent.futures import Future -from toga.types import LatLng from travertino.size import at_least +from toga.types import LatLng + from ..libs import Gtk, WebKit2 from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/numberinput.py b/gtk/src/toga_gtk/widgets/numberinput.py index f8001ac2e3..59fc4a3217 100644 --- a/gtk/src/toga_gtk/widgets/numberinput.py +++ b/gtk/src/toga_gtk/widgets/numberinput.py @@ -1,9 +1,10 @@ import sys from decimal import InvalidOperation -from toga.widgets.numberinput import _clean_decimal from travertino.size import at_least +from toga.widgets.numberinput import _clean_decimal + from ..libs import Gtk, gtk_text_align from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/slider.py b/gtk/src/toga_gtk/widgets/slider.py index b6a268d2ce..33e78b5037 100644 --- a/gtk/src/toga_gtk/widgets/slider.py +++ b/gtk/src/toga_gtk/widgets/slider.py @@ -1,6 +1,7 @@ -from toga.widgets.slider import SliderImpl from travertino.size import at_least +from toga.widgets.slider import SliderImpl + from ..libs import Gtk from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/table.py b/gtk/src/toga_gtk/widgets/table.py index 679da83c89..b50b2ac95d 100644 --- a/gtk/src/toga_gtk/widgets/table.py +++ b/gtk/src/toga_gtk/widgets/table.py @@ -1,8 +1,9 @@ import warnings -import toga from travertino.size import at_least +import toga + from ..libs import GdkPixbuf, GObject, Gtk from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/textinput.py b/gtk/src/toga_gtk/widgets/textinput.py index 7d6ce8e468..141ec5de54 100644 --- a/gtk/src/toga_gtk/widgets/textinput.py +++ b/gtk/src/toga_gtk/widgets/textinput.py @@ -1,6 +1,7 @@ +from travertino.size import at_least + from toga.keys import Key from toga_gtk.keys import toga_key -from travertino.size import at_least from ..libs import Gtk, gtk_text_align from .base import Widget diff --git a/gtk/src/toga_gtk/widgets/webview.py b/gtk/src/toga_gtk/widgets/webview.py index 0937b2e2e8..94c066621e 100644 --- a/gtk/src/toga_gtk/widgets/webview.py +++ b/gtk/src/toga_gtk/widgets/webview.py @@ -1,8 +1,9 @@ from http.cookiejar import CookieJar -from toga.widgets.webview import CookiesResult, JavaScriptResult from travertino.size import at_least +from toga.widgets.webview import CookiesResult, JavaScriptResult + from ..libs import GLib, WebKit2 from .base import Widget diff --git a/iOS/src/toga_iOS/widgets/box.py b/iOS/src/toga_iOS/widgets/box.py index 010c4e78f6..e0c55847fa 100644 --- a/iOS/src/toga_iOS/widgets/box.py +++ b/iOS/src/toga_iOS/widgets/box.py @@ -1,8 +1,8 @@ from rubicon.objc import objc_property +from travertino.size import at_least from toga_iOS.libs import UIView from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaView(UIView): diff --git a/iOS/src/toga_iOS/widgets/button.py b/iOS/src/toga_iOS/widgets/button.py index 0244a5602a..2d050a12ea 100644 --- a/iOS/src/toga_iOS/widgets/button.py +++ b/iOS/src/toga_iOS/widgets/button.py @@ -1,4 +1,5 @@ from rubicon.objc import SEL, CGSize, objc_method, objc_property +from travertino.size import at_least from toga.colors import TRANSPARENT from toga_iOS.colors import native_color @@ -10,7 +11,6 @@ UIControlStateNormal, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaButton(UIButton): diff --git a/iOS/src/toga_iOS/widgets/canvas.py b/iOS/src/toga_iOS/widgets/canvas.py index ea665db80d..9fc213f304 100644 --- a/iOS/src/toga_iOS/widgets/canvas.py +++ b/iOS/src/toga_iOS/widgets/canvas.py @@ -13,6 +13,7 @@ objc_method, objc_property, ) +from travertino.size import at_least from toga.colors import BLACK, TRANSPARENT, color from toga.constants import Baseline, FillRule @@ -36,7 +37,6 @@ uikit, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaCanvas(UIView): diff --git a/iOS/src/toga_iOS/widgets/detailedlist.py b/iOS/src/toga_iOS/widgets/detailedlist.py index 1dcbf8e804..5dd9b8a51a 100644 --- a/iOS/src/toga_iOS/widgets/detailedlist.py +++ b/iOS/src/toga_iOS/widgets/detailedlist.py @@ -5,6 +5,7 @@ objc_method, objc_property, ) +from travertino.size import at_least from toga_iOS.libs import ( NSIndexPath, @@ -20,7 +21,6 @@ UITableViewScrollPositionNone, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaTableViewController(UITableViewController): diff --git a/iOS/src/toga_iOS/widgets/divider.py b/iOS/src/toga_iOS/widgets/divider.py index 1f53fed4bb..cd785d3396 100644 --- a/iOS/src/toga_iOS/widgets/divider.py +++ b/iOS/src/toga_iOS/widgets/divider.py @@ -1,6 +1,7 @@ +from travertino.size import at_least + from toga_iOS.libs import UIColor from toga_iOS.widgets.base import Widget -from travertino.size import at_least from .box import TogaView diff --git a/iOS/src/toga_iOS/widgets/label.py b/iOS/src/toga_iOS/widgets/label.py index bb96e8f513..fb0de83e1a 100644 --- a/iOS/src/toga_iOS/widgets/label.py +++ b/iOS/src/toga_iOS/widgets/label.py @@ -1,6 +1,7 @@ from math import ceil from rubicon.objc import CGRect, NSInteger, NSMakeRect, objc_method, send_super +from travertino.size import at_least from toga_iOS.colors import native_color from toga_iOS.libs import ( @@ -9,7 +10,6 @@ UILabel, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaLabel(UILabel): diff --git a/iOS/src/toga_iOS/widgets/mapview.py b/iOS/src/toga_iOS/widgets/mapview.py index a873d62226..2dd8470240 100644 --- a/iOS/src/toga_iOS/widgets/mapview.py +++ b/iOS/src/toga_iOS/widgets/mapview.py @@ -1,9 +1,9 @@ import math from rubicon.objc import objc_method, objc_property +from travertino.size import at_least from toga.types import LatLng -from travertino.size import at_least from ..libs import ( CLLocationCoordinate2D, diff --git a/iOS/src/toga_iOS/widgets/multilinetextinput.py b/iOS/src/toga_iOS/widgets/multilinetextinput.py index 01d55aaa0f..6d97b38fed 100644 --- a/iOS/src/toga_iOS/widgets/multilinetextinput.py +++ b/iOS/src/toga_iOS/widgets/multilinetextinput.py @@ -1,6 +1,7 @@ from ctypes import c_void_p from rubicon.objc import CGPoint, NSRange, objc_method, objc_property, send_super +from travertino.size import at_least from toga_iOS.colors import native_color from toga_iOS.libs import ( @@ -16,7 +17,6 @@ UITextView, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaMultilineTextView(UITextView, protocols=[UIKeyInput]): diff --git a/iOS/src/toga_iOS/widgets/numberinput.py b/iOS/src/toga_iOS/widgets/numberinput.py index e80a822401..af1cf8f43f 100644 --- a/iOS/src/toga_iOS/widgets/numberinput.py +++ b/iOS/src/toga_iOS/widgets/numberinput.py @@ -2,6 +2,7 @@ from decimal import InvalidOperation from rubicon.objc import SEL, CGSize, NSRange, objc_method, objc_property, send_message +from travertino.size import at_least from toga.widgets.numberinput import _clean_decimal from toga_iOS.colors import native_color @@ -13,7 +14,6 @@ UITextField, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaNumericTextField(UITextField): diff --git a/iOS/src/toga_iOS/widgets/optioncontainer.py b/iOS/src/toga_iOS/widgets/optioncontainer.py index 55d5204857..783c501225 100644 --- a/iOS/src/toga_iOS/widgets/optioncontainer.py +++ b/iOS/src/toga_iOS/widgets/optioncontainer.py @@ -1,10 +1,10 @@ from rubicon.objc import SEL, objc_method, objc_property +from travertino.size import at_least import toga from toga_iOS.container import ControlledContainer from toga_iOS.libs import UITabBarController, UITabBarItem from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaTabBarController(UITabBarController): diff --git a/iOS/src/toga_iOS/widgets/progressbar.py b/iOS/src/toga_iOS/widgets/progressbar.py index cd332d3162..3cbb80c03b 100644 --- a/iOS/src/toga_iOS/widgets/progressbar.py +++ b/iOS/src/toga_iOS/widgets/progressbar.py @@ -1,8 +1,9 @@ import asyncio +from travertino.size import at_least + from toga_iOS.libs import CGSize, UIProgressView, UIProgressViewStyle from toga_iOS.widgets.base import Widget -from travertino.size import at_least # Implementation notes # ==================== diff --git a/iOS/src/toga_iOS/widgets/scrollcontainer.py b/iOS/src/toga_iOS/widgets/scrollcontainer.py index 38a883b03f..a95b9296f0 100644 --- a/iOS/src/toga_iOS/widgets/scrollcontainer.py +++ b/iOS/src/toga_iOS/widgets/scrollcontainer.py @@ -1,9 +1,9 @@ from rubicon.objc import SEL, NSMakePoint, NSMakeSize, objc_method, objc_property +from travertino.size import at_least from toga_iOS.container import Container from toga_iOS.libs import UIScrollView from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaScrollView(UIScrollView): diff --git a/iOS/src/toga_iOS/widgets/selection.py b/iOS/src/toga_iOS/widgets/selection.py index 8cb2011117..3aa05c2441 100644 --- a/iOS/src/toga_iOS/widgets/selection.py +++ b/iOS/src/toga_iOS/widgets/selection.py @@ -1,4 +1,5 @@ from rubicon.objc import CGSize, objc_method, objc_property +from travertino.size import at_least from toga_iOS.colors import native_color from toga_iOS.libs import ( @@ -9,7 +10,6 @@ UITextField, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaBaseTextField(UITextField): diff --git a/iOS/src/toga_iOS/widgets/slider.py b/iOS/src/toga_iOS/widgets/slider.py index 471d3b2f85..6db9758378 100644 --- a/iOS/src/toga_iOS/widgets/slider.py +++ b/iOS/src/toga_iOS/widgets/slider.py @@ -1,4 +1,5 @@ from rubicon.objc import SEL, CGSize, objc_method, objc_property +from travertino.size import at_least from toga_iOS.libs import ( UIControlEventTouchCancel, @@ -9,7 +10,6 @@ UISlider, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least # Implementation notes # ==================== diff --git a/iOS/src/toga_iOS/widgets/switch.py b/iOS/src/toga_iOS/widgets/switch.py index 2bbeb1bf00..89cc6cdb3b 100644 --- a/iOS/src/toga_iOS/widgets/switch.py +++ b/iOS/src/toga_iOS/widgets/switch.py @@ -1,4 +1,5 @@ from rubicon.objc import SEL, CGSize, objc_method, objc_property +from travertino.size import at_least from toga_iOS.colors import native_color from toga_iOS.libs import ( @@ -10,7 +11,6 @@ UISwitch, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaStackView(UIStackView): diff --git a/iOS/src/toga_iOS/widgets/textinput.py b/iOS/src/toga_iOS/widgets/textinput.py index 8ac81047b9..2400ca210c 100644 --- a/iOS/src/toga_iOS/widgets/textinput.py +++ b/iOS/src/toga_iOS/widgets/textinput.py @@ -1,4 +1,5 @@ from rubicon.objc import SEL, CGSize, objc_method, objc_property +from travertino.size import at_least from toga.constants import LEFT, RIGHT from toga_iOS.colors import native_color @@ -16,7 +17,6 @@ UITextField, ) from toga_iOS.widgets.base import Widget -from travertino.size import at_least class TogaTextField(UITextField): diff --git a/iOS/src/toga_iOS/widgets/webview.py b/iOS/src/toga_iOS/widgets/webview.py index ab22fe56d8..0f4e7cfa9b 100644 --- a/iOS/src/toga_iOS/widgets/webview.py +++ b/iOS/src/toga_iOS/widgets/webview.py @@ -1,11 +1,11 @@ from http.cookiejar import Cookie, CookieJar from rubicon.objc import objc_id, objc_method, objc_property, py_from_ns +from travertino.size import at_least from toga.widgets.webview import CookiesResult, JavaScriptResult from toga_iOS.libs import NSURL, NSURLRequest, WKWebView from toga_iOS.widgets.base import Widget -from travertino.size import at_least def js_completion_handler(result): diff --git a/pyproject.toml b/pyproject.toml index d4b453b3ad..5dba9590d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ split_on_trailing_comma = true combine_as_imports = true known_third_party = [ "android", # isort defaults to making this first-party for some reason. + "travertino", # In this repo, but still a separate package ] known_first_party = [ "testbed", @@ -61,6 +62,7 @@ known_first_party = [ "toga_web", "toga_winforms", ] +extend_skip = ["travertino"] [tool.towncrier] directory = "changes" diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index 785a2c658e..91713cffce 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -1,6 +1,7 @@ +from travertino.size import at_least + from toga.style.pack import ROW from toga.types import Size -from travertino.size import at_least # We assume a terminal is 800x600 pixels, mapping to 80x25 characters. diff --git a/textual/src/toga_textual/widgets/box.py b/textual/src/toga_textual/widgets/box.py index 6ac5a5435c..01a363ad33 100644 --- a/textual/src/toga_textual/widgets/box.py +++ b/textual/src/toga_textual/widgets/box.py @@ -1,6 +1,7 @@ +from travertino.size import at_least + from textual.containers import Container as TextualContainer from toga.style.pack import ROW -from travertino.size import at_least from .base import Widget diff --git a/textual/src/toga_textual/widgets/button.py b/textual/src/toga_textual/widgets/button.py index 7708eec995..ec86c2ad33 100644 --- a/textual/src/toga_textual/widgets/button.py +++ b/textual/src/toga_textual/widgets/button.py @@ -1,6 +1,7 @@ -from textual.widgets import Button as TextualButton from travertino.size import at_least +from textual.widgets import Button as TextualButton + from .base import Widget diff --git a/textual/src/toga_textual/widgets/label.py b/textual/src/toga_textual/widgets/label.py index 10c071db00..424d12e071 100644 --- a/textual/src/toga_textual/widgets/label.py +++ b/textual/src/toga_textual/widgets/label.py @@ -1,6 +1,7 @@ -from textual.widgets import Label as TextualLabel from travertino.size import at_least +from textual.widgets import Label as TextualLabel + from .base import Widget diff --git a/textual/src/toga_textual/widgets/textinput.py b/textual/src/toga_textual/widgets/textinput.py index f65ceb618b..df2bbcd0b2 100644 --- a/textual/src/toga_textual/widgets/textinput.py +++ b/textual/src/toga_textual/widgets/textinput.py @@ -1,6 +1,7 @@ -from textual.widgets import Input as TextualInput from travertino.size import at_least +from textual.widgets import Input as TextualInput + from .base import Widget diff --git a/winforms/src/toga_winforms/colors.py b/winforms/src/toga_winforms/colors.py index c96ae2cd31..4a7b89d08f 100644 --- a/winforms/src/toga_winforms/colors.py +++ b/winforms/src/toga_winforms/colors.py @@ -1,5 +1,4 @@ from System.Drawing import Color - from travertino.colors import TRANSPARENT CACHE = {TRANSPARENT: Color.Transparent} diff --git a/winforms/src/toga_winforms/widgets/base.py b/winforms/src/toga_winforms/widgets/base.py index 1385675d97..cf772f5e5e 100644 --- a/winforms/src/toga_winforms/widgets/base.py +++ b/winforms/src/toga_winforms/widgets/base.py @@ -7,10 +7,10 @@ Size, SystemColors, ) +from travertino.size import at_least from toga.colors import TRANSPARENT from toga_winforms.colors import native_color -from travertino.size import at_least class Scalable(ABC): diff --git a/winforms/src/toga_winforms/widgets/button.py b/winforms/src/toga_winforms/widgets/button.py index 61018f7a40..34839a7344 100644 --- a/winforms/src/toga_winforms/widgets/button.py +++ b/winforms/src/toga_winforms/widgets/button.py @@ -1,7 +1,6 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms - from travertino.size import at_least from ..libs.wrapper import WeakrefCallable diff --git a/winforms/src/toga_winforms/widgets/divider.py b/winforms/src/toga_winforms/widgets/divider.py index cc176b297f..86379a121a 100644 --- a/winforms/src/toga_winforms/widgets/divider.py +++ b/winforms/src/toga_winforms/widgets/divider.py @@ -1,7 +1,6 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms - from travertino.size import at_least from .base import Widget diff --git a/winforms/src/toga_winforms/widgets/label.py b/winforms/src/toga_winforms/widgets/label.py index 907bdfdbca..bab8c7e001 100644 --- a/winforms/src/toga_winforms/widgets/label.py +++ b/winforms/src/toga_winforms/widgets/label.py @@ -1,9 +1,9 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms +from travertino.size import at_least from toga_winforms.libs.fonts import TextAlignment -from travertino.size import at_least from .base import Widget diff --git a/winforms/src/toga_winforms/widgets/multilinetextinput.py b/winforms/src/toga_winforms/widgets/multilinetextinput.py index edeb6637b6..db12269b6c 100644 --- a/winforms/src/toga_winforms/widgets/multilinetextinput.py +++ b/winforms/src/toga_winforms/widgets/multilinetextinput.py @@ -1,9 +1,9 @@ import System.Windows.Forms as WinForms from System.Drawing import SystemColors +from travertino.size import at_least from toga_winforms.colors import native_color from toga_winforms.libs.fonts import HorizontalTextAlignment -from travertino.size import at_least from ..libs.wrapper import WeakrefCallable from .textinput import TextInput diff --git a/winforms/src/toga_winforms/widgets/scrollcontainer.py b/winforms/src/toga_winforms/widgets/scrollcontainer.py index ff022b2c4c..415a149484 100644 --- a/winforms/src/toga_winforms/widgets/scrollcontainer.py +++ b/winforms/src/toga_winforms/widgets/scrollcontainer.py @@ -2,9 +2,9 @@ from System.Drawing import Point from System.Windows.Forms import Panel, SystemInformation +from travertino.node import Node from toga_winforms.container import Container -from travertino.node import Node from ..libs.wrapper import WeakrefCallable from .base import Widget diff --git a/winforms/src/toga_winforms/widgets/switch.py b/winforms/src/toga_winforms/widgets/switch.py index 4a56861827..16792bb01e 100644 --- a/winforms/src/toga_winforms/widgets/switch.py +++ b/winforms/src/toga_winforms/widgets/switch.py @@ -1,7 +1,6 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms - from travertino.size import at_least from ..libs.wrapper import WeakrefCallable From e652c07b9650292e74a447b9f4854701a0878351 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sat, 18 Jan 2025 01:42:17 -0500 Subject: [PATCH 21/53] Make sure style mixin works for property aliases too --- .pre-commit-config.yaml | 5 +++++ core/src/toga/style/mixin.py | 4 ++-- core/tests/style/test_mixin.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c35492180..55fe8b19cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,9 +17,14 @@ repos: - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: + # isort for Travertino - id: isort args: [--settings-path=travertino] + # For some reason, providing "travertino" as an argument doesn't work to specify + # the target, like it would on the command line; it still runs against the + # whole repo. Setting it here seems to work, though. files: travertino + # isort for the rest of the repo - id: isort - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.10.0 diff --git a/core/src/toga/style/mixin.py b/core/src/toga/style/mixin.py index 7618d59b66..be95467645 100644 --- a/core/src/toga/style/mixin.py +++ b/core/src/toga/style/mixin.py @@ -1,4 +1,4 @@ -from travertino.declaration import validated_property +from travertino.declaration import directional_property, validated_property class StyleProperty: @@ -26,7 +26,7 @@ def style_mixin(style_cls): for name in dir(style_cls): if not name.startswith("_") and isinstance( - getattr(style_cls, name), validated_property + getattr(style_cls, name), (validated_property, directional_property) ): mixin_dict[name] = StyleProperty() diff --git a/core/tests/style/test_mixin.py b/core/tests/style/test_mixin.py index d5861efb39..2a5bbee541 100644 --- a/core/tests/style/test_mixin.py +++ b/core/tests/style/test_mixin.py @@ -1,3 +1,4 @@ +import pytest from pytest import raises from toga.style import Pack @@ -63,7 +64,12 @@ def test_attribute(): widget.my_attr -def test_class_attribute(): +@pytest.mark.parametrize( + "prop_name", + # Make sure it works for both a plain property and a directional alias. + ["flex", "margin"], +) +def test_class_attribute(prop_name): """Getting a style attribute from the class should return a property object.""" - prop = ExampleWidget.flex + prop = getattr(ExampleWidget, prop_name) assert type(prop).__name__ == "StyleProperty" From a73b150e9ebf79bbbdf1e1cfd0d72f15f939e3f3 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sat, 18 Jan 2025 15:38:39 -0500 Subject: [PATCH 22/53] Fix rcfile for coverage-trav --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 69d3fe6818..ec523383f1 100644 --- a/tox.ini +++ b/tox.ini @@ -66,13 +66,14 @@ setenv = fail: REPORT_FAIL_COND = --fail-under=100 # Even when run with "fail", cut Travertino some slack. trav: REPORT_FAIL_COND = - CORE_RCFILE = --rcfile {tox_root}{/}core{/}pyproject.toml + !trav: PACKAGE_RCFILE = --rcfile {tox_root}{/}core{/}pyproject.toml + trav: PACKAGE_RCFILE = --rcfile {tox_root}{/}travertino{/}pyproject.toml PROJECT_RCFILE = --rcfile {tox_root}{/}pyproject.toml # disable conditional coverage exclusions for Python version {platform}: COVERAGE_EXCLUDE_PYTHON_VERSION=disable commands_pre = python --version commands = - -python -m coverage combine {env:CORE_RCFILE} {env:COMBINE_KEEP} + -python -m coverage combine {env:PACKAGE_RCFILE} {env:COMBINE_KEEP} html: python -m coverage html {env:PROJECT_RCFILE} --skip-covered --skip-empty python -m coverage report {env:PROJECT_RCFILE} {env:REPORT_FAIL_COND} From bd37dec37c5e57688701c59a15da8b464110330c Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sat, 18 Jan 2025 16:50:25 -0500 Subject: [PATCH 23/53] Add dev extras to Travertino and use as tox project root in CI --- .github/workflows/ci.yml | 4 ++-- travertino/pyproject.toml | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd14417f70..db70130a88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: with: requirements: tox extra: dev - project-root: core + project-root: ${{ matrix.package }} - name: Get Packages uses: actions/download-artifact@v4.1.8 @@ -220,7 +220,7 @@ jobs: with: requirements: tox extra: dev - project-root: core + project-root: ${{ matrix.package }} - name: Retrieve Coverage Data uses: actions/download-artifact@v4.1.8 diff --git a/travertino/pyproject.toml b/travertino/pyproject.toml index 29d85c1a52..8c9561ba9a 100644 --- a/travertino/pyproject.toml +++ b/travertino/pyproject.toml @@ -40,6 +40,22 @@ classifiers = [ "Topic :: Software Development :: User Interfaces", ] +[project.optional-dependencies] +# Extras used by developers *of* Toga are pinned to specific versions to +# ensure environment consistency. +dev = [ + "coverage[toml] == 7.6.10", + "coverage-conditional-plugin == 0.9.0", + "pre-commit == 4.0.1", + "pytest == 8.3.4", + "pytest-asyncio == 0.25.2", + "pytest-freezer == 0.4.9", + "setuptools-scm == 8.1.0", + "tox == 4.23.2", + # typing-extensions needed for TypeAlias added in Py 3.10 + "typing-extensions == 4.12.2 ; python_version < '3.10'", +] + [project.urls] Homepage = "https://beeware.org/travertino" Funding = "https://beeware.org/contributing/membership/" From 51ebc6a9dbc9b7eb51a76245c0a3628ed7fe3a6f Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sat, 18 Jan 2025 16:58:16 -0500 Subject: [PATCH 24/53] put env name parts in right order --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db70130a88..aa2cffc796 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -199,9 +199,9 @@ jobs: package: ["core", "travertino"] include: - package: "core" - env-suffix: "" + travertino: "" - package: "travertino" - env-suffix: "-trav" + travertino: "-trav" steps: - name: Checkout uses: actions/checkout@v4.2.2 @@ -231,7 +231,7 @@ jobs: - name: Generate Coverage Report # Even with "fail" on, Travertino will accept <100%. - run: tox -e coverage-html-fail-platform${{ matrix.env-suffix }} + run: tox -e coverage${{ matrix.travertino }}-html-fail-platform - name: Upload HTML Coverage Report uses: actions/upload-artifact@v4.6.0 From 807c9bc3eb5388a6b609253b8e095df890f0b540 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sat, 18 Jan 2025 17:47:36 -0500 Subject: [PATCH 25/53] consistent env var names, updated Travertino URLs --- .github/workflows/ci.yml | 8 ++++---- travertino/README.rst | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa2cffc796..39860e7813 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,9 +82,9 @@ jobs: include: - experimental: false - package: "core" - env-suffix: "" + travertino: "" - package: "travertino" - env-suffix: "-trav" + travertino: "-trav" steps: - name: Checkout @@ -117,8 +117,8 @@ jobs: # The $(ls ...) shell expansion is done in the Github environment; # the value of TOGA_INSTALL_COMMAND will be a literal string without any shell expansions to perform TOGA_INSTALL_COMMAND="python -m pip install ../$(ls dist/toga_core-*.whl)[dev] ../$(ls dist/toga_dummy-*.whl) ../$(ls dist/travertino-*.whl)" - tox -e py-cov${{ matrix.env-suffix }} - tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}")${{ matrix.env-suffix }} + tox -e py-cov${{ matrix.travertino }} + tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}")${{ matrix.travertino }} mv ${{ matrix.package }}/.coverage ${{ matrix.package }}/.coverage.${{ matrix.platform }}.${{ matrix.python-version }} - name: Store Coverage Data diff --git a/travertino/README.rst b/travertino/README.rst index 6187b7b8f3..ca1866412f 100644 --- a/travertino/README.rst +++ b/travertino/README.rst @@ -91,8 +91,7 @@ Although that document is for a different project, the details about setting up your development environment are the same. .. _BeeWare suite: https://beeware.org -.. _Read The Docs: https://travertino.readthedocs.io .. _BeeWare Community Code of Conduct: https://beeware.org/community/behavior/ -.. _log them on Github: https://github.com/beeware/travertino/issues -.. _fork the code: https://github.com/beeware/travertino -.. _submit a pull request: https://github.com/beeware/travertino/pulls +.. _log them on Github: https://github.com/beeware/toga/issues +.. _fork the code: https://github.com/beeware/toga +.. _submit a pull request: https://github.com/beeware/toga/pulls From 4afdc859c23c5c2617cf800a9faf67870a33b8b5 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 19 Jan 2025 22:39:15 -0500 Subject: [PATCH 26/53] Fix layout() signature in gtk container.py --- gtk/src/toga_gtk/container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gtk/src/toga_gtk/container.py b/gtk/src/toga_gtk/container.py index 37ab26dbda..564a9434de 100644 --- a/gtk/src/toga_gtk/container.py +++ b/gtk/src/toga_gtk/container.py @@ -103,7 +103,7 @@ def recompute(self): widget.rehint() # Recompute the layout - self._content.interface.style.layout(self._content.interface, self) + self._content.interface.style.layout(self) self.min_width = self._content.interface.layout.min_width self.min_height = self._content.interface.layout.min_height @@ -173,7 +173,7 @@ def do_size_allocate(self, allocation): # Re-evaluate the layout using the allocation size as the basis # for geometry # print("REFRESH LAYOUT", allocation.width, allocation.height) - self._content.interface.style.layout(self._content.interface, self) + self._content.interface.style.layout(self) # Ensure the minimum content size from the layout is retained self.min_width = self._content.interface.layout.min_width From 925774911476995550a1eafcb8fdb6ec45d1725c Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 19 Jan 2025 22:52:38 -0500 Subject: [PATCH 27/53] Only test Travertino on one platform --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39860e7813..d989bbce47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,11 @@ jobs: platform: [ "macos-latest", "ubuntu-latest", "windows-latest" ] python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] package: ["core", "travertino"] + exclude: + - package: travertino + platform: macos-latest + - package: travertino + platform: ubuntu-latest include: - experimental: false - package: "core" From e62e1cdc271a0dcefa8888c726afbeecd6bd7461 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 19 Jan 2025 23:21:10 -0500 Subject: [PATCH 28/53] Fix more links in readme --- travertino/README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/travertino/README.rst b/travertino/README.rst index ca1866412f..fcf086d8ea 100644 --- a/travertino/README.rst +++ b/travertino/README.rst @@ -15,11 +15,11 @@ :alt: Project status .. |license| image:: https://img.shields.io/pypi/l/travertino.svg - :target: https://github.com/beeware/travertino/blob/main/LICENSE + :target: https://github.com/beeware/toga/blob/main/travertino/LICENSE :alt: BSD License -.. |ci| image:: https://github.com/beeware/travertino/workflows/CI/badge.svg?branch=main - :target: https://github.com/beeware/travertino/actions +.. |ci| image:: https://github.com/beeware/toga/workflows/CI/badge.svg?branch=main + :target: https://github.com/beeware/toga/actions :alt: Build Status .. |social| image:: https://img.shields.io/discord/836455665257021440?label=Discord%20Chat&logo=discord&style=plastic From 26b529e0ba7c01fd13cec2e6afca9200916b8f26 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 19 Jan 2025 23:40:59 -0500 Subject: [PATCH 29/53] Remove placeholder future Pack properties in old format --- core/src/toga/style/pack.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index e801e87cda..9fa7dd4c84 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -140,17 +140,12 @@ class IntrinsicSize(BaseIntrinsicSize): ) font_family: str = validated_property(choices=FONT_FAMILY_CHOICES, initial=SYSTEM) - # Pack.list_property('font_family', choices=FONT_FAMILY_CHOICES) font_style: str = validated_property(choices=FONT_STYLE_CHOICES, initial=NORMAL) font_variant: str = validated_property(choices=FONT_VARIANT_CHOICES, initial=NORMAL) font_weight: str = validated_property(choices=FONT_WEIGHT_CHOICES, initial=NORMAL) font_size: int = validated_property( choices=FONT_SIZE_CHOICES, initial=SYSTEM_DEFAULT_FONT_SIZE ) - # Pack.composite_property([ - # 'font_family', 'font_style', 'font_variant', 'font_weight', 'font_size' - # FONT_CHOICES - # ]) @classmethod def _debug(cls, *args: str) -> None: # pragma: no cover From 61d5a19e526399295843296127f0acd88df89a37 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Mon, 20 Jan 2025 22:21:24 -0500 Subject: [PATCH 30/53] remove more backwards compat; switch Travertino test to Ubuntu --- .github/workflows/ci.yml | 2 +- core/src/toga/style/pack.py | 20 ++------------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d989bbce47..74c10e408a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: - package: travertino platform: macos-latest - package: travertino - platform: ubuntu-latest + platform: windows-latest include: - experimental: false - package: "core" diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 9fa7dd4c84..6ae23fe8f6 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -12,6 +12,7 @@ CENTER, COLUMN, CURSIVE, + END, FANTASY, HIDDEN, ITALIC, @@ -28,6 +29,7 @@ SANS_SERIF, SERIF, SMALL_CAPS, + START, SYSTEM, TOP, TRANSPARENT, @@ -64,10 +66,6 @@ # Declaration choices ###################################################################### -# Define here, since they're not available in Travertino 0.3.0 -START = "start" -END = "end" - # Used in backwards compatibility section below ALIGNMENT = "alignment" ALIGN_ITEMS = "align_items" @@ -268,26 +266,12 @@ def __delattr__(self, name): # Index notation def __getitem__(self, name): - # As long as we're mucking about with backwards compatibility: Travertino 0.3.0 - # doesn't support accessing directional properties via bracket notation, so - # special-case it here to gain access to the FUTURE. - if name in {"padding", "margin"}: - return getattr(self, name) - return super().__getitem__(self._update_property_name(name.replace("-", "_"))) def __setitem__(self, name, value): - if name in {"padding", "margin"}: - setattr(self, name, value) - return - super().__setitem__(self._update_property_name(name.replace("-", "_")), value) def __delitem__(self, name): - if name in {"padding", "margin"}: - delattr(self, name) - return - super().__delitem__(self._update_property_name(name.replace("-", "_"))) ###################################################################### From 5d260e5b1124ed5a74c11ca52a9cf5ea27976c85 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Tue, 21 Jan 2025 19:47:33 -0500 Subject: [PATCH 31/53] Remove subclass_init to fix MicroPython --- core/src/toga/style/mixin.py | 2 +- core/src/toga/style/pack.py | 2 +- travertino/src/travertino/declaration.py | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/src/toga/style/mixin.py b/core/src/toga/style/mixin.py index 8713dc6391..bd755dc4d9 100644 --- a/core/src/toga/style/mixin.py +++ b/core/src/toga/style/mixin.py @@ -22,7 +22,7 @@ def style_mixin(style_cls): the widget. For example, instead of ``widget.style.color``, you can simply write ``widget.color``. """, - **{name: StyleProperty() for name in style_cls._ALL_PROPERTIES}, + **{name: StyleProperty() for name in style_cls._BASE_ALL_PROPERTIES[style_cls]}, } return type(style_cls.__name__ + "Mixin", (), mixin_dict) diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index ad70aa7f35..3fa5848e1e 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -980,4 +980,4 @@ def __css__(self) -> str: return " ".join(css) -Pack._ALL_PROPERTIES.update(Pack._ALIASES) +Pack._BASE_ALL_PROPERTIES[Pack].update(Pack._ALIASES) diff --git a/travertino/src/travertino/declaration.py b/travertino/src/travertino/declaration.py index 9e5b3f460a..e6c115d641 100644 --- a/travertino/src/travertino/declaration.py +++ b/travertino/src/travertino/declaration.py @@ -280,10 +280,15 @@ class BaseStyle: _BASE_PROPERTIES = defaultdict(set) _BASE_ALL_PROPERTIES = defaultdict(set) - def __init_subclass__(cls): - # Give the subclass a direct reference to its properties. - cls._PROPERTIES = cls._BASE_PROPERTIES[cls] - cls._ALL_PROPERTIES = cls._BASE_ALL_PROPERTIES[cls] + # Give instances a direct reference to their properties. + + @property + def _PROPERTIES(self): + return self._BASE_PROPERTIES[type(self)] + + @property + def _ALL_PROPERTIES(self): + return self._BASE_ALL_PROPERTIES[type(self)] # Fallback in case subclass isn't decorated as subclass (probably from using # previous API) or for pre-3.10, before kw_only argument existed. From 12f61db004fdd57ad0568aece34d7cb02eb08d01 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Tue, 21 Jan 2025 19:56:04 -0500 Subject: [PATCH 32/53] Remove non-MicroPython-compatible syntax --- core/src/toga/style/mixin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/toga/style/mixin.py b/core/src/toga/style/mixin.py index bd755dc4d9..822aa26c3d 100644 --- a/core/src/toga/style/mixin.py +++ b/core/src/toga/style/mixin.py @@ -22,7 +22,9 @@ def style_mixin(style_cls): the widget. For example, instead of ``widget.style.color``, you can simply write ``widget.color``. """, - **{name: StyleProperty() for name in style_cls._BASE_ALL_PROPERTIES[style_cls]}, } + mixin_dict.update( + {name: StyleProperty() for name in style_cls._BASE_ALL_PROPERTIES[style_cls]} + ) return type(style_cls.__name__ + "Mixin", (), mixin_dict) From e46124ba4aa7c1b5bd348a7b7ec0fc21217dff63 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 10:35:19 -0500 Subject: [PATCH 33/53] Change matrix variable name Co-authored-by: Russell Keith-Magee --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74c10e408a..aafa830074 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,9 +87,9 @@ jobs: include: - experimental: false - package: "core" - travertino: "" + tox-suffix: "" - package: "travertino" - travertino: "-trav" + tox-suffix: "-trav" steps: - name: Checkout From 08786340d3e11d5494d691750f5d66a9c7ca09b2 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 10:36:08 -0500 Subject: [PATCH 34/53] Change matrix variable name Co-authored-by: Russell Keith-Magee --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aafa830074..4761833bc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,8 +122,8 @@ jobs: # The $(ls ...) shell expansion is done in the Github environment; # the value of TOGA_INSTALL_COMMAND will be a literal string without any shell expansions to perform TOGA_INSTALL_COMMAND="python -m pip install ../$(ls dist/toga_core-*.whl)[dev] ../$(ls dist/toga_dummy-*.whl) ../$(ls dist/travertino-*.whl)" - tox -e py-cov${{ matrix.travertino }} - tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}")${{ matrix.travertino }} + tox -e py-cov${{ matrix.tox-suffix }} + tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}")${{ matrix.tox-suffix }} mv ${{ matrix.package }}/.coverage ${{ matrix.package }}/.coverage.${{ matrix.platform }}.${{ matrix.python-version }} - name: Store Coverage Data From f0b609e4aa7c53ddc659558060d3407ac7ffbcf2 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 10:36:45 -0500 Subject: [PATCH 35/53] Change matrix variable name Co-authored-by: Russell Keith-Magee --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4761833bc4..6f00248a35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -204,9 +204,9 @@ jobs: package: ["core", "travertino"] include: - package: "core" - travertino: "" + tox-suffix: "" - package: "travertino" - travertino: "-trav" + tox-suffix: "-trav" steps: - name: Checkout uses: actions/checkout@v4.2.2 From 17ad08693c67b808a94f3141ce53be8bd36e6f95 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 10:37:13 -0500 Subject: [PATCH 36/53] Right folder for version Co-authored-by: Russell Keith-Magee --- travertino/src/travertino/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/travertino/src/travertino/__init__.py b/travertino/src/travertino/__init__.py index ec61d4b3ba..f26dcd5f6c 100644 --- a/travertino/src/travertino/__init__.py +++ b/travertino/src/travertino/__init__.py @@ -5,7 +5,7 @@ # Excluded from coverage because a pure test environment (such as the one # used by tox in CI) won't have setuptools_scm - __version__ = get_version("../..", relative_to=__file__) # pragma: no cover + __version__ = get_version("../../..", relative_to=__file__) # pragma: no cover except (ModuleNotFoundError, LookupError): # If setuptools_scm isn't in the environment, the call to import will fail. # If it *is* in the environment, but the code isn't a git checkout (e.g., From 1933350584311cc1f2a0e0814f774d2cb5f1c1c0 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 10:44:08 -0500 Subject: [PATCH 37/53] Add coverage to envlist Co-authored-by: Russell Keith-Magee --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ec523383f1..7d9259f7f0 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ extend-ignore = E203, [tox] -envlist = py{39,310,311,312,313}-cov,coverage +envlist = py{39,310,311,312,313}-cov{,-trav},coverage labels = test = py-cov{,-trav},coverage{,-trav} test39 = py39-cov{,-trav},coverage39{,-trav} From c3d533eab6f44db5f5553d4a55f40cbc33b268e1 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 10:44:58 -0500 Subject: [PATCH 38/53] Change matrix variable name Co-authored-by: Russell Keith-Magee --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f00248a35..2dabd58127 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -236,7 +236,7 @@ jobs: - name: Generate Coverage Report # Even with "fail" on, Travertino will accept <100%. - run: tox -e coverage${{ matrix.travertino }}-html-fail-platform + run: tox -e coverage${{ matrix.tox-suffix }}-html-fail-platform - name: Upload HTML Coverage Report uses: actions/upload-artifact@v4.6.0 From 52a2738ae6da90963d25aab42d3866932a884c85 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 22:20:14 -0500 Subject: [PATCH 39/53] trim Travertino test deps; editable install for docs-live --- tox.ini | 7 +++---- travertino/pyproject.toml | 9 ++------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tox.ini b/tox.ini index 7d9259f7f0..b4450f5210 100644 --- a/tox.ini +++ b/tox.ini @@ -58,9 +58,8 @@ base_python = coverage312: py312 coverage313: py313 deps = - # Installing core[dev] even when just testing Travertino ensures the same versions - # of testing tools. - {tox_root}{/}core[dev] + !trav: {tox_root}{/}core[dev] + trav: {tox_root}{/}travertino[test] setenv = keep: COMBINE_KEEP = --keep fail: REPORT_FAIL_COND = --fail-under=100 @@ -100,7 +99,7 @@ suicide_timeout = 1 deps = # editable install so docstrings can be updated for 'all' and 'live' -e {tox_root}{/}core[docs] - {tox_root}{/}travertino + -e {tox_root}{/}travertino passenv = # On macOS M1, you need to manually set the location of the PyEnchant # library: diff --git a/travertino/pyproject.toml b/travertino/pyproject.toml index 8c9561ba9a..5cb729ca7a 100644 --- a/travertino/pyproject.toml +++ b/travertino/pyproject.toml @@ -41,17 +41,12 @@ classifiers = [ ] [project.optional-dependencies] -# Extras used by developers *of* Toga are pinned to specific versions to -# ensure environment consistency. -dev = [ +# These are needed in order to run Travertino's test suite. +test = [ "coverage[toml] == 7.6.10", "coverage-conditional-plugin == 0.9.0", - "pre-commit == 4.0.1", "pytest == 8.3.4", - "pytest-asyncio == 0.25.2", "pytest-freezer == 0.4.9", - "setuptools-scm == 8.1.0", - "tox == 4.23.2", # typing-extensions needed for TypeAlias added in Py 3.10 "typing-extensions == 4.12.2 ; python_version < '3.10'", ] From 2347dc28fbe6ee0b96025647a882683e1d4333e7 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 22:41:51 -0500 Subject: [PATCH 40/53] Add Travertino changelog, including 0.5.0 --- travertino/CHANGELOG.rst | 132 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 travertino/CHANGELOG.rst diff --git a/travertino/CHANGELOG.rst b/travertino/CHANGELOG.rst new file mode 100644 index 0000000000..ba32598af6 --- /dev/null +++ b/travertino/CHANGELOG.rst @@ -0,0 +1,132 @@ +Changelog +========= + +Note: As of version 0.5.0, Travertino is now hosted and developed as part of the Toga repository. It's now released along with — and has the same version number as — each [Toga release](https://github.com/beeware/toga/releases). + +For all development beyond 0.5.0, any changes made to Travertino will be logged along with Toga's overall list of changes for each new release. + +.. towncrier release notes start + +0.5.0 (2025-01-22) +============================================== + +Features +-------- + +* Validated properties of styles can now be defined as dataclass class attributes. (`#141 `_) +* BaseStyle now supports |, |=, and 'in' operators. (`#143 `_) +* Added a ``list_property`` for storing multi-valued elements. (`#148 `_) +* Support for Python 3.13 was added. (`#149 `_) +* Support for Python 3.14 was added. (`#223 `_) +* Constants now include START and END. (`#241 `_) + + +Bugfixes +-------- + +* Assigning a new style object to a node that already has an applicator assigned now properly maintains an association between the applicator and the new style, and triggers a style reapplication. (`#224 `_) +* Fixed a bug which caused an equality check between a Font object and a non-Font to throw an exception instead of returning False. (`#233 `_) + + +Backward Incompatible Changes +----------------------------- + +* The 'default' parameter for Choice has been deprecated. (`#139 `_) +* Python 3.8 is no longer supported. (`#223 `_) +* The mechanisms for assigning styles and applicators to nodes, and applying styles, have been reworked. A node will now attempt to apply its style as soon as it is assigned an applicator; this means you should not assign an applicator to a node until the node is sufficiently initialized to apply its style. To accommodate uses that currently do not follow this order, any exceptions resulting from a failed style application are caught, and a runtime warning is issued. In a future version, this will be an exception. (`#224 `_) +* Supplying an applicator to BaseStyle.copy() has been deprecated. If you need to manually assign an applicator to a style, do it separately, after the copy. (`#224 `_) +* The API for ``Style.layout()`` has been formally specified as part of the Travertino API. The initial ``node`` argument is no longer required as part of the ``layout()`` method. A ``Style`` instance can interrogate ``self._applicator.node`` to retrieve the node to which the style is being applied. (`#244 `_) + + +Documentation +------------- + +* The README badges were updated to display correctly on GitHub. (`#170 `_) + + +Misc +---- + +* `#88 `_, `#89 `_, `#90 `_, `#91 `_, `#92 `_, `#93 `_, `#94 `_, `#95 `_, `#96 `_, `#97 `_, `#98 `_, `#99 `_, `#100 `_, `#101 `_, `#102 `_, `#103 `_, `#104 `_, `#105 `_, `#106 `_, `#107 `_, `#108 `_, `#109 `_, `#110 `_, `#111 `_, `#112 `_, `#113 `_, `#114 `_, `#115 `_, `#116 `_, `#117 `_, `#118 `_, `#120 `_, `#121 `_, `#122 `_, `#123 `_, `#124 `_, `#125 `_, `#126 `_, `#127 `_, `#128 `_, `#129 `_, `#130 `_, `#131 `_, `#132 `_, `#133 `_, `#134 `_, `#135 `_, `#136 `_, `#137 `_, `#138 `_, `#140 `_, `#142 `_, `#144 `_, `#145 `_, `#146 `_, `#147 `_, `#150 `_, `#151 `_, `#152 `_, `#154 `_, `#155 `_, `#156 `_, `#157 `_, `#158 `_, `#159 `_, `#160 `_, `#161 `_, `#162 `_, `#163 `_, `#164 `_, `#165 `_, `#166 `_, `#167 `_, `#168 `_, `#169 `_, `#171 `_, `#172 `_, `#173 `_, `#174 `_, `#175 `_, `#176 `_, `#177 `_, `#178 `_, `#179 `_, `#180 `_, `#181 `_, `#182 `_, `#183 `_, `#184 `_, `#185 `_, `#186 `_, `#187 `_, `#188 `_, `#189 `_, `#190 `_, `#191 `_, `#192 `_, `#193 `_, `#194 `_, `#195 `_, `#196 `_, `#197 `_, `#199 `_, `#200 `_, `#202 `_, `#204 `_, `#205 `_, `#206 `_, `#207 `_, `#208 `_, `#209 `_, `#210 `_, `#211 `_, `#212 `_, `#213 `_, `#214 `_, `#215 `_, `#216 `_, `#217 `_, `#218 `_, `#219 `_, `#220 `_, `#221 `_, `#224 `_, `#225 `_, `#226 `_, `#227 `_, `#228 `_, `#229 `_, `#230 `_, `#231 `_, `#232 `_, `#234 `_, `#235 `_, `#236 `_, `#237 `_, `#238 `_, `#239 `_, `#240 `_, `#242 `_, `#245 `_, `#247 `_, `#248 `_ + + +0.3.0 (2023-08-16) +================== + +Features +-------- + +* Layout nodes can now track the minimum permitted layout size in addition to the current actual layout size. (`#78 `_) + + +Backward Incompatible Changes +----------------------------- + +* Support for Python 3.7 was removed. (`#80 `_) + + +Misc +---- + +* `#44 `_, `#45 `_, `#46 `_, `#47 `_, `#48 `_, `#49 `_, `#50 `_, `#51 `_, `#52 `_, `#53 `_, `#54 `_, `#55 `_, `#56 `_, `#57 `_, `#58 `_, `#59 `_, `#60 `_, `#61 `_, `#62 `_, `#63 `_, `#65 `_, `#66 `_, `#67 `_, `#72 `_, `#73 `_, `#74 `_, `#75 `_, `#76 `_, `#77 `_, `#79 `_, `#81 `_, `#82 `_, `#83 `_, `#84 `_, `#85 `_, `#86 `_, `#87 `_ + + +0.2.0 (2023-03-24) +================== + +Features +-------- + +* Node now supports the ``clear`` method in order to clear all children. (`#23 `_) +* Constants for absolute and relative font sizing were added. (`#43 `_) + + +Bugfixes +-------- + +* Handling of ``none`` as a property value has been corrected. (`#3 `_) + + +Improved Documentation +---------------------- + +* Details on towncrier and pre-commit ussage were added to the README. (`#18 `_) + + +Misc +---- + +* `#22 `_, `#24 `_, `#25 `_, `#26 `_, `#30 `_, `#34 `_, `#35 `_, `#36 `_, `#37 `_, `#38 `_, `#39 `_, `#40 `_, `#41 `_, `#42 `_ + + +0.1.3 (2020-05-25) +------------------ + +Features +^^^^^^^^ + +* Introduced some constants used by Pack that have more general uses. (`#5 `_) +* Added the ability to add, insert and remove children from a node tree. (`#10 `_) +* Added color validation in rgba and hsla constructors (`#17 `_) +* Added support for declaring a system default font size. (`#19 `_) + +Misc +^^^^ + +* `#15 `_, `#16 `_ + + +0.1.2 +----- +* Added constants for system and message fonts +* Added hash method to fonts and colors + +0.1.1 +----- + +* Added font definitions + +0.1.0 +----- + +Initial release. From 4c65c7df3bd7365ab1e1deaceb72c62ecbd3c5df Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 22:44:08 -0500 Subject: [PATCH 41/53] re-add tox --- travertino/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/travertino/pyproject.toml b/travertino/pyproject.toml index 5cb729ca7a..e2caf5eb6e 100644 --- a/travertino/pyproject.toml +++ b/travertino/pyproject.toml @@ -47,6 +47,7 @@ test = [ "coverage-conditional-plugin == 0.9.0", "pytest == 8.3.4", "pytest-freezer == 0.4.9", + "tox == 4.23.2", # typing-extensions needed for TypeAlias added in Py 3.10 "typing-extensions == 4.12.2 ; python_version < '3.10'", ] From 54f076359375e73829115a6ef7e684d1cb45c8b4 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 22:50:16 -0500 Subject: [PATCH 42/53] Change name back to dev, makes things simpler --- tox.ini | 2 +- travertino/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b4450f5210..48fe930421 100644 --- a/tox.ini +++ b/tox.ini @@ -59,7 +59,7 @@ base_python = coverage313: py313 deps = !trav: {tox_root}{/}core[dev] - trav: {tox_root}{/}travertino[test] + trav: {tox_root}{/}travertino[dev] setenv = keep: COMBINE_KEEP = --keep fail: REPORT_FAIL_COND = --fail-under=100 diff --git a/travertino/pyproject.toml b/travertino/pyproject.toml index e2caf5eb6e..ccf0e49a71 100644 --- a/travertino/pyproject.toml +++ b/travertino/pyproject.toml @@ -42,7 +42,7 @@ classifiers = [ [project.optional-dependencies] # These are needed in order to run Travertino's test suite. -test = [ +dev = [ "coverage[toml] == 7.6.10", "coverage-conditional-plugin == 0.9.0", "pytest == 8.3.4", From 8fe41d255ecb1e48c53b34054b54e58f53eb150b Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 23:03:25 -0500 Subject: [PATCH 43/53] Fixed markdown link to RST --- travertino/CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/travertino/CHANGELOG.rst b/travertino/CHANGELOG.rst index ba32598af6..044613957a 100644 --- a/travertino/CHANGELOG.rst +++ b/travertino/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -Note: As of version 0.5.0, Travertino is now hosted and developed as part of the Toga repository. It's now released along with — and has the same version number as — each [Toga release](https://github.com/beeware/toga/releases). +Note: As of version 0.5.0, Travertino is now hosted and developed as part of the Toga repository. It's now released along with — and has the same version number as — each `Toga release `_. For all development beyond 0.5.0, any changes made to Travertino will be logged along with Toga's overall list of changes for each new release. From 986f99dd5167eb788747482d608dd91e3c39593a Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 23:05:37 -0500 Subject: [PATCH 44/53] Escape | in RST --- travertino/CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/travertino/CHANGELOG.rst b/travertino/CHANGELOG.rst index 044613957a..ca7eddd222 100644 --- a/travertino/CHANGELOG.rst +++ b/travertino/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -Note: As of version 0.5.0, Travertino is now hosted and developed as part of the Toga repository. It's now released along with — and has the same version number as — each `Toga release `_. +**Note:** As of version 0.5.0, Travertino is now hosted and developed as part of the Toga repository. It's now released along with — and has the same version number as — each `Toga release `_. For all development beyond 0.5.0, any changes made to Travertino will be logged along with Toga's overall list of changes for each new release. @@ -14,7 +14,7 @@ Features -------- * Validated properties of styles can now be defined as dataclass class attributes. (`#141 `_) -* BaseStyle now supports |, |=, and 'in' operators. (`#143 `_) +* BaseStyle now supports \|, \|=, and 'in' operators. (`#143 `_) * Added a ``list_property`` for storing multi-valued elements. (`#148 `_) * Support for Python 3.13 was added. (`#149 `_) * Support for Python 3.14 was added. (`#223 `_) From 618d50c0499c3cfed795204305274f7b04a96e41 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 23:09:33 -0500 Subject: [PATCH 45/53] Standardize RST formatting --- travertino/CHANGELOG.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/travertino/CHANGELOG.rst b/travertino/CHANGELOG.rst index ca7eddd222..f507686976 100644 --- a/travertino/CHANGELOG.rst +++ b/travertino/CHANGELOG.rst @@ -14,7 +14,7 @@ Features -------- * Validated properties of styles can now be defined as dataclass class attributes. (`#141 `_) -* BaseStyle now supports \|, \|=, and 'in' operators. (`#143 `_) +* BaseStyle now supports ``|``, ``|=``, and ``in`` operators. (`#143 `_) * Added a ``list_property`` for storing multi-valued elements. (`#148 `_) * Support for Python 3.13 was added. (`#149 `_) * Support for Python 3.14 was added. (`#223 `_) @@ -100,10 +100,10 @@ Misc 0.1.3 (2020-05-25) ------------------- +============================================== Features -^^^^^^^^ +-------- * Introduced some constants used by Pack that have more general uses. (`#5 `_) * Added the ability to add, insert and remove children from a node tree. (`#10 `_) @@ -111,22 +111,23 @@ Features * Added support for declaring a system default font size. (`#19 `_) Misc -^^^^ +---- * `#15 `_, `#16 `_ 0.1.2 ------ +============================================== + * Added constants for system and message fonts * Added hash method to fonts and colors 0.1.1 ------ +============================================== * Added font definitions 0.1.0 ------ +============================================== Initial release. From 8079fa4b7ce3f755478c1fc740f508dc18f1f228 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 23:53:02 -0500 Subject: [PATCH 46/53] Added to how-to; added test-core and test-trav labels --- docs/how-to/contribute/code.rst | 120 +++++++++++++++++++++++++++----- tox.ini | 2 + 2 files changed, 103 insertions(+), 19 deletions(-) diff --git a/docs/how-to/contribute/code.rst b/docs/how-to/contribute/code.rst index 1026aed7dd..7425eee9aa 100644 --- a/docs/how-to/contribute/code.rst +++ b/docs/how-to/contribute/code.rst @@ -145,21 +145,21 @@ source packages, so we have to manually install each package: .. code-block:: console (venv) $ cd toga - (venv) $ pip install -e "./core[dev]" -e ./dummy -e ./cocoa + (venv) $ pip install -e "./core[dev]" -e ./dummy -e ./cocoa -e ./travertino .. group-tab:: Linux .. code-block:: console (venv) $ cd toga - (venv) $ pip install -e ./core[dev] -e ./dummy -e ./gtk + (venv) $ pip install -e ./core[dev] -e ./dummy -e ./gtk -e ./travertino .. group-tab:: Windows .. code-block:: doscon (venv) C:\...>cd toga - (venv) C:\...>pip install -e ./core[dev] -e ./dummy -e ./winforms + (venv) C:\...>pip install -e ./core[dev] -e ./dummy -e ./winforms -e ./travertino Pre-commit automatically runs during the commit ----------------------------------------------- @@ -488,10 +488,14 @@ app. .. _run-core-test-suite: -Running the core test suite -=========================== +Running the test suites +======================= Toga uses `tox `__ to manage the testing process. + +Testing Core +------------ + To run the core test suite: .. tabs:: @@ -500,19 +504,19 @@ To run the core test suite: .. code-block:: console - (venv) $ tox -m test + (venv) $ tox -m test-core .. group-tab:: Linux .. code-block:: console - (venv) $ tox -m test + (venv) $ tox -m test-core .. group-tab:: Windows .. code-block:: doscon - (venv) C:\...>tox -m test + (venv) C:\...>tox -m test-core You should get some output indicating that tests have been run. You may see ``SKIPPED`` tests, but shouldn't ever get any ``FAIL`` or ``ERROR`` test @@ -547,6 +551,61 @@ This tells us that line 211, and lines 238-240 are not being executed by the tes suite. You'll need to add new tests (or modify an existing test) to restore this coverage. +Testing Travertino +------------------ + +In addition to the core library, the Toga repository also includes Travertino, a package +that defines the lower-level layout mechanisms which core then builds on. Its test suite +can be run just like that of core: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox -m test-trav + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox -m test-trav + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox -m test-trav + +Just as with core, this should report 100% test coverage. + +You can run both the core and Travertino tests with one command: + + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox -m test + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox -m test + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox -m test + +This will run both test suites, and report the two coverage results one after the other. + + Run a subset of tests --------------------- @@ -575,15 +634,38 @@ specific test, using `pytest specifiers (venv) C:\...>tox -e py -- tests/path_to_test_file/test_some_test.py -These test paths are relative to the ``core`` directory. You'll still get a -coverage report when running a part of the test suite - but the coverage results -will only report the lines of code that were executed by the specific tests you -ran. +These test paths are relative to the ``core`` directory. To run a Travertino test +instead, add ``-trav``: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox -e py-trav -- tests/path_to_test_file/test_some_test.py + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox -e py-trav -- tests/path_to_test_file/test_some_test.py + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox -e py-trav -- tests/path_to_test_file/test_some_test.py + + +Either way, you'll still get a coverage report when running a part of the test suite - +but the coverage results will only report the lines of code that were executed by the +specific tests you ran. -Running the test suite for multiple Python versions ---------------------------------------------------- +Running the test suites for multiple Python versions +---------------------------------------------------- -Tox can also run the test suite for all supported version of Python. This +Tox can also run the test suites for all supported version of Python. This requires that each version of Python is available from ``Path``. .. tabs:: @@ -637,10 +719,10 @@ most useful prior to committing and pushing your changes. Running the testbed =================== -The core API tests exercise ``toga-core`` - but what about the backends? To verify -the behavior of the backends, Toga has a testbed app. This app uses the core API -to exercise all the behaviors that the backend APIs need to perform - but uses -an actual platform backend to implement that behavior. +The above test suites exercise ``toga-core`` and ``travertino`` - but what about the +backends? To verify the behavior of the backends, Toga has a testbed app. This app uses +the core API to exercise all the behaviors that the backend APIs need to perform - but +uses an actual platform backend to implement that behavior. To run the testbed app, install `Briefcase `__, and run the app in developer diff --git a/tox.ini b/tox.ini index 48fe930421..a1325706a7 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,8 @@ extend-ignore = envlist = py{39,310,311,312,313}-cov{,-trav},coverage labels = test = py-cov{,-trav},coverage{,-trav} + test-core = py-cov,coverage + test-trav = py-cov-trav,coverage-trav test39 = py39-cov{,-trav},coverage39{,-trav} test310 = py310-cov{,-trav},coverage310{,-trav} test311 = py311-cov{,-trav},coverage311{,-trav} From 4b4536a7633261a6536fef815b585e75eb3ebcea Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Wed, 22 Jan 2025 23:57:42 -0500 Subject: [PATCH 47/53] Added Travertino to spelling list --- docs/spelling_wordlist | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index e9b403ee51..1adafdffda 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -93,6 +93,7 @@ toolkits tooltip Towncrier Tox +Travertino triaged Triaging tvOS From 37251ce46356c10f28fc0b36b03f0469cbd6abc2 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 23 Jan 2025 00:10:54 -0500 Subject: [PATCH 48/53] Add Travertino to dependabot.yml --- .github/dependabot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a917719b4f..181013bdd5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -73,6 +73,14 @@ updates: day: "sunday" time: "20:00" + - package-ecosystem: "pip" + directory: "/travertino" + schedule: + # Check for updates on Sunday, 8PM UTC + interval: "weekly" + day: "sunday" + time: "20:00" + - package-ecosystem: "pip" directory: "/testbed" ignore: From 1ea9df7de8b2a6383eb6c2effb23ad165f7d2c9c Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 23 Jan 2025 00:50:37 -0500 Subject: [PATCH 49/53] Rearrange mixin_dict to avoid creating intermediate dict --- core/src/toga/style/mixin.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/toga/style/mixin.py b/core/src/toga/style/mixin.py index 822aa26c3d..cdd0410d56 100644 --- a/core/src/toga/style/mixin.py +++ b/core/src/toga/style/mixin.py @@ -17,14 +17,14 @@ def __delete__(self, widget): def style_mixin(style_cls): mixin_dict = { - "__doc__": f""" - Allows accessing the {style_cls.__name__} {style_cls._doc_link} directly on - the widget. For example, instead of ``widget.style.color``, you can simply - write ``widget.color``. - """, + name: StyleProperty() for name in style_cls._BASE_ALL_PROPERTIES[style_cls] } - mixin_dict.update( - {name: StyleProperty() for name in style_cls._BASE_ALL_PROPERTIES[style_cls]} + + mixin_dict["__doc__"] = ( + f"""Allows accessing the {style_cls.__name__} {style_cls._doc_link} directly on + the widget. For example, instead of ``widget.style.color``, you can simply write + ``widget.color``. + """ ) return type(style_cls.__name__ + "Mixin", (), mixin_dict) From 858d2a18892e92c1507bcaebca9541f6b208074f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 24 Jan 2025 11:53:05 +0800 Subject: [PATCH 50/53] Promote release note to Feature. --- changes/3086.feature.rst | 3 +++ changes/3086.misc.rst | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changes/3086.feature.rst delete mode 100644 changes/3086.misc.rst diff --git a/changes/3086.feature.rst b/changes/3086.feature.rst new file mode 100644 index 0000000000..2901f8805c --- /dev/null +++ b/changes/3086.feature.rst @@ -0,0 +1,3 @@ +The Travertino library, providing the base classes for Toga's style and box model, is now managed as part of the Toga release process. + +TODO: CORRECT THE RELEASE DATE ON THE TRAVERTINO RELEASE NOTES diff --git a/changes/3086.misc.rst b/changes/3086.misc.rst deleted file mode 100644 index 5a8418326c..0000000000 --- a/changes/3086.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Travertino has moved from its own repository to Toga's and is now released to coincide with each version release of Toga. From 0bee60a48c0dc9c00d882e5e8e51c708cce1a89c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 24 Jan 2025 11:53:31 +0800 Subject: [PATCH 51/53] Minor edits to Travertino release notes. --- travertino/CHANGELOG.rst | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/travertino/CHANGELOG.rst b/travertino/CHANGELOG.rst index f507686976..4d2c88e032 100644 --- a/travertino/CHANGELOG.rst +++ b/travertino/CHANGELOG.rst @@ -1,39 +1,40 @@ Changelog ========= -**Note:** As of version 0.5.0, Travertino is now hosted and developed as part of the Toga repository. It's now released along with — and has the same version number as — each `Toga release `_. +**Note:** As of version 0.5.0, Travertino is now hosted and developed as part of the +Toga repository. It's now released along with — and has the same version number as — +each `Toga release `_. -For all development beyond 0.5.0, any changes made to Travertino will be logged along with Toga's overall list of changes for each new release. +For all development beyond 0.5.0, any changes made to Travertino will be logged along +with Toga's overall list of changes for each new release. .. towncrier release notes start -0.5.0 (2025-01-22) -============================================== +0.5.0 (2025-??-??) +================== Features -------- * Validated properties of styles can now be defined as dataclass class attributes. (`#141 `_) * BaseStyle now supports ``|``, ``|=``, and ``in`` operators. (`#143 `_) -* Added a ``list_property`` for storing multi-valued elements. (`#148 `_) +* A ``list_property`` declaration has been added to support storing multi-valueds style elements. (`#148 `_) * Support for Python 3.13 was added. (`#149 `_) * Support for Python 3.14 was added. (`#223 `_) -* Constants now include START and END. (`#241 `_) - +* The constants ``START`` and ``END`` have been added. (`#241 `_) Bugfixes -------- * Assigning a new style object to a node that already has an applicator assigned now properly maintains an association between the applicator and the new style, and triggers a style reapplication. (`#224 `_) -* Fixed a bug which caused an equality check between a Font object and a non-Font to throw an exception instead of returning False. (`#233 `_) - +* Equality checks between a Font object and a non-Font object will now throw an exception instead of returning False. (`#233 `_) Backward Incompatible Changes ----------------------------- -* The 'default' parameter for Choice has been deprecated. (`#139 `_) +* The `default` parameter for Choice has been deprecated. (`#139 `_) * Python 3.8 is no longer supported. (`#223 `_) -* The mechanisms for assigning styles and applicators to nodes, and applying styles, have been reworked. A node will now attempt to apply its style as soon as it is assigned an applicator; this means you should not assign an applicator to a node until the node is sufficiently initialized to apply its style. To accommodate uses that currently do not follow this order, any exceptions resulting from a failed style application are caught, and a runtime warning is issued. In a future version, this will be an exception. (`#224 `_) +* The mechanisms for assigning styles and applicators to nodes, and applying styles, have been reworked. A node will now attempt to apply its style as soon as it is assigned an applicator. This means you should not assign an applicator to a node until the node is sufficiently initialized to apply its style. To accommodate uses that currently do not follow this order, any exceptions resulting from a failed style application are caught, and a runtime warning is issued. In a future version, this will be an exception. (`#224 `_) * Supplying an applicator to BaseStyle.copy() has been deprecated. If you need to manually assign an applicator to a style, do it separately, after the copy. (`#224 `_) * The API for ``Style.layout()`` has been formally specified as part of the Travertino API. The initial ``node`` argument is no longer required as part of the ``layout()`` method. A ``Style`` instance can interrogate ``self._applicator.node`` to retrieve the node to which the style is being applied. (`#244 `_) @@ -100,7 +101,7 @@ Misc 0.1.3 (2020-05-25) -============================================== +================== Features -------- @@ -117,17 +118,17 @@ Misc 0.1.2 -============================================== +===== * Added constants for system and message fonts * Added hash method to fonts and colors 0.1.1 -============================================== +===== * Added font definitions 0.1.0 -============================================== +===== Initial release. From 30dd2773d836b980bd8ecdf22761089e2b232d36 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 24 Jan 2025 11:58:25 +0800 Subject: [PATCH 52/53] Minor tweaks to contribution docs. --- docs/how-to/contribute/code.rst | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/how-to/contribute/code.rst b/docs/how-to/contribute/code.rst index 7425eee9aa..1c5e579e16 100644 --- a/docs/how-to/contribute/code.rst +++ b/docs/how-to/contribute/code.rst @@ -488,8 +488,8 @@ app. .. _run-core-test-suite: -Running the test suites -======================= +Running the core test suites +============================ Toga uses `tox `__ to manage the testing process. @@ -555,8 +555,8 @@ Testing Travertino ------------------ In addition to the core library, the Toga repository also includes Travertino, a package -that defines the lower-level layout mechanisms which core then builds on. Its test suite -can be run just like that of core: +that defines the lower-level layout mechanisms and style definitions which core then +builds on. Its test suite can be run just like that of core: .. tabs:: @@ -582,7 +582,6 @@ Just as with core, this should report 100% test coverage. You can run both the core and Travertino tests with one command: - .. tabs:: .. group-tab:: macOS @@ -605,7 +604,6 @@ You can run both the core and Travertino tests with one command: This will run both test suites, and report the two coverage results one after the other. - Run a subset of tests --------------------- @@ -657,7 +655,6 @@ instead, add ``-trav``: (venv) C:\...>tox -e py-trav -- tests/path_to_test_file/test_some_test.py - Either way, you'll still get a coverage report when running a part of the test suite - but the coverage results will only report the lines of code that were executed by the specific tests you ran. From 55988b8cd1f2b0fa3d7a6fa694bd24ac90ceea07 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 24 Jan 2025 12:03:41 +0800 Subject: [PATCH 53/53] Minor tweaks to Travertino project metadata. --- travertino/pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/travertino/pyproject.toml b/travertino/pyproject.toml index ccf0e49a71..fab6da60a6 100644 --- a/travertino/pyproject.toml +++ b/travertino/pyproject.toml @@ -46,7 +46,6 @@ dev = [ "coverage[toml] == 7.6.10", "coverage-conditional-plugin == 0.9.0", "pytest == 8.3.4", - "pytest-freezer == 0.4.9", "tox == 4.23.2", # typing-extensions needed for TypeAlias added in Py 3.10 "typing-extensions == 4.12.2 ; python_version < '3.10'",