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

PR: Add font properties handling #52

Open
wants to merge 35 commits into
base: main
Choose a base branch
from

Conversation

goanpeca
Copy link
Contributor

@goanpeca goanpeca commented Jan 2, 2020

This should add support for font properties and shorthand handling.

I have some questions I left with TODO on the code.

I also created a new file fonts.py, since I think we will need to put more font stuff on the future.

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

A good start here. I've raised a few questions; the biggest is around the handling of the font_family property (which I think is going to need it's own special property declaration to be managed well)

colosseum/constants.py Outdated Show resolved Hide resolved
colosseum/validators.py Outdated Show resolved Hide resolved
colosseum/declaration.py Outdated Show resolved Hide resolved
colosseum/constants.py Show resolved Hide resolved
colosseum/fonts.py Outdated Show resolved Hide resolved
colosseum/fonts.py Outdated Show resolved Hide resolved
colosseum/fonts.py Outdated Show resolved Hide resolved
colosseum/validators.py Outdated Show resolved Hide resolved
colosseum/declaration.py Outdated Show resolved Hide resolved
tests/test_validators.py Outdated Show resolved Hide resolved
@goanpeca goanpeca force-pushed the enh/add-font-properties branch 2 times, most recently from 18436e9 to 1f48945 Compare January 2, 2020 06:23
@goanpeca
Copy link
Contributor Author

goanpeca commented Jan 2, 2020

Another question: Should the setter of font receive a tuple of values where the last item might be a list? or have the setter(self, *args) (does this even work for setters :-p ?) ? Should we have several options?

# String
node.style.font = '12px/10px palatino, serif'


# # Which one should we provide, enforce besides string?

# Implicit tuple
node.style.font = '12px/10px', 'palatino'
node.style.font = 'normal', 'normal', 'normal', '12px/10px', 'palatino'

# Tuple
node.style.font = ('normal', 'normal', 'normal', '12px/10px', 'palatino')

# List
node.style.font = ['normal', 'normal', 'normal', '12px/10px', 'palatino']

# List and string for family
node.style.font = ['normal', 'normal', 'normal', '12px/10px', 'palatino, serif']

# List for all
node.style.font = ['normal', 'normal', 'normal', '12px/10px', ['palatino, serif']]

# Dictionary
node.style.font = {'size': '12px', 'family': 'palatino'}
node.style.font = {'size': '12px', 'family': 'palatino, serif'}
node.style.font = {'size': '12px', 'family': ['palatino', 'serif']}

@goanpeca
Copy link
Contributor Author

goanpeca commented Jan 3, 2020

Hi @freakboy3742 made the fixes as suggested and simplify the parsing (no more regex! yei).

Added a new list validated property and completed tests for font_family and font shorthand. Left a couple of questions as TODO:

I could add validation of system fonts by querying for available fonts or I could leave that for a separate PR

colosseum/constants.py Outdated Show resolved Hide resolved
Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

More good improvements; still some work to be done.

The new font identification stuff is great; however, as I've flagged inline, we proably want to reverse the lookup (i.e., investigate if a font name is valid on request, rather than pre-computing a list of all valid fonts). It also adds some new testing requirements that we need to make sure we've got in CI.

Other than that, the big requirement is more testing. The font parsing in particular is a complex piece; I'd expect to see a successful and failing parse case for each font attribute in each possible position, as well as a comprehensive set of cases for the comma-splitting part of the font name parsing.

.github/workflows/ci.yml Outdated Show resolved Hide resolved
colosseum/constants.py Outdated Show resolved Hide resolved
colosseum/constants.py Outdated Show resolved Hide resolved
colosseum/constants.py Outdated Show resolved Hide resolved
colosseum/constants.py Outdated Show resolved Hide resolved
colosseum/fonts.py Outdated Show resolved Hide resolved
colosseum/fonts.py Outdated Show resolved Hide resolved
tests/test_declaration.py Outdated Show resolved Hide resolved
tests/test_declaration.py Outdated Show resolved Hide resolved
tests/test_declaration.py Outdated Show resolved Hide resolved
colosseum/fonts.py Outdated Show resolved Hide resolved
@goanpeca
Copy link
Contributor Author

The new font identification stuff is great; however, as I've flagged inline, we proably want to reverse the lookup (i.e., investigate if a font name is valid on request, rather than pre-computing a list of all valid fonts). It also adds some new testing requirements that we need to make sure we've got in CI.

I have added an automatic way to add the needed fonts so users do not really need to follow any special instruction and I added a FontDatabase class that checks for fonts on demand and caches them if found so I think this solves your initial concerns

Other than that, the big requirement is more testing. The font parsing in particular is a complex piece; I'd expect to see a successful and failing parse case for each font attribute in each possible position, as well as a comprehensive set of cases for the comma-splitting part of the font name parsing.

