Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Font trait and parser #1042

Merged
merged 8 commits into from
Jan 7, 2022
2 changes: 1 addition & 1 deletion pyface/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ def to_toolkit(self):
return font_to_toolkit_font(self)

def __str__(self):
""" Produce a CSS-style representation of the font. """
""" Produce a CSS2-style representation of the font. """
terms = []
if self.style != 'normal':
terms.append(self.style)
Expand Down
133 changes: 133 additions & 0 deletions pyface/tests/test_ui_traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import os
import unittest

from pyface.util.font_parser import simple_parser

# importlib.resources is new in Python 3.7, and importlib.resources.files is
# new in Python 3.9, so for Python < 3.9 we must rely on the 3rd party
# importlib_resources package.
Expand All @@ -30,6 +32,7 @@
from traits.testing.api import UnittestTools

from ..color import Color
from ..font import Font
from ..image_resource import ImageResource
from ..ui_traits import (
Border,
Expand All @@ -38,6 +41,7 @@
Image,
Margin,
PyfaceColor,
PyfaceFont,
image_resource_cache,
image_bitmap_cache,
)
Expand All @@ -56,6 +60,11 @@ class ColorClass(HasTraits):
color = PyfaceColor()


class FontClass(HasTraits):

font = PyfaceFont()


class HasMarginClass(HasTraits):

margin = HasMargin
Expand Down Expand Up @@ -354,6 +363,130 @@ def test_set_structured_dtype(self):
self.assertEqual(color_class.color, color)


class TestPyfaceFont(unittest.TestCase):

def test_init(self):
trait = PyfaceFont()
self.assertEqual(trait.default_value, (Font, (), {}))
self.assertEqual(
trait.default_value_type,
DefaultValue.callable_and_args,
)

def test_init_empty_string(self):
trait = PyfaceFont("")
self.assertEqual(
trait.default_value,
(
Font,
(),
{
'family': ["default"],
'size': 12.0,
'weight': "normal",
'stretch': 100,
'style': "normal",
'variants': set(),
'decorations': set(),
},
)
)

def test_init_typical_string(self):
trait = PyfaceFont(
"10 pt bold condensed italic underline Helvetica sans-serif")
self.assertEqual(
trait.default_value,
(
Font,
(),
{
'family': ["helvetica", "sans-serif"],
'size': 10.0,
'weight': "bold",
'stretch': 75.0,
'style': "italic",
'variants': set(),
'decorations': {"underline"},
},
)
)

def test_init_font(self):
font = Font(
family=["helvetica", "sans-serif"],
size=10.0,
weight="bold",
stretch=75.0,
style="italic",
variants=set(),
decorations={"underline"},
)
trait = PyfaceFont(font)
self.assertEqual(
trait.default_value,
(
Font,
(),
{
'family': ["helvetica", "sans-serif"],
'size': 10.0,
'weight': "bold",
'stretch': 75.0,
'style': "italic",
'variants': set(),
'decorations': {"underline"},
},
)
)

def test_set_empty_string(self):
font_class = FontClass()
font_class.font = ""
self.assertFontEqual(font_class.font, Font())

def test_set_typical_string(self):
font_class = FontClass()
font_class.font = "10 pt bold condensed italic underline Helvetica sans-serif" # noqa: E501
self.assertFontEqual(
font_class.font,
Font(
family=["helvetica", "sans-serif"],
size=10.0,
weight="bold",
stretch=75.0,
style="italic",
variants=set(),
decorations={"underline"},
),
)

def test_set_font(self):
font_class = FontClass()
font = Font(
family=["helvetica", "sans-serif"],
size=10.0,
weight="bold",
stretch=75.0,
style="italic",
variants=set(),
decorations={"underline"},
)
font_class.font = font
self.assertIs(font_class.font, font)

def test_set_failure(self):
font_class = FontClass()

with self.assertRaises(TraitError):
font_class.font = None

def assertFontEqual(self, font1, font2):
state1 = font1.trait_get(transient=lambda x: not x)
state2 = font2.trait_get(transient=lambda x: not x)
self.assertEqual(state1, state2)


class TestHasMargin(unittest.TestCase, UnittestTools):
def test_defaults(self):
has_margin = HasMarginClass()
Expand Down
3 changes: 0 additions & 3 deletions pyface/ui/wx/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@

import wx

from pyface.font import Font


# font weight and size features changed in wxPython 4.1/wxWidgets 3.1
wx_python_4_1 = (wx.VERSION >= (4, 1))

Expand Down
52 changes: 51 additions & 1 deletion pyface/ui_traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
from traits.trait_base import get_resource_path

from pyface.color import Color
from pyface.font import Font
from pyface.i_image import IImage
from pyface.util.color_parser import ColorParseError
from pyface.util.font_parser import simple_parser, FontParseError


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -144,7 +146,7 @@ def create_editor(self):


class PyfaceColor(TraitType):
""" A Trait which casts strings and tuples to a PyfaceColor value.
""" A Trait which casts strings and tuples to a Pyface Color value.
"""

#: The default value should be a tuple (factory, args, kwargs)
Expand Down Expand Up @@ -186,6 +188,54 @@ def info(self):
)


# -------------------------------------------------------------------------------
# Font
# -------------------------------------------------------------------------------


class PyfaceFont(TraitType):
""" A Trait which casts strings to a Pyface Font value.
"""

#: The default value should be a tuple (factory, args, kwargs)
default_value_type = DefaultValue.callable_and_args

#: The parser to use when converting text to keyword args. This should
#: accept a string and return a dictionary of Font class trait values (ie.
#: "family", "size", "weight", etc.).
parser = None

def __init__(self, value=None, *, parser=simple_parser, **metadata):
if parser is not None:
self.parser = parser
if value is not None:
font = self.validate(None, None, value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks as though this means that a validation error would be turned into a TraitError. Would it make sense to explicitly raise ValueError or TypeError (as appropriate) instead? The error message right now isn't as helpful as it could be:

>>> PyfaceFont(b"times new roman")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/mdickinson/Enthought/ETS/pyface/pyface/ui_traits.py", line 212, in __init__
    font = self.validate(None, None, value)
  File "/Users/mdickinson/Enthought/ETS/pyface/pyface/ui_traits.py", line 231, in validate
    self.error(object, name, value)
  File "/Users/mdickinson/.venvs/pyface/lib/python3.9/site-packages/traits/base_trait_handler.py", line 74, in error
    raise TraitError(
traits.trait_errors.TraitError: None

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be something that's worth looking into in Traits about why the error message isn't good, since calling validate with None object and None trait name is done in a few places. It feels like this code might need an extra branch for that situation: https://github.com/enthought/traits/blob/d22ce1f096e2a6f87c78d7f1bb5bf0abab1a18ff/traits/trait_errors.py#L66-L94

I'll convert this to a ValueError here for now.

default_value = (
Font,
(),
font.trait_get(transient=lambda x: not x),
)
else:
default_value = (Font, (), {})
super().__init__(default_value, **metadata)

def validate(self, object, name, value):
if isinstance(value, Font):
return value
if isinstance(value, str):
try:
return Font(**self.parser(value))
except FontParseError:
self.error(object, name, value)

self.error(object, name, value)

def info(self):
return (
"a Pyface Font, or a string describing a Pyface Font"
)


# -------------------------------------------------------------------------------
# Borders, Margins and Layout
# -------------------------------------------------------------------------------
Expand Down
Loading