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

Add typing to otfautohint #1719

Open
wants to merge 42 commits into
base: develop
Choose a base branch
from

Conversation

simoncozens
Copy link
Collaborator

Description

otfautohint is a complex piece of code, and it's easy to get lost; it's even easier to make mistakes. Having typing information available makes it easier to catch such mistakes, and allows editors with Python type inferencing (Visual Code with pylance, Sublime Text with pyright, etc.) to provide type hints for variables and methods. This in turn helps to find typos, dead code, and so on.

Checklist:

  • I have followed the Contribution Guidelines
  • I have added test code and data to prove that my code functions correctly (N/A)
  • I have verified that new and existing tests pass locally with my changes
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (N/A)

@skef
Copy link
Collaborator

skef commented Oct 18, 2023

As you're probably realizing this has a lot of formatting related problems (flake8), some of which are due to edits for line length that you've removed.

@josh-hadley
Copy link
Collaborator

@simoncozens thanks for this contribution. It looks like there are a number of linter failures that are blocking the tests from running. I recommend that you run flake8 using the config from this repo, fix all reported issues, and resubmit. Once we get the linter + tests green we will have a closer review and see about getting it merged.

@simoncozens
Copy link
Collaborator Author

Yeah, sorry, I was assuming the code formatting was black/PEP8 so my editor runs black on save to keep things tidy. I'll try and put things back into your preferred style.

@skef
Copy link
Collaborator

skef commented Oct 18, 2023

I think I would prefer a submission with the substantive changes and then (perhaps) a different submission with the large amount of stylistic changes. Or perhaps just skip the latter? Best to either mesh one's contributions with the existing style or talk in advance about what you want to change and why.

Edit: Ninja-ed.

@skef
Copy link
Collaborator

skef commented Oct 18, 2023

@josh-hadley Merging this would presumably require additional pytype github checks, so that notations like # pytype: disable=attribute-error won't just rot.

@simoncozens
Copy link
Collaborator Author

To be honest, it doesn't currently work with pytype. I started working with pytype, and as I added annotations I found myself in situations where a method returned an instance of its own class, which needs the Self type, and pytype doesn't currently support that. So I switched to mypy. In a couple of cases mypy doesn't like the Self return value, but it does a better job than pytype (and continues running, whereas pytype just stops and refuses to go on).

@simoncozens
Copy link
Collaborator Author

Coverage tests are sad because apparently you can't use weakref.ReferenceType as a generic in Python 3.8.

Copy link
Collaborator

@skef skef left a comment

Choose a reason for hiding this comment

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

Some preliminary review feedback

from fontTools.pens.basePen import BasePen

import logging

# from .hintstate import glyphHintState
Copy link
Collaborator

Choose a reason for hiding this comment

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

May not need this commented import?

"""Returns True if a and b are not close enough to be considered equal"""
return abs(a - b) >= factor


class pt(tuple):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I did make pt a tuple consciously, so that they are immutable and reusable and for certain other benefits. Can you talk about the reasons for your converting them to standard objects?

@@ -224,14 +238,14 @@ def __mul__(self, other):
Returns a new pt object with this object's coordinates multiplied by
a scalar value
"""
if not isinstance(other, numbers.Number):
if not isinstance(other, (int, float)):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess I'm OK with this, although it goes a bit against the spirit of the interface. In theory, at least, someone might want to call these operations with some other number-like parameter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

numbers.Number includes complex, which I think you should probably exclude.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Fair enough.

@@ -1407,7 +1439,7 @@ def toStems(self, data):
for i in range(len(data) // 2):
low = high + data[i * 2]
high = low + data[i * 2 + 1]
sl.append(stem(low, high))
sl.append(stem(low, high)) # pytype: disable=wrong-arg-count
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the thinking about keeping these relative to pytype not working with this code (at least for now)

python/afdko/otfautohint/otfFont.py Show resolved Hide resolved
python/afdko/otfautohint/hintstate.py Show resolved Hide resolved
python/afdko/otfautohint/hintstate.py Outdated Show resolved Hide resolved
@@ -428,7 +462,7 @@ def handleOverlap(self):
else:
return self.isMulti

def addSegment(self, fr, to, loc, pe1, pe2, typ, desc, mid=False):
def addSegment(self, fr, to, loc, pe1: Optional[pathElement], pe2: Optional[pathElement], typ, desc, mid=False) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This has flake8 problems too, doesn't it?

"""Return point cp3 adjusted relative to cp2 by CPFrac"""
return (cp3 - cp2) * (1.0 - self.CPfrac) + cp2

def CPTo(self, cp0, cp1):
def CPTo(self, cp0, cp1) -> Any:
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the purpose of the Any return type notations? Does it help compared with just leaving them off?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Python typing is progressive, and basically, I haven't finished adding all the annotations. But if you add any type notation, a method gets checked as best it can be, but with no type notation at all you don't get any checking or inferencing.

if seg0.isBBox():
if seg0.isGBBox():
pbs = self.glyph.getBounds(None)
else:
# I don't believe this can be reached, because
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is probably just so obscure a corner case that none of our tests exercise it and no one has hit it yet, so we didn't catch the bug. I'll take a look at some point.

seg0.isBBox() and not seg0.isGBBox() implies that the stems for seg0 were generated from the bounding box of the segment itself because nothing else worked. Only very unusual splines will have that problem.

@skef
Copy link
Collaborator

skef commented Oct 19, 2023

One reason we ported psautohint to python was the hope that it could be modified by the broader community, in comparison to the C code which was only ever worked on by a small group of Adobe-internal folks while squinting hard and crossing their fingers.

This PR is both an external contribution and an effort to make further contributions easier, so in that sense it's what we want and were hoping for.

Still, there is a basic concern about keeping these annotations from rotting. Some users will have environments that will make good use of the typing. Others will just be editing the files and running the tests and may have no idea what this stuff is or, if they do, how to change the annotations as the code changes.

Obviously some sort of verifier that can be run as part of the GitHub checks would be ideal, but it sounds like pytype can't do that and mypy might or might not be able to.

Do you have thoughts on this? Basically, how to maintain types in Python assuming a wide variety of users with different levels of experience and tooling?

Also: Keep in mind that we haven't been doing this internally, so it make take a while just to digest the implications of typed Python on our end.

@anthrotype
Copy link
Member

FWIW annotations are pretty useless unless they are actually checked by a typechecker

@anthrotype
Copy link
Member

ok, well.. self-documentation sure, but they then tend to rot as @skef noted.

@simoncozens
Copy link
Collaborator Author

Obviously some sort of verifier that can be run as part of the GitHub checks would be ideal, but it sounds like pytype can't do that and mypy might or might not be able to.

We can certainly get mypy set up in the CI, and add a disable comment to turn off the Self checks.

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.

4 participants