-
-
Notifications
You must be signed in to change notification settings - Fork 137
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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)
18436e9
to
1f48945
Compare
Another question: Should the setter of # 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']} |
1f48945
to
fc14114
Compare
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 I could add validation of system fonts by querying for available fonts or I could leave that for a separate PR |
c56254f
to
4f8c4b9
Compare
c1c5556
to
861786b
Compare
861786b
to
bc376e5
Compare
There was a problem hiding this 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.
44d3c2e
to
dfe6e61
Compare
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
Added more tests for all the cases you suggested. |
There was a problem hiding this 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.
@freakboy3742 added the corrections and updted CI to use pytest |
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 ? |
There was a problem hiding this 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.
colosseum/fonts.py
Outdated
@staticmethod | ||
def fonts_path(system=False): | ||
"""Return the path for cross platform user fonts.""" | ||
if sys.platform == 'win32': |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
try: | ||
if getattr(self, '_%s' % name, None): | ||
non_default.append((name, getattr(self, name))) | ||
except AttributeError: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
# 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: |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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
|
||
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, sounds good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough :-)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
I think
Not sure, your call :-)
Will look into pytest-xdist
Will do! |
There was a problem hiding this 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 :-)
colosseum/declaration.py
Outdated
|
||
def setter(self, value): | ||
try: | ||
shorthand = storage_class(**parser(value)) |
There was a problem hiding this comment.
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: ...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
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.