Added more tests for all the cases you suggested.

@goanpeca goanpeca mentioned this pull request Jan 11, 2020
Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

Some more good improvement here. A few more comments inline.

colosseum/declaration.py Outdated Show resolved Hide resolved
.github/workflows/ci.yml Show resolved Hide resolved
colosseum/fonts.py Outdated Show resolved Hide resolved
colosseum/fonts.py Outdated Show resolved Hide resolved
colosseum/fonts.py Outdated Show resolved Hide resolved
colosseum/validators.py Outdated Show resolved Hide resolved
colosseum/validators.py Outdated Show resolved Hide resolved
tests/test_parser.py Outdated Show resolved Hide resolved
tests/utils.py Outdated Show resolved Hide resolved
tests/utils.py Outdated Show resolved Hide resolved
@goanpeca
Copy link
Contributor Author

@freakboy3742 added the corrections and updted CI to use pytest

@goanpeca
Copy link
Contributor Author

I gave it a bit of thought and I don't know if it is worth it to use pytest. I could just use permutations instead of generating over a thousand different tests. Instructions become less simple and pytest takes longer to run. Thoughts @freakboy3742 ?

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

More solid improvements; still a bit more to go.

In particular, the FontShorthand handling (and, more generally, the approach we take for "shorthand" properties) needs some work

It strikes me that we've got two different purposes for FontShorthand. There's a storage class that is "a font" (i.e., this is "italic bold 14pt Helvetica") that can be easily mapped to an actual platform font instance; and there's the validation and storage mechanism for the individual configuration points that make up that definition. Asking for "the font" should give you a single object; however, that doesn't necessarily mean that's how it should be stored. This becomes most apparent in the tests you've added for FontShorthand.__repr__ - a font declaration of "normal normal normal..." ... isn't normal. :-)

I'm wondering if the best approach here might be to make the underlying base properties (e.g., font_family) the storage point for each individual property; with the "shorthand" properties able to either compose the underlying properties into a string representation, or generate an "object" representation as needed.

Regarding adopting pytest - my general preference is to have more, finer granularity tests. When you have monolithic tests, seeing a test failure doesn't tell you how widespread the problem actually is. The worst outcome for me is when a test fails, you make a fix, and the same test fails, but three lines later. For me, that means you've got 2 different tests; it's vastly preferable to have 2 failures in that situation. Having 1000 tests isn't a problem in my book. If it's causing a problem with test runtime, I'd be interested to know if the pytest-xdist extension helps.

Also - could I ask that you don't click mark as resolved if it's a question/issue that I've raised? On a long-running ticket like this one, it's difficult for me to tell which pieces of feedback you've addressed and which ones you haven't.

@staticmethod
def fonts_path(system=False):
"""Return the path for cross platform user fonts."""
if sys.platform == 'win32':
Copy link
Member

Choose a reason for hiding this comment

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

The fact that there are multiple instances of this platform check is mildly concerning. It feels like there's an opportunity for a base "FontDatabase" class with platform-specific subclasses, with a single package-level platform check to determine which class actually gets instantiated when you ask for the FontDatabase.

Copy link
Contributor Author

@goanpeca goanpeca Jan 18, 2020

Choose a reason for hiding this comment

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

Fixed

try:
if getattr(self, '_%s' % name, None):
non_default.append((name, getattr(self, name)))
except AttributeError:
Copy link
Member

Choose a reason for hiding this comment

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

Is this exception handler still needed if the getattr on L623 has a default value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm nope, you are correct.

Copy link
Contributor Author

@goanpeca goanpeca Jan 18, 2020

Choose a reason for hiding this comment

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

Fixed

# Shorthand values are constructed on demand since they depend on
# other properties, we have to use getattr(self, name) instead of
# getattr(self, '_%s' % name)
if name in shorthand_properties:
Copy link
Member

Choose a reason for hiding this comment

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

If we need this kind of logic check, then I'd argue the abstraction has failed. L617 should be a relatively simple "iterate over all the properties and if the value isn't the default, store it".

Copy link
Contributor Author

@goanpeca goanpeca Jan 18, 2020

Choose a reason for hiding this comment

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

Fixed


if values != getattr(self, '_%s' % name, list(initial)):
# We use the initial type to make an instance with the set values
setattr(self, '_%s' % name, initial.__class__(values))
Copy link
Member

Choose a reason for hiding this comment

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

I get what you're doing here, but I think it would be better to be explicit - set the "storage class" as an attribute of the property, and then coerce the initial value into that storage class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, sounds good

Copy link
Contributor Author

@goanpeca goanpeca Jan 18, 2020

Choose a reason for hiding this comment

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

Fixed

colosseum/declaration.py Outdated Show resolved Hide resolved
self.assertNotEqual(id(ilist1), id(ilist1.copy()))
self.assertNotEqual(id(ilist2), id(ilist1.copy()))
self.assertEqual(hash(ilist2), hash(ilist1.copy()))
self.assertEqual(ilist1, ilist1.copy())
Copy link
Member

Choose a reason for hiding this comment

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

This is (at least) 8 independent tests, not one tests. You've already got the ImmutableList namespace; might as well use it to differentiate the different properties you're checking.

Copy link
Member

Choose a reason for hiding this comment

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

Same comment for many of the following tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok

Copy link
Contributor Author

@goanpeca goanpeca Jan 18, 2020

Choose a reason for hiding this comment

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

Fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will add more shorthand tests as soon as I have more clarity on the approach

def test_font_shorthand(self):
# Check initial
font = FontShorthand()
self.assertEqual(str(font), 'normal normal normal medium/normal initial')
Copy link
Member

Choose a reason for hiding this comment

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

While this is technically correct, this... isn't a very natural way to describe a "default font". If you're not setting any values, then I'd expect this to fall back to the barest minimum on output - "initial". I'd only expect to see "normal normal normal medium/normal" if those values had been explicitly set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough :-)

Copy link
Contributor Author

@goanpeca goanpeca Jan 18, 2020

Choose a reason for hiding this comment

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

@freakboy3742 I kinda disagree because setting the font shorthand to say just 'initial', will reset all other font properties to their default values so this is like effectively setting those values to their initial values "implicitly"

I am not sure how we could really handle this use case.

I could try to display the minimum values in this str but once this "Font" object is applied to the style of a given node it is effectively resetting all the other properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Still need to think this a bit further

@goanpeca
Copy link
Contributor Author

More solid improvements; still a bit more to go.

In particular, the FontShorthand handling (and, more generally, the approach we take for "shorthand" properties) needs some work

It strikes me that we've got two different purposes for FontShorthand. There's a storage class that is "a font" (i.e., this is "italic bold 14pt Helvetica") that can be easily mapped to an actual platform font instance; and there's the validation and storage mechanism for the individual configuration points that make up that definition. Asking for "the font" should give you a single object; however, that doesn't necessarily mean that's how it should be stored. This becomes most apparent in the tests you've added for FontShorthand.repr - a font declaration of "normal normal normal..." ... isn't normal. :-)

I think FontShorthand and the Font object that should provide whatever font specific properties (em values x height etc...) we might need might be a single object. They could be one and the same I guess.

I'm wondering if the best approach here might be to make the underlying base properties (e.g., font_family) the storage point for each individual property; with the "shorthand" properties able to either compose the underlying properties into a string representation, or generate an "object" representation as needed.

Not sure, your call :-)

Regarding adopting pytest - my general preference is to have more, finer granularity tests. When you have monolithic tests, seeing a test failure doesn't tell you how widespread the problem actually is. The worst outcome for me is when a test fails, you make a fix, and the same test fails, but three lines later. For me, that means you've got 2 different tests; it's vastly preferable to have 2 failures in that situation. Having 1000 tests isn't a problem in my book. If it's causing a problem with test runtime, I'd be interested to know if the pytest-xdist extension helps.

Will look into pytest-xdist

Also - could I ask that you don't click mark as resolved if it's a question/issue that I've raised? On a long-running ticket like this one, it's difficult for me to tell which pieces of feedback you've addressed and which ones you haven't.

Will do!

Copy link
Contributor Author

@goanpeca goanpeca left a comment

Choose a reason for hiding this comment

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

Thanks for the review, indeed the shorthand hadling might need some extra thinking but I think we are progressing in that direction :-)


def setter(self, value):
try:
shorthand = storage_class(**parser(value))
Copy link
Contributor Author

@goanpeca goanpeca Jan 23, 2020

Choose a reason for hiding this comment

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

@freakboy3742 what should be the behavior when setting a font shorthand?

If the user only provides some parts, let's say"

style.font = "12px/3 serif"

Since this shorthand resets the missing properties to their initial values, then should we explicitly write the properties or delete them (which would be akin to setting them to their initial values when the user requests them)

So:

Set properties with missing values

style.font = "12px/3 serif"
print(style)

font: ..
font_size: ...
font_family: ...
font_style: ...
font_variant: ...
font_weight: ...

vs.

Delete properties with missing values

style.font = "12px/3 serif"
print(style)

font: ..
font_size: ...
font_family: ...

Copy link
Member

Choose a reason for hiding this comment

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

I'd say the latter. "normal normal normal normal 12px/3 serif" doesn't communicate anything that "12px/3 serif" doesn't communicate; and I'd say the same is also true for the inverse. If I explicitly ask for "what is the font variant", then the answer is "normal"; but if I just ask for the font, the explicitness of including "normal" for font variant doesn't really aid any understanding.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants