fonttools 4.59.2__cp39-cp39-musllinux_1_2_aarch64.whl → 4.60.0__cp39-cp39-musllinux_1_2_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fonttools might be problematic. Click here for more details.

Files changed (42) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/annotations.py +30 -0
  3. fontTools/cu2qu/cu2qu.c +1057 -937
  4. fontTools/cu2qu/cu2qu.cpython-39-aarch64-linux-gnu.so +0 -0
  5. fontTools/cu2qu/cu2qu.py +19 -2
  6. fontTools/feaLib/lexer.c +8 -3
  7. fontTools/feaLib/lexer.cpython-39-aarch64-linux-gnu.so +0 -0
  8. fontTools/misc/bezierTools.c +8 -3
  9. fontTools/misc/bezierTools.cpython-39-aarch64-linux-gnu.so +0 -0
  10. fontTools/misc/enumTools.py +23 -0
  11. fontTools/pens/filterPen.py +218 -26
  12. fontTools/pens/momentsPen.c +8 -3
  13. fontTools/pens/momentsPen.cpython-39-aarch64-linux-gnu.so +0 -0
  14. fontTools/pens/pointPen.py +40 -6
  15. fontTools/qu2cu/qu2cu.c +20 -7
  16. fontTools/qu2cu/qu2cu.cpython-39-aarch64-linux-gnu.so +0 -0
  17. fontTools/ttLib/tables/_p_o_s_t.py +5 -5
  18. fontTools/ufoLib/__init__.py +279 -176
  19. fontTools/ufoLib/converters.py +14 -5
  20. fontTools/ufoLib/filenames.py +16 -6
  21. fontTools/ufoLib/glifLib.py +286 -190
  22. fontTools/ufoLib/kerning.py +32 -12
  23. fontTools/ufoLib/utils.py +41 -13
  24. fontTools/ufoLib/validators.py +121 -97
  25. fontTools/varLib/avar/__init__.py +0 -0
  26. fontTools/varLib/avar/__main__.py +72 -0
  27. fontTools/varLib/avar/build.py +79 -0
  28. fontTools/varLib/avar/map.py +108 -0
  29. fontTools/varLib/avar/plan.py +1004 -0
  30. fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
  31. fontTools/varLib/avarPlanner.py +3 -999
  32. fontTools/varLib/interpolatableHelpers.py +3 -0
  33. fontTools/varLib/iup.c +14 -5
  34. fontTools/varLib/iup.cpython-39-aarch64-linux-gnu.so +0 -0
  35. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/METADATA +29 -2
  36. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/RECORD +42 -35
  37. {fonttools-4.59.2.data → fonttools-4.60.0.data}/data/share/man/man1/ttx.1 +0 -0
  38. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/WHEEL +0 -0
  39. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/entry_points.txt +0 -0
  40. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/licenses/LICENSE +0 -0
  41. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/licenses/LICENSE.external +0 -0
  42. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/top_level.txt +0 -0
@@ -32,6 +32,8 @@ Value conversion functions are available for converting
32
32
  - :func:`.convertFontInfoValueForAttributeFromVersion3ToVersion2`
33
33
  """
34
34
 
35
+ from __future__ import annotations
36
+
35
37
  import enum
36
38
  import logging
37
39
  import os
@@ -39,19 +41,47 @@ import zipfile
39
41
  from collections import OrderedDict
40
42
  from copy import deepcopy
41
43
  from os import fsdecode
44
+ from typing import IO, TYPE_CHECKING, Any, Optional, Union, cast
42
45
 
43
46
  from fontTools.misc import filesystem as fs
44
47
  from fontTools.misc import plistlib
45
48
  from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
46
49
  from fontTools.ufoLib.errors import UFOLibError
47
50
  from fontTools.ufoLib.filenames import userNameToFileName
48
- from fontTools.ufoLib.utils import _VersionTupleEnumMixin, numberTypes
51
+ from fontTools.ufoLib.utils import (
52
+ BaseFormatVersion,
53
+ normalizeFormatVersion,
54
+ numberTypes,
55
+ )
49
56
  from fontTools.ufoLib.validators import *
50
57
 
58
+ if TYPE_CHECKING:
59
+ from logging import Logger
60
+
61
+ from fontTools.annotations import (
62
+ GlyphNameToFileNameFunc,
63
+ K,
64
+ KerningDict,
65
+ KerningGroups,
66
+ KerningNested,
67
+ PathOrFS,
68
+ PathStr,
69
+ UFOFormatVersionInput,
70
+ V,
71
+ )
72
+ from fontTools.misc.filesystem._base import FS
73
+ from fontTools.ufoLib.glifLib import GlyphSet
74
+
75
+ KerningGroupRenameMaps = dict[str, dict[str, str]]
76
+ LibDict = dict[str, Any]
77
+ LayerOrderList = Optional[list[Optional[str]]]
78
+ AttributeDataDict = dict[str, Any]
79
+ FontInfoAttributes = dict[str, AttributeDataDict]
80
+
51
81
  # client code can check this to see if the upstream `fs` package is being used
52
82
  haveFS = fs._haveFS
53
83
 
54
- __all__ = [
84
+ __all__: list[str] = [
55
85
  "haveFS",
56
86
  "makeUFOPath",
57
87
  "UFOLibError",
@@ -69,43 +99,37 @@ __all__ = [
69
99
  "convertFontInfoValueForAttributeFromVersion2ToVersion1",
70
100
  ]
71
101
 
72
- __version__ = "3.0.0"
102
+ __version__: str = "3.0.0"
73
103
 
74
104
 
75
- logger = logging.getLogger(__name__)
105
+ logger: Logger = logging.getLogger(__name__)
76
106
 
77
107
 
78
108
  # ---------
79
109
  # Constants
80
110
  # ---------
81
111
 
82
- DEFAULT_GLYPHS_DIRNAME = "glyphs"
83
- DATA_DIRNAME = "data"
84
- IMAGES_DIRNAME = "images"
85
- METAINFO_FILENAME = "metainfo.plist"
86
- FONTINFO_FILENAME = "fontinfo.plist"
87
- LIB_FILENAME = "lib.plist"
88
- GROUPS_FILENAME = "groups.plist"
89
- KERNING_FILENAME = "kerning.plist"
90
- FEATURES_FILENAME = "features.fea"
91
- LAYERCONTENTS_FILENAME = "layercontents.plist"
92
- LAYERINFO_FILENAME = "layerinfo.plist"
112
+ DEFAULT_GLYPHS_DIRNAME: str = "glyphs"
113
+ DATA_DIRNAME: str = "data"
114
+ IMAGES_DIRNAME: str = "images"
115
+ METAINFO_FILENAME: str = "metainfo.plist"
116
+ FONTINFO_FILENAME: str = "fontinfo.plist"
117
+ LIB_FILENAME: str = "lib.plist"
118
+ GROUPS_FILENAME: str = "groups.plist"
119
+ KERNING_FILENAME: str = "kerning.plist"
120
+ FEATURES_FILENAME: str = "features.fea"
121
+ LAYERCONTENTS_FILENAME: str = "layercontents.plist"
122
+ LAYERINFO_FILENAME: str = "layerinfo.plist"
93
123
 
94
- DEFAULT_LAYER_NAME = "public.default"
124
+ DEFAULT_LAYER_NAME: str = "public.default"
95
125
 
96
126
 
97
- class UFOFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
127
+ class UFOFormatVersion(BaseFormatVersion):
98
128
  FORMAT_1_0 = (1, 0)
99
129
  FORMAT_2_0 = (2, 0)
100
130
  FORMAT_3_0 = (3, 0)
101
131
 
102
132
 
103
- # python 3.11 doesn't like when a mixin overrides a dunder method like __str__
104
- # for some reasons it keep using Enum.__str__, see
105
- # https://github.com/fonttools/fonttools/pull/2655
106
- UFOFormatVersion.__str__ = _VersionTupleEnumMixin.__str__
107
-
108
-
109
133
  class UFOFileStructure(enum.Enum):
110
134
  ZIP = "zip"
111
135
  PACKAGE = "package"
@@ -117,7 +141,11 @@ class UFOFileStructure(enum.Enum):
117
141
 
118
142
 
119
143
  class _UFOBaseIO:
120
- def getFileModificationTime(self, path):
144
+ if TYPE_CHECKING:
145
+ fs: FS
146
+ _havePreviousFile: bool
147
+
148
+ def getFileModificationTime(self, path: PathStr) -> Optional[float]:
121
149
  """
122
150
  Returns the modification time for the file at the given path, as a
123
151
  floating point number giving the number of seconds since the epoch.
@@ -129,9 +157,11 @@ class _UFOBaseIO:
129
157
  except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound):
130
158
  return None
131
159
  else:
132
- return dt.timestamp()
160
+ if dt is not None:
161
+ return dt.timestamp()
162
+ return None
133
163
 
134
- def _getPlist(self, fileName, default=None):
164
+ def _getPlist(self, fileName: str, default: Optional[Any] = None) -> Any:
135
165
  """
136
166
  Read a property list relative to the UFO filesystem's root.
137
167
  Raises UFOLibError if the file is missing and default is None,
@@ -155,7 +185,7 @@ class _UFOBaseIO:
155
185
  # TODO(anthrotype): try to narrow this down a little
156
186
  raise UFOLibError(f"'{fileName}' could not be read on {self.fs}: {e}")
157
187
 
158
- def _writePlist(self, fileName, obj):
188
+ def _writePlist(self, fileName: str, obj: Any) -> None:
159
189
  """
160
190
  Write a property list to a file relative to the UFO filesystem's root.
161
191
 
@@ -209,15 +239,17 @@ class UFOReader(_UFOBaseIO):
209
239
  ``False`` to not validate the data.
210
240
  """
211
241
 
212
- def __init__(self, path, validate=True):
213
- if hasattr(path, "__fspath__"): # support os.PathLike objects
242
+ def __init__(self, path: PathOrFS, validate: bool = True) -> None:
243
+ # Only call __fspath__ if path is not already a str or FS object
244
+ if not isinstance(path, (str, fs.base.FS)) and hasattr(path, "__fspath__"):
214
245
  path = path.__fspath__()
215
246
 
216
247
  if isinstance(path, str):
217
248
  structure = _sniffFileStructure(path)
249
+ parentFS: FS
218
250
  try:
219
251
  if structure is UFOFileStructure.ZIP:
220
- parentFS = fs.zipfs.ZipFS(path, write=False, encoding="utf-8")
252
+ parentFS = fs.zipfs.ZipFS(path, write=False, encoding="utf-8") # type: ignore[abstract]
221
253
  else:
222
254
  parentFS = fs.osfs.OSFS(path)
223
255
  except fs.errors.CreateFailed as e:
@@ -235,7 +267,7 @@ class UFOReader(_UFOBaseIO):
235
267
  if len(rootDirs) == 1:
236
268
  # 'ClosingSubFS' ensures that the parent zip file is closed when
237
269
  # its root subdirectory is closed
238
- self.fs = parentFS.opendir(
270
+ self.fs: FS = parentFS.opendir(
239
271
  rootDirs[0], factory=fs.subfs.ClosingSubFS
240
272
  )
241
273
  else:
@@ -247,10 +279,10 @@ class UFOReader(_UFOBaseIO):
247
279
  self.fs = parentFS
248
280
  # when passed a path string, we make sure we close the newly opened fs
249
281
  # upon calling UFOReader.close method or context manager's __exit__
250
- self._shouldClose = True
282
+ self._shouldClose: bool = True
251
283
  self._fileStructure = structure
252
284
  elif isinstance(path, fs.base.FS):
253
- filesystem = path
285
+ filesystem: FS = path
254
286
  try:
255
287
  filesystem.check()
256
288
  except fs.errors.FilesystemClosed:
@@ -272,9 +304,9 @@ class UFOReader(_UFOBaseIO):
272
304
  "Expected a path string or fs.base.FS object, found '%s'"
273
305
  % type(path).__name__
274
306
  )
275
- self._path = fsdecode(path)
276
- self._validate = validate
277
- self._upConvertedKerningData = None
307
+ self._path: str = fsdecode(path)
308
+ self._validate: bool = validate
309
+ self._upConvertedKerningData: Optional[dict[str, Any]] = None
278
310
 
279
311
  try:
280
312
  self.readMetaInfo(validate=validate)
@@ -284,7 +316,7 @@ class UFOReader(_UFOBaseIO):
284
316
 
285
317
  # properties
286
318
 
287
- def _get_path(self):
319
+ def _get_path(self) -> str:
288
320
  import warnings
289
321
 
290
322
  warnings.warn(
@@ -294,9 +326,9 @@ class UFOReader(_UFOBaseIO):
294
326
  )
295
327
  return self._path
296
328
 
297
- path = property(_get_path, doc="The path of the UFO (DEPRECATED).")
329
+ path: property = property(_get_path, doc="The path of the UFO (DEPRECATED).")
298
330
 
299
- def _get_formatVersion(self):
331
+ def _get_formatVersion(self) -> int:
300
332
  import warnings
301
333
 
302
334
  warnings.warn(
@@ -312,16 +344,16 @@ class UFOReader(_UFOBaseIO):
312
344
  )
313
345
 
314
346
  @property
315
- def formatVersionTuple(self):
347
+ def formatVersionTuple(self) -> tuple[int, int]:
316
348
  """The (major, minor) format version of the UFO.
317
349
  This is determined by reading metainfo.plist during __init__.
318
350
  """
319
351
  return self._formatVersion
320
352
 
321
- def _get_fileStructure(self):
353
+ def _get_fileStructure(self) -> Any:
322
354
  return self._fileStructure
323
355
 
324
- fileStructure = property(
356
+ fileStructure: property = property(
325
357
  _get_fileStructure,
326
358
  doc=(
327
359
  "The file structure of the UFO: "
@@ -331,7 +363,7 @@ class UFOReader(_UFOBaseIO):
331
363
 
332
364
  # up conversion
333
365
 
334
- def _upConvertKerning(self, validate):
366
+ def _upConvertKerning(self, validate: bool) -> None:
335
367
  """
336
368
  Up convert kerning and groups in UFO 1 and 2.
337
369
  The data will be held internally until each bit of data
@@ -385,7 +417,7 @@ class UFOReader(_UFOBaseIO):
385
417
 
386
418
  # support methods
387
419
 
388
- def readBytesFromPath(self, path):
420
+ def readBytesFromPath(self, path: PathStr) -> Optional[bytes]:
389
421
  """
390
422
  Returns the bytes in the file at the given path.
391
423
  The path must be relative to the UFO's filesystem root.
@@ -396,7 +428,9 @@ class UFOReader(_UFOBaseIO):
396
428
  except fs.errors.ResourceNotFound:
397
429
  return None
398
430
 
399
- def getReadFileForPath(self, path, encoding=None):
431
+ def getReadFileForPath(
432
+ self, path: PathStr, encoding: Optional[str] = None
433
+ ) -> Optional[Union[IO[bytes], IO[str]]]:
400
434
  """
401
435
  Returns a file (or file-like) object for the file at the given path.
402
436
  The path must be relative to the UFO path.
@@ -417,7 +451,7 @@ class UFOReader(_UFOBaseIO):
417
451
 
418
452
  # metainfo.plist
419
453
 
420
- def _readMetaInfo(self, validate=None):
454
+ def _readMetaInfo(self, validate: Optional[bool] = None) -> dict[str, Any]:
421
455
  """
422
456
  Read metainfo.plist and return raw data. Only used for internal operations.
423
457
 
@@ -459,7 +493,7 @@ class UFOReader(_UFOBaseIO):
459
493
  data["formatVersionTuple"] = formatVersion
460
494
  return data
461
495
 
462
- def readMetaInfo(self, validate=None):
496
+ def readMetaInfo(self, validate: Optional[bool] = None) -> None:
463
497
  """
464
498
  Read metainfo.plist and set formatVersion. Only used for internal operations.
465
499
 
@@ -471,7 +505,7 @@ class UFOReader(_UFOBaseIO):
471
505
 
472
506
  # groups.plist
473
507
 
474
- def _readGroups(self):
508
+ def _readGroups(self) -> dict[str, list[str]]:
475
509
  groups = self._getPlist(GROUPS_FILENAME, {})
476
510
  # remove any duplicate glyphs in a kerning group
477
511
  for groupName, glyphList in groups.items():
@@ -479,7 +513,7 @@ class UFOReader(_UFOBaseIO):
479
513
  groups[groupName] = list(OrderedDict.fromkeys(glyphList))
480
514
  return groups
481
515
 
482
- def readGroups(self, validate=None):
516
+ def readGroups(self, validate: Optional[bool] = None) -> dict[str, list[str]]:
483
517
  """
484
518
  Read groups.plist. Returns a dict.
485
519
  ``validate`` will validate the read data, by default it is set to the
@@ -490,7 +524,7 @@ class UFOReader(_UFOBaseIO):
490
524
  # handle up conversion
491
525
  if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
492
526
  self._upConvertKerning(validate)
493
- groups = self._upConvertedKerningData["groups"]
527
+ groups = cast(dict, self._upConvertedKerningData)["groups"]
494
528
  # normal
495
529
  else:
496
530
  groups = self._readGroups()
@@ -500,7 +534,9 @@ class UFOReader(_UFOBaseIO):
500
534
  raise UFOLibError(message)
501
535
  return groups
502
536
 
503
- def getKerningGroupConversionRenameMaps(self, validate=None):
537
+ def getKerningGroupRenameMaps(
538
+ self, validate: Optional[bool] = None
539
+ ) -> KerningGroupRenameMaps:
504
540
  """
505
541
  Get maps defining the renaming that was done during any
506
542
  needed kerning group conversion. This method returns a
@@ -524,17 +560,17 @@ class UFOReader(_UFOBaseIO):
524
560
  # use the public group reader to force the load and
525
561
  # conversion of the data if it hasn't happened yet.
526
562
  self.readGroups(validate=validate)
527
- return self._upConvertedKerningData["groupRenameMaps"]
563
+ return cast(dict, self._upConvertedKerningData)["groupRenameMaps"]
528
564
 
529
565
  # fontinfo.plist
530
566
 
531
- def _readInfo(self, validate):
567
+ def _readInfo(self, validate: bool) -> dict[str, Any]:
532
568
  data = self._getPlist(FONTINFO_FILENAME, {})
533
569
  if validate and not isinstance(data, dict):
534
570
  raise UFOLibError("fontinfo.plist is not properly formatted.")
535
571
  return data
536
572
 
537
- def readInfo(self, info, validate=None):
573
+ def readInfo(self, info: Any, validate: Optional[bool] = None) -> None:
538
574
  """
539
575
  Read fontinfo.plist. It requires an object that allows
540
576
  setting attributes with names that follow the fontinfo.plist
@@ -593,11 +629,11 @@ class UFOReader(_UFOBaseIO):
593
629
 
594
630
  # kerning.plist
595
631
 
596
- def _readKerning(self):
632
+ def _readKerning(self) -> KerningNested:
597
633
  data = self._getPlist(KERNING_FILENAME, {})
598
634
  return data
599
635
 
600
- def readKerning(self, validate=None):
636
+ def readKerning(self, validate: Optional[bool] = None) -> KerningDict:
601
637
  """
602
638
  Read kerning.plist. Returns a dict.
603
639
 
@@ -609,7 +645,7 @@ class UFOReader(_UFOBaseIO):
609
645
  # handle up conversion
610
646
  if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
611
647
  self._upConvertKerning(validate)
612
- kerningNested = self._upConvertedKerningData["kerning"]
648
+ kerningNested = cast(dict, self._upConvertedKerningData)["kerning"]
613
649
  # normal
614
650
  else:
615
651
  kerningNested = self._readKerning()
@@ -627,7 +663,7 @@ class UFOReader(_UFOBaseIO):
627
663
 
628
664
  # lib.plist
629
665
 
630
- def readLib(self, validate=None):
666
+ def readLib(self, validate: Optional[bool] = None) -> dict[str, Any]:
631
667
  """
632
668
  Read lib.plist. Returns a dict.
633
669
 
@@ -645,7 +681,7 @@ class UFOReader(_UFOBaseIO):
645
681
 
646
682
  # features.fea
647
683
 
648
- def readFeatures(self):
684
+ def readFeatures(self) -> str:
649
685
  """
650
686
  Read features.fea. Return a string.
651
687
  The returned string is empty if the file is missing.
@@ -658,7 +694,7 @@ class UFOReader(_UFOBaseIO):
658
694
 
659
695
  # glyph sets & layers
660
696
 
661
- def _readLayerContents(self, validate):
697
+ def _readLayerContents(self, validate: bool) -> list[tuple[str, str]]:
662
698
  """
663
699
  Rebuild the layer contents list by checking what glyphsets
664
700
  are available on disk.
@@ -674,7 +710,7 @@ class UFOReader(_UFOBaseIO):
674
710
  raise UFOLibError(error)
675
711
  return contents
676
712
 
677
- def getLayerNames(self, validate=None):
713
+ def getLayerNames(self, validate: Optional[bool] = None) -> list[str]:
678
714
  """
679
715
  Get the ordered layer names from layercontents.plist.
680
716
 
@@ -687,7 +723,7 @@ class UFOReader(_UFOBaseIO):
687
723
  layerNames = [layerName for layerName, directoryName in layerContents]
688
724
  return layerNames
689
725
 
690
- def getDefaultLayerName(self, validate=None):
726
+ def getDefaultLayerName(self, validate: Optional[bool] = None) -> str:
691
727
  """
692
728
  Get the default layer name from layercontents.plist.
693
729
 
@@ -703,7 +739,12 @@ class UFOReader(_UFOBaseIO):
703
739
  # this will already have been raised during __init__
704
740
  raise UFOLibError("The default layer is not defined in layercontents.plist.")
705
741
 
706
- def getGlyphSet(self, layerName=None, validateRead=None, validateWrite=None):
742
+ def getGlyphSet(
743
+ self,
744
+ layerName: Optional[str] = None,
745
+ validateRead: Optional[bool] = None,
746
+ validateWrite: Optional[bool] = None,
747
+ ) -> GlyphSet:
707
748
  """
708
749
  Return the GlyphSet associated with the
709
750
  glyphs directory mapped to layerName
@@ -744,7 +785,9 @@ class UFOReader(_UFOBaseIO):
744
785
  expectContentsFile=True,
745
786
  )
746
787
 
747
- def getCharacterMapping(self, layerName=None, validate=None):
788
+ def getCharacterMapping(
789
+ self, layerName: Optional[str] = None, validate: Optional[bool] = None
790
+ ) -> dict[int, list[str]]:
748
791
  """
749
792
  Return a dictionary that maps unicode values (ints) to
750
793
  lists of glyph names.
@@ -755,7 +798,7 @@ class UFOReader(_UFOBaseIO):
755
798
  layerName, validateRead=validate, validateWrite=True
756
799
  )
757
800
  allUnicodes = glyphSet.getUnicodes()
758
- cmap = {}
801
+ cmap: dict[int, list[str]] = {}
759
802
  for glyphName, unicodes in allUnicodes.items():
760
803
  for code in unicodes:
761
804
  if code in cmap:
@@ -766,7 +809,7 @@ class UFOReader(_UFOBaseIO):
766
809
 
767
810
  # /data
768
811
 
769
- def getDataDirectoryListing(self):
812
+ def getDataDirectoryListing(self) -> list[str]:
770
813
  """
771
814
  Returns a list of all files in the data directory.
772
815
  The returned paths will be relative to the UFO.
@@ -787,7 +830,7 @@ class UFOReader(_UFOBaseIO):
787
830
  except fs.errors.ResourceError:
788
831
  return []
789
832
 
790
- def getImageDirectoryListing(self, validate=None):
833
+ def getImageDirectoryListing(self, validate: Optional[bool] = None) -> list[str]:
791
834
  """
792
835
  Returns a list of all image file names in
793
836
  the images directory. Each of the images will
@@ -823,7 +866,7 @@ class UFOReader(_UFOBaseIO):
823
866
  result.append(path.name)
824
867
  return result
825
868
 
826
- def readData(self, fileName):
869
+ def readData(self, fileName: PathStr) -> bytes:
827
870
  """
828
871
  Return bytes for the file named 'fileName' inside the 'data/' directory.
829
872
  """
@@ -839,7 +882,7 @@ class UFOReader(_UFOBaseIO):
839
882
  raise UFOLibError(f"No data file named '{fileName}' on {self.fs}")
840
883
  return data
841
884
 
842
- def readImage(self, fileName, validate=None):
885
+ def readImage(self, fileName: PathStr, validate: Optional[bool] = None) -> bytes:
843
886
  """
844
887
  Return image data for the file named fileName.
845
888
 
@@ -868,14 +911,14 @@ class UFOReader(_UFOBaseIO):
868
911
  raise UFOLibError(error)
869
912
  return data
870
913
 
871
- def close(self):
914
+ def close(self) -> None:
872
915
  if self._shouldClose:
873
916
  self.fs.close()
874
917
 
875
- def __enter__(self):
918
+ def __enter__(self) -> UFOReader:
876
919
  return self
877
920
 
878
- def __exit__(self, exc_type, exc_value, exc_tb):
921
+ def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None:
879
922
  self.close()
880
923
 
881
924
 
@@ -910,14 +953,14 @@ class UFOWriter(UFOReader):
910
953
 
911
954
  def __init__(
912
955
  self,
913
- path,
914
- formatVersion=None,
915
- fileCreator="com.github.fonttools.ufoLib",
916
- structure=None,
917
- validate=True,
918
- ):
956
+ path: PathOrFS,
957
+ formatVersion: UFOFormatVersionInput = None,
958
+ fileCreator: str = "com.github.fonttools.ufoLib",
959
+ structure: Optional[UFOFileStructure] = None,
960
+ validate: bool = True,
961
+ ) -> None:
919
962
  try:
920
- formatVersion = UFOFormatVersion(formatVersion)
963
+ formatVersion = normalizeFormatVersion(formatVersion, UFOFormatVersion)
921
964
  except ValueError as e:
922
965
  from fontTools.ufoLib.errors import UnsupportedUFOFormat
923
966
 
@@ -963,8 +1006,8 @@ class UFOWriter(UFOReader):
963
1006
  # we can't write a zip in-place, so we have to copy its
964
1007
  # contents to a temporary location and work from there, then
965
1008
  # upon closing UFOWriter we create the final zip file
966
- parentFS = fs.tempfs.TempFS()
967
- with fs.zipfs.ZipFS(path, encoding="utf-8") as origFS:
1009
+ parentFS: FS = fs.tempfs.TempFS()
1010
+ with fs.zipfs.ZipFS(path, encoding="utf-8") as origFS: # type: ignore[abstract]
968
1011
  fs.copy.copy_fs(origFS, parentFS)
969
1012
  # if output path is an existing zip, we require that it contains
970
1013
  # one, and only one, root directory (with arbitrary name), in turn
@@ -986,7 +1029,7 @@ class UFOWriter(UFOReader):
986
1029
  # if the output zip file didn't exist, we create the root folder;
987
1030
  # we name it the same as input 'path', but with '.ufo' extension
988
1031
  rootDir = os.path.splitext(os.path.basename(path))[0] + ".ufo"
989
- parentFS = fs.zipfs.ZipFS(path, write=True, encoding="utf-8")
1032
+ parentFS = fs.zipfs.ZipFS(path, write=True, encoding="utf-8") # type: ignore[abstract]
990
1033
  parentFS.makedir(rootDir)
991
1034
  # 'ClosingSubFS' ensures that the parent filesystem is closed
992
1035
  # when its root subdirectory is closed
@@ -997,7 +1040,7 @@ class UFOWriter(UFOReader):
997
1040
  self._havePreviousFile = havePreviousFile
998
1041
  self._shouldClose = True
999
1042
  elif isinstance(path, fs.base.FS):
1000
- filesystem = path
1043
+ filesystem: FS = path
1001
1044
  try:
1002
1045
  filesystem.check()
1003
1046
  except fs.errors.FilesystemClosed:
@@ -1032,7 +1075,7 @@ class UFOWriter(UFOReader):
1032
1075
  self._path = fsdecode(path)
1033
1076
  self._formatVersion = formatVersion
1034
1077
  self._fileCreator = fileCreator
1035
- self._downConversionKerningData = None
1078
+ self._downConversionKerningData: Optional[KerningGroupRenameMaps] = None
1036
1079
  self._validate = validate
1037
1080
  # if the file already exists, get the format version.
1038
1081
  # this will be needed for up and down conversion.
@@ -1050,7 +1093,7 @@ class UFOWriter(UFOReader):
1050
1093
  "that is trying to be written. This is not supported."
1051
1094
  )
1052
1095
  # handle the layer contents
1053
- self.layerContents = {}
1096
+ self.layerContents: Union[dict[str, str], OrderedDict[str, str]] = {}
1054
1097
  if previousFormatVersion is not None and previousFormatVersion.major >= 3:
1055
1098
  # already exists
1056
1099
  self.layerContents = OrderedDict(self._readLayerContents(validate))
@@ -1064,17 +1107,19 @@ class UFOWriter(UFOReader):
1064
1107
 
1065
1108
  # properties
1066
1109
 
1067
- def _get_fileCreator(self):
1110
+ def _get_fileCreator(self) -> str:
1068
1111
  return self._fileCreator
1069
1112
 
1070
- fileCreator = property(
1113
+ fileCreator: property = property(
1071
1114
  _get_fileCreator,
1072
1115
  doc="The file creator of the UFO. This is set into metainfo.plist during __init__.",
1073
1116
  )
1074
1117
 
1075
1118
  # support methods for file system interaction
1076
1119
 
1077
- def copyFromReader(self, reader, sourcePath, destPath):
1120
+ def copyFromReader(
1121
+ self, reader: UFOReader, sourcePath: PathStr, destPath: PathStr
1122
+ ) -> None:
1078
1123
  """
1079
1124
  Copy the sourcePath in the provided UFOReader to destPath
1080
1125
  in this writer. The paths must be relative. This works with
@@ -1097,7 +1142,7 @@ class UFOWriter(UFOReader):
1097
1142
  else:
1098
1143
  fs.copy.copy_file(reader.fs, sourcePath, self.fs, destPath)
1099
1144
 
1100
- def writeBytesToPath(self, path, data):
1145
+ def writeBytesToPath(self, path: PathStr, data: bytes) -> None:
1101
1146
  """
1102
1147
  Write bytes to a path relative to the UFO filesystem's root.
1103
1148
  If writing to an existing UFO, check to see if data matches the data
@@ -1117,7 +1162,12 @@ class UFOWriter(UFOReader):
1117
1162
  self.fs.makedirs(fs.path.dirname(path), recreate=True)
1118
1163
  self.fs.writebytes(path, data)
1119
1164
 
1120
- def getFileObjectForPath(self, path, mode="w", encoding=None):
1165
+ def getFileObjectForPath(
1166
+ self,
1167
+ path: PathStr,
1168
+ mode: str = "w",
1169
+ encoding: Optional[str] = None,
1170
+ ) -> Optional[IO[Any]]:
1121
1171
  """
1122
1172
  Returns a file (or file-like) object for the
1123
1173
  file at the given path. The path must be relative
@@ -1140,9 +1190,12 @@ class UFOWriter(UFOReader):
1140
1190
  self.fs.makedirs(fs.path.dirname(path), recreate=True)
1141
1191
  return self.fs.open(path, mode=mode, encoding=encoding)
1142
1192
  except fs.errors.ResourceError as e:
1143
- return UFOLibError(f"unable to open '{path}' on {self.fs}: {e}")
1193
+ raise UFOLibError(f"unable to open '{path}' on {self.fs}: {e}")
1194
+ return None
1144
1195
 
1145
- def removePath(self, path, force=False, removeEmptyParents=True):
1196
+ def removePath(
1197
+ self, path: PathStr, force: bool = False, removeEmptyParents: bool = True
1198
+ ) -> None:
1146
1199
  """
1147
1200
  Remove the file (or directory) at path. The path
1148
1201
  must be relative to the UFO.
@@ -1169,7 +1222,7 @@ class UFOWriter(UFOReader):
1169
1222
 
1170
1223
  # UFO mod time
1171
1224
 
1172
- def setModificationTime(self):
1225
+ def setModificationTime(self) -> None:
1173
1226
  """
1174
1227
  Set the UFO modification time to the current time.
1175
1228
  This is never called automatically. It is up to the
@@ -1185,7 +1238,7 @@ class UFOWriter(UFOReader):
1185
1238
 
1186
1239
  # metainfo.plist
1187
1240
 
1188
- def _writeMetaInfo(self):
1241
+ def _writeMetaInfo(self) -> None:
1189
1242
  metaInfo = dict(
1190
1243
  creator=self._fileCreator,
1191
1244
  formatVersion=self._formatVersion.major,
@@ -1196,7 +1249,7 @@ class UFOWriter(UFOReader):
1196
1249
 
1197
1250
  # groups.plist
1198
1251
 
1199
- def setKerningGroupConversionRenameMaps(self, maps):
1252
+ def setKerningGroupConversionRenameMaps(self, maps: KerningGroupRenameMaps) -> None:
1200
1253
  """
1201
1254
  Set maps defining the renaming that should be done
1202
1255
  when writing groups and kerning in UFO 1 and UFO 2.
@@ -1210,7 +1263,7 @@ class UFOWriter(UFOReader):
1210
1263
  }
1211
1264
 
1212
1265
  This is the same form returned by UFOReader's
1213
- getKerningGroupConversionRenameMaps method.
1266
+ getKerningGroupRenameMaps method.
1214
1267
  """
1215
1268
  if self._formatVersion >= UFOFormatVersion.FORMAT_3_0:
1216
1269
  return # XXX raise an error here
@@ -1221,7 +1274,9 @@ class UFOWriter(UFOReader):
1221
1274
  remap[dataName] = writeName
1222
1275
  self._downConversionKerningData = dict(groupRenameMap=remap)
1223
1276
 
1224
- def writeGroups(self, groups, validate=None):
1277
+ def writeGroups(
1278
+ self, groups: KerningGroups, validate: Optional[bool] = None
1279
+ ) -> None:
1225
1280
  """
1226
1281
  Write groups.plist. This method requires a
1227
1282
  dict of glyph groups as an argument.
@@ -1276,7 +1331,7 @@ class UFOWriter(UFOReader):
1276
1331
 
1277
1332
  # fontinfo.plist
1278
1333
 
1279
- def writeInfo(self, info, validate=None):
1334
+ def writeInfo(self, info: Any, validate: Optional[bool] = None) -> None:
1280
1335
  """
1281
1336
  Write info.plist. This method requires an object
1282
1337
  that supports getting attributes that follow the
@@ -1322,7 +1377,9 @@ class UFOWriter(UFOReader):
1322
1377
 
1323
1378
  # kerning.plist
1324
1379
 
1325
- def writeKerning(self, kerning, validate=None):
1380
+ def writeKerning(
1381
+ self, kerning: KerningDict, validate: Optional[bool] = None
1382
+ ) -> None:
1326
1383
  """
1327
1384
  Write kerning.plist. This method requires a
1328
1385
  dict of kerning pairs as an argument.
@@ -1366,7 +1423,7 @@ class UFOWriter(UFOReader):
1366
1423
  remappedKerning[side1, side2] = value
1367
1424
  kerning = remappedKerning
1368
1425
  # pack and write
1369
- kerningDict = {}
1426
+ kerningDict: KerningNested = {}
1370
1427
  for left, right in kerning.keys():
1371
1428
  value = kerning[left, right]
1372
1429
  if left not in kerningDict:
@@ -1379,7 +1436,7 @@ class UFOWriter(UFOReader):
1379
1436
 
1380
1437
  # lib.plist
1381
1438
 
1382
- def writeLib(self, libDict, validate=None):
1439
+ def writeLib(self, libDict: LibDict, validate: Optional[bool] = None) -> None:
1383
1440
  """
1384
1441
  Write lib.plist. This method requires a
1385
1442
  lib dict as an argument.
@@ -1400,7 +1457,7 @@ class UFOWriter(UFOReader):
1400
1457
 
1401
1458
  # features.fea
1402
1459
 
1403
- def writeFeatures(self, features, validate=None):
1460
+ def writeFeatures(self, features: str, validate: Optional[bool] = None) -> None:
1404
1461
  """
1405
1462
  Write features.fea. This method requires a
1406
1463
  features string as an argument.
@@ -1419,7 +1476,9 @@ class UFOWriter(UFOReader):
1419
1476
 
1420
1477
  # glyph sets & layers
1421
1478
 
1422
- def writeLayerContents(self, layerOrder=None, validate=None):
1479
+ def writeLayerContents(
1480
+ self, layerOrder: LayerOrderList = None, validate: Optional[bool] = None
1481
+ ) -> None:
1423
1482
  """
1424
1483
  Write the layercontents.plist file. This method *must* be called
1425
1484
  after all glyph sets have been written.
@@ -1429,7 +1488,7 @@ class UFOWriter(UFOReader):
1429
1488
  if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
1430
1489
  return
1431
1490
  if layerOrder is not None:
1432
- newOrder = []
1491
+ newOrder: list[Optional[str]] = []
1433
1492
  for layerName in layerOrder:
1434
1493
  if layerName is None:
1435
1494
  layerName = DEFAULT_LAYER_NAME
@@ -1442,11 +1501,13 @@ class UFOWriter(UFOReader):
1442
1501
  "The layer order content does not match the glyph sets that have been created."
1443
1502
  )
1444
1503
  layerContents = [
1445
- (layerName, self.layerContents[layerName]) for layerName in layerOrder
1504
+ (layerName, self.layerContents[layerName])
1505
+ for layerName in layerOrder
1506
+ if layerName is not None
1446
1507
  ]
1447
1508
  self._writePlist(LAYERCONTENTS_FILENAME, layerContents)
1448
1509
 
1449
- def _findDirectoryForLayerName(self, layerName):
1510
+ def _findDirectoryForLayerName(self, layerName: Optional[str]) -> str:
1450
1511
  foundDirectory = None
1451
1512
  for existingLayerName, directoryName in list(self.layerContents.items()):
1452
1513
  if layerName is None and directoryName == DEFAULT_GLYPHS_DIRNAME:
@@ -1462,15 +1523,15 @@ class UFOWriter(UFOReader):
1462
1523
  )
1463
1524
  return foundDirectory
1464
1525
 
1465
- def getGlyphSet(
1526
+ def getGlyphSet( # type: ignore[override]
1466
1527
  self,
1467
- layerName=None,
1468
- defaultLayer=True,
1469
- glyphNameToFileNameFunc=None,
1470
- validateRead=None,
1471
- validateWrite=None,
1472
- expectContentsFile=False,
1473
- ):
1528
+ layerName: Optional[str] = None,
1529
+ defaultLayer: bool = True,
1530
+ glyphNameToFileNameFunc: GlyphNameToFileNameFunc = None,
1531
+ validateRead: Optional[bool] = None,
1532
+ validateWrite: Optional[bool] = None,
1533
+ expectContentsFile: bool = False,
1534
+ ) -> GlyphSet:
1474
1535
  """
1475
1536
  Return the GlyphSet object associated with the
1476
1537
  appropriate glyph directory in the .ufo.
@@ -1530,11 +1591,11 @@ class UFOWriter(UFOReader):
1530
1591
 
1531
1592
  def _getDefaultGlyphSet(
1532
1593
  self,
1533
- validateRead,
1534
- validateWrite,
1535
- glyphNameToFileNameFunc=None,
1536
- expectContentsFile=False,
1537
- ):
1594
+ validateRead: bool,
1595
+ validateWrite: bool,
1596
+ glyphNameToFileNameFunc: GlyphNameToFileNameFunc = None,
1597
+ expectContentsFile: bool = False,
1598
+ ) -> GlyphSet:
1538
1599
  from fontTools.ufoLib.glifLib import GlyphSet
1539
1600
 
1540
1601
  glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True)
@@ -1549,13 +1610,13 @@ class UFOWriter(UFOReader):
1549
1610
 
1550
1611
  def _getGlyphSetFormatVersion3(
1551
1612
  self,
1552
- validateRead,
1553
- validateWrite,
1554
- layerName=None,
1555
- defaultLayer=True,
1556
- glyphNameToFileNameFunc=None,
1557
- expectContentsFile=False,
1558
- ):
1613
+ validateRead: bool,
1614
+ validateWrite: bool,
1615
+ layerName: Optional[str] = None,
1616
+ defaultLayer: bool = True,
1617
+ glyphNameToFileNameFunc: GlyphNameToFileNameFunc = None,
1618
+ expectContentsFile: bool = False,
1619
+ ) -> GlyphSet:
1559
1620
  from fontTools.ufoLib.glifLib import GlyphSet
1560
1621
 
1561
1622
  # if the default flag is on, make sure that the default in the file
@@ -1573,6 +1634,11 @@ class UFOWriter(UFOReader):
1573
1634
  raise UFOLibError(
1574
1635
  "The layer name is already mapped to a non-default layer."
1575
1636
  )
1637
+
1638
+ # handle layerName is None to avoid MyPy errors
1639
+ if layerName is None:
1640
+ raise TypeError("'leyerName' cannot be None.")
1641
+
1576
1642
  # get an existing directory name
1577
1643
  if layerName in self.layerContents:
1578
1644
  directory = self.layerContents[layerName]
@@ -1601,7 +1667,12 @@ class UFOWriter(UFOReader):
1601
1667
  expectContentsFile=expectContentsFile,
1602
1668
  )
1603
1669
 
1604
- def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False):
1670
+ def renameGlyphSet(
1671
+ self,
1672
+ layerName: Optional[str],
1673
+ newLayerName: Optional[str],
1674
+ defaultLayer: bool = False,
1675
+ ) -> None:
1605
1676
  """
1606
1677
  Rename a glyph set.
1607
1678
 
@@ -1615,7 +1686,7 @@ class UFOWriter(UFOReader):
1615
1686
  return
1616
1687
  # the new and old names can be the same
1617
1688
  # as long as the default is being switched
1618
- if layerName == newLayerName:
1689
+ if layerName is not None and layerName == newLayerName:
1619
1690
  # if the default is off and the layer is already not the default, skip
1620
1691
  if (
1621
1692
  self.layerContents[layerName] != DEFAULT_GLYPHS_DIRNAME
@@ -1644,12 +1715,13 @@ class UFOWriter(UFOReader):
1644
1715
  newLayerName, existing=existing, prefix="glyphs."
1645
1716
  )
1646
1717
  # update the internal mapping
1647
- del self.layerContents[layerName]
1718
+ if layerName is not None:
1719
+ del self.layerContents[layerName]
1648
1720
  self.layerContents[newLayerName] = newDirectory
1649
1721
  # do the file system copy
1650
1722
  self.fs.movedir(oldDirectory, newDirectory, create=True)
1651
1723
 
1652
- def deleteGlyphSet(self, layerName):
1724
+ def deleteGlyphSet(self, layerName: Optional[str]) -> None:
1653
1725
  """
1654
1726
  Remove the glyph set matching layerName.
1655
1727
  """
@@ -1659,16 +1731,17 @@ class UFOWriter(UFOReader):
1659
1731
  return
1660
1732
  foundDirectory = self._findDirectoryForLayerName(layerName)
1661
1733
  self.removePath(foundDirectory, removeEmptyParents=False)
1662
- del self.layerContents[layerName]
1734
+ if layerName is not None:
1735
+ del self.layerContents[layerName]
1663
1736
 
1664
- def writeData(self, fileName, data):
1737
+ def writeData(self, fileName: PathStr, data: bytes) -> None:
1665
1738
  """
1666
1739
  Write data to fileName in the 'data' directory.
1667
1740
  The data must be a bytes string.
1668
1741
  """
1669
1742
  self.writeBytesToPath(f"{DATA_DIRNAME}/{fsdecode(fileName)}", data)
1670
1743
 
1671
- def removeData(self, fileName):
1744
+ def removeData(self, fileName: PathStr) -> None:
1672
1745
  """
1673
1746
  Remove the file named fileName from the data directory.
1674
1747
  """
@@ -1676,7 +1749,12 @@ class UFOWriter(UFOReader):
1676
1749
 
1677
1750
  # /images
1678
1751
 
1679
- def writeImage(self, fileName, data, validate=None):
1752
+ def writeImage(
1753
+ self,
1754
+ fileName: PathStr,
1755
+ data: bytes,
1756
+ validate: Optional[bool] = None,
1757
+ ) -> None:
1680
1758
  """
1681
1759
  Write data to fileName in the images directory.
1682
1760
  The data must be a valid PNG.
@@ -1694,7 +1772,11 @@ class UFOWriter(UFOReader):
1694
1772
  raise UFOLibError(error)
1695
1773
  self.writeBytesToPath(f"{IMAGES_DIRNAME}/{fileName}", data)
1696
1774
 
1697
- def removeImage(self, fileName, validate=None): # XXX remove unused 'validate'?
1775
+ def removeImage(
1776
+ self,
1777
+ fileName: PathStr,
1778
+ validate: Optional[bool] = None,
1779
+ ) -> None: # XXX remove unused 'validate'?
1698
1780
  """
1699
1781
  Remove the file named fileName from the
1700
1782
  images directory.
@@ -1705,7 +1787,13 @@ class UFOWriter(UFOReader):
1705
1787
  )
1706
1788
  self.removePath(f"{IMAGES_DIRNAME}/{fsdecode(fileName)}")
1707
1789
 
1708
- def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None):
1790
+ def copyImageFromReader(
1791
+ self,
1792
+ reader: UFOReader,
1793
+ sourceFileName: PathStr,
1794
+ destFileName: PathStr,
1795
+ validate: Optional[bool] = None,
1796
+ ) -> None:
1709
1797
  """
1710
1798
  Copy the sourceFileName in the provided UFOReader to destFileName
1711
1799
  in this writer. This uses the most memory efficient method possible
@@ -1721,12 +1809,12 @@ class UFOWriter(UFOReader):
1721
1809
  destPath = f"{IMAGES_DIRNAME}/{fsdecode(destFileName)}"
1722
1810
  self.copyFromReader(reader, sourcePath, destPath)
1723
1811
 
1724
- def close(self):
1812
+ def close(self) -> None:
1725
1813
  if self._havePreviousFile and self._fileStructure is UFOFileStructure.ZIP:
1726
1814
  # if we are updating an existing zip file, we can now compress the
1727
1815
  # contents of the temporary filesystem in the destination path
1728
1816
  rootDir = os.path.splitext(os.path.basename(self._path))[0] + ".ufo"
1729
- with fs.zipfs.ZipFS(self._path, write=True, encoding="utf-8") as destFS:
1817
+ with fs.zipfs.ZipFS(self._path, write=True, encoding="utf-8") as destFS: # type: ignore[abstract]
1730
1818
  fs.copy.copy_fs(self.fs, destFS.makedir(rootDir))
1731
1819
  super().close()
1732
1820
 
@@ -1740,7 +1828,7 @@ UFOReaderWriter = UFOWriter
1740
1828
  # ----------------
1741
1829
 
1742
1830
 
1743
- def _sniffFileStructure(ufo_path):
1831
+ def _sniffFileStructure(ufo_path: PathStr) -> UFOFileStructure:
1744
1832
  """Return UFOFileStructure.ZIP if the UFO at path 'ufo_path' (str)
1745
1833
  is a zip file, else return UFOFileStructure.PACKAGE if 'ufo_path' is a
1746
1834
  directory.
@@ -1759,7 +1847,7 @@ def _sniffFileStructure(ufo_path):
1759
1847
  raise UFOLibError("No such file or directory: '%s'" % ufo_path)
1760
1848
 
1761
1849
 
1762
- def makeUFOPath(path):
1850
+ def makeUFOPath(path: PathStr) -> str:
1763
1851
  """
1764
1852
  Return a .ufo pathname.
1765
1853
 
@@ -1786,7 +1874,7 @@ def makeUFOPath(path):
1786
1874
  # cases of invalid values.
1787
1875
 
1788
1876
 
1789
- def validateFontInfoVersion2ValueForAttribute(attr, value):
1877
+ def validateFontInfoVersion2ValueForAttribute(attr: str, value: Any) -> bool:
1790
1878
  """
1791
1879
  This performs very basic validation of the value for attribute
1792
1880
  following the UFO 2 fontinfo.plist specification. The results
@@ -1798,7 +1886,7 @@ def validateFontInfoVersion2ValueForAttribute(attr, value):
1798
1886
  """
1799
1887
  dataValidationDict = fontInfoAttributesVersion2ValueData[attr]
1800
1888
  valueType = dataValidationDict.get("type")
1801
- validator = dataValidationDict.get("valueValidator")
1889
+ validator = dataValidationDict.get("valueValidator", genericTypeValidator)
1802
1890
  valueOptions = dataValidationDict.get("valueOptions")
1803
1891
  # have specific options for the validator
1804
1892
  if valueOptions is not None:
@@ -1812,7 +1900,7 @@ def validateFontInfoVersion2ValueForAttribute(attr, value):
1812
1900
  return isValidValue
1813
1901
 
1814
1902
 
1815
- def validateInfoVersion2Data(infoData):
1903
+ def validateInfoVersion2Data(infoData: dict[str, Any]) -> dict[str, Any]:
1816
1904
  """
1817
1905
  This performs very basic validation of the value for infoData
1818
1906
  following the UFO 2 fontinfo.plist specification. The results
@@ -1832,7 +1920,7 @@ def validateInfoVersion2Data(infoData):
1832
1920
  return validInfoData
1833
1921
 
1834
1922
 
1835
- def validateFontInfoVersion3ValueForAttribute(attr, value):
1923
+ def validateFontInfoVersion3ValueForAttribute(attr: str, value: Any) -> bool:
1836
1924
  """
1837
1925
  This performs very basic validation of the value for attribute
1838
1926
  following the UFO 3 fontinfo.plist specification. The results
@@ -1844,7 +1932,7 @@ def validateFontInfoVersion3ValueForAttribute(attr, value):
1844
1932
  """
1845
1933
  dataValidationDict = fontInfoAttributesVersion3ValueData[attr]
1846
1934
  valueType = dataValidationDict.get("type")
1847
- validator = dataValidationDict.get("valueValidator")
1935
+ validator = dataValidationDict.get("valueValidator", genericTypeValidator)
1848
1936
  valueOptions = dataValidationDict.get("valueOptions")
1849
1937
  # have specific options for the validator
1850
1938
  if valueOptions is not None:
@@ -1858,7 +1946,7 @@ def validateFontInfoVersion3ValueForAttribute(attr, value):
1858
1946
  return isValidValue
1859
1947
 
1860
1948
 
1861
- def validateInfoVersion3Data(infoData):
1949
+ def validateInfoVersion3Data(infoData: dict[str, Any]) -> dict[str, Any]:
1862
1950
  """
1863
1951
  This performs very basic validation of the value for infoData
1864
1952
  following the UFO 3 fontinfo.plist specification. The results
@@ -1880,18 +1968,18 @@ def validateInfoVersion3Data(infoData):
1880
1968
 
1881
1969
  # Value Options
1882
1970
 
1883
- fontInfoOpenTypeHeadFlagsOptions = list(range(0, 15))
1884
- fontInfoOpenTypeOS2SelectionOptions = [1, 2, 3, 4, 7, 8, 9]
1885
- fontInfoOpenTypeOS2UnicodeRangesOptions = list(range(0, 128))
1886
- fontInfoOpenTypeOS2CodePageRangesOptions = list(range(0, 64))
1887
- fontInfoOpenTypeOS2TypeOptions = [0, 1, 2, 3, 8, 9]
1971
+ fontInfoOpenTypeHeadFlagsOptions: list[int] = list(range(0, 15))
1972
+ fontInfoOpenTypeOS2SelectionOptions: list[int] = [1, 2, 3, 4, 7, 8, 9]
1973
+ fontInfoOpenTypeOS2UnicodeRangesOptions: list[int] = list(range(0, 128))
1974
+ fontInfoOpenTypeOS2CodePageRangesOptions: list[int] = list(range(0, 64))
1975
+ fontInfoOpenTypeOS2TypeOptions: list[int] = [0, 1, 2, 3, 8, 9]
1888
1976
 
1889
1977
  # Version Attribute Definitions
1890
1978
  # This defines the attributes, types and, in some
1891
1979
  # cases the possible values, that can exist is
1892
1980
  # fontinfo.plist.
1893
1981
 
1894
- fontInfoAttributesVersion1 = {
1982
+ fontInfoAttributesVersion1: set[str] = {
1895
1983
  "familyName",
1896
1984
  "styleName",
1897
1985
  "fullName",
@@ -1934,7 +2022,7 @@ fontInfoAttributesVersion1 = {
1934
2022
  "ttVersion",
1935
2023
  }
1936
2024
 
1937
- fontInfoAttributesVersion2ValueData = {
2025
+ fontInfoAttributesVersion2ValueData: FontInfoAttributes = {
1938
2026
  "familyName": dict(type=str),
1939
2027
  "styleName": dict(type=str),
1940
2028
  "styleMapFamilyName": dict(type=str),
@@ -2076,9 +2164,11 @@ fontInfoAttributesVersion2ValueData = {
2076
2164
  "macintoshFONDFamilyID": dict(type=int),
2077
2165
  "macintoshFONDName": dict(type=str),
2078
2166
  }
2079
- fontInfoAttributesVersion2 = set(fontInfoAttributesVersion2ValueData.keys())
2167
+ fontInfoAttributesVersion2: set[str] = set(fontInfoAttributesVersion2ValueData.keys())
2080
2168
 
2081
- fontInfoAttributesVersion3ValueData = deepcopy(fontInfoAttributesVersion2ValueData)
2169
+ fontInfoAttributesVersion3ValueData: FontInfoAttributes = deepcopy(
2170
+ fontInfoAttributesVersion2ValueData
2171
+ )
2082
2172
  fontInfoAttributesVersion3ValueData.update(
2083
2173
  {
2084
2174
  "versionMinor": dict(type=int, valueValidator=genericNonNegativeIntValidator),
@@ -2161,7 +2251,7 @@ fontInfoAttributesVersion3ValueData.update(
2161
2251
  "guidelines": dict(type=list, valueValidator=guidelinesValidator),
2162
2252
  }
2163
2253
  )
2164
- fontInfoAttributesVersion3 = set(fontInfoAttributesVersion3ValueData.keys())
2254
+ fontInfoAttributesVersion3: set[str] = set(fontInfoAttributesVersion3ValueData.keys())
2165
2255
 
2166
2256
  # insert the type validator for all attrs that
2167
2257
  # have no defined validator.
@@ -2178,14 +2268,14 @@ for attr, dataDict in list(fontInfoAttributesVersion3ValueData.items()):
2178
2268
  # to version 2 or vice-versa.
2179
2269
 
2180
2270
 
2181
- def _flipDict(d):
2271
+ def _flipDict(d: dict[K, V]) -> dict[V, K]:
2182
2272
  flipped = {}
2183
2273
  for key, value in list(d.items()):
2184
2274
  flipped[value] = key
2185
2275
  return flipped
2186
2276
 
2187
2277
 
2188
- fontInfoAttributesVersion1To2 = {
2278
+ fontInfoAttributesVersion1To2: dict[str, str] = {
2189
2279
  "menuName": "styleMapFamilyName",
2190
2280
  "designer": "openTypeNameDesigner",
2191
2281
  "designerURL": "openTypeNameDesignerURL",
@@ -2217,12 +2307,17 @@ fontInfoAttributesVersion1To2 = {
2217
2307
  fontInfoAttributesVersion2To1 = _flipDict(fontInfoAttributesVersion1To2)
2218
2308
  deprecatedFontInfoAttributesVersion2 = set(fontInfoAttributesVersion1To2.keys())
2219
2309
 
2220
- _fontStyle1To2 = {64: "regular", 1: "italic", 32: "bold", 33: "bold italic"}
2221
- _fontStyle2To1 = _flipDict(_fontStyle1To2)
2310
+ _fontStyle1To2: dict[int, str] = {
2311
+ 64: "regular",
2312
+ 1: "italic",
2313
+ 32: "bold",
2314
+ 33: "bold italic",
2315
+ }
2316
+ _fontStyle2To1: dict[str, int] = _flipDict(_fontStyle1To2)
2222
2317
  # Some UFO 1 files have 0
2223
2318
  _fontStyle1To2[0] = "regular"
2224
2319
 
2225
- _widthName1To2 = {
2320
+ _widthName1To2: dict[str, int] = {
2226
2321
  "Ultra-condensed": 1,
2227
2322
  "Extra-condensed": 2,
2228
2323
  "Condensed": 3,
@@ -2233,7 +2328,7 @@ _widthName1To2 = {
2233
2328
  "Extra-expanded": 8,
2234
2329
  "Ultra-expanded": 9,
2235
2330
  }
2236
- _widthName2To1 = _flipDict(_widthName1To2)
2331
+ _widthName2To1: dict[int, str] = _flipDict(_widthName1To2)
2237
2332
  # FontLab's default width value is "Normal".
2238
2333
  # Many format version 1 UFOs will have this.
2239
2334
  _widthName1To2["Normal"] = 5
@@ -2245,7 +2340,7 @@ _widthName1To2["medium"] = 5
2245
2340
  # "Medium" appears in a lot of UFO 1 files.
2246
2341
  _widthName1To2["Medium"] = 5
2247
2342
 
2248
- _msCharSet1To2 = {
2343
+ _msCharSet1To2: dict[int, int] = {
2249
2344
  0: 1,
2250
2345
  1: 2,
2251
2346
  2: 3,
@@ -2267,12 +2362,14 @@ _msCharSet1To2 = {
2267
2362
  238: 19,
2268
2363
  255: 20,
2269
2364
  }
2270
- _msCharSet2To1 = _flipDict(_msCharSet1To2)
2365
+ _msCharSet2To1: dict[int, int] = _flipDict(_msCharSet1To2)
2271
2366
 
2272
2367
  # 1 <-> 2
2273
2368
 
2274
2369
 
2275
- def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value):
2370
+ def convertFontInfoValueForAttributeFromVersion1ToVersion2(
2371
+ attr: str, value: Any
2372
+ ) -> tuple[str, Any]:
2276
2373
  """
2277
2374
  Convert value from version 1 to version 2 format.
2278
2375
  Returns the new attribute name and the converted value.
@@ -2284,7 +2381,7 @@ def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value):
2284
2381
  value = int(value)
2285
2382
  if value is not None:
2286
2383
  if attr == "fontStyle":
2287
- v = _fontStyle1To2.get(value)
2384
+ v: Optional[Union[str, int]] = _fontStyle1To2.get(value)
2288
2385
  if v is None:
2289
2386
  raise UFOLibError(
2290
2387
  f"Cannot convert value ({value!r}) for attribute {attr}."
@@ -2308,7 +2405,9 @@ def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value):
2308
2405
  return attr, value
2309
2406
 
2310
2407
 
2311
- def convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value):
2408
+ def convertFontInfoValueForAttributeFromVersion2ToVersion1(
2409
+ attr: str, value: Any
2410
+ ) -> tuple[str, Any]:
2312
2411
  """
2313
2412
  Convert value from version 2 to version 1 format.
2314
2413
  Returns the new attribute name and the converted value.
@@ -2325,7 +2424,7 @@ def convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value):
2325
2424
  return attr, value
2326
2425
 
2327
2426
 
2328
- def _convertFontInfoDataVersion1ToVersion2(data):
2427
+ def _convertFontInfoDataVersion1ToVersion2(data: dict[str, Any]) -> dict[str, Any]:
2329
2428
  converted = {}
2330
2429
  for attr, value in list(data.items()):
2331
2430
  # FontLab gives -1 for the weightValue
@@ -2349,7 +2448,7 @@ def _convertFontInfoDataVersion1ToVersion2(data):
2349
2448
  return converted
2350
2449
 
2351
2450
 
2352
- def _convertFontInfoDataVersion2ToVersion1(data):
2451
+ def _convertFontInfoDataVersion2ToVersion1(data: dict[str, Any]) -> dict[str, Any]:
2353
2452
  converted = {}
2354
2453
  for attr, value in list(data.items()):
2355
2454
  newAttr, newValue = convertFontInfoValueForAttributeFromVersion2ToVersion1(
@@ -2370,16 +2469,16 @@ def _convertFontInfoDataVersion2ToVersion1(data):
2370
2469
 
2371
2470
  # 2 <-> 3
2372
2471
 
2373
- _ufo2To3NonNegativeInt = {
2472
+ _ufo2To3NonNegativeInt: set[str] = {
2374
2473
  "versionMinor",
2375
2474
  "openTypeHeadLowestRecPPEM",
2376
2475
  "openTypeOS2WinAscent",
2377
2476
  "openTypeOS2WinDescent",
2378
2477
  }
2379
- _ufo2To3NonNegativeIntOrFloat = {
2478
+ _ufo2To3NonNegativeIntOrFloat: set[str] = {
2380
2479
  "unitsPerEm",
2381
2480
  }
2382
- _ufo2To3FloatToInt = {
2481
+ _ufo2To3FloatToInt: set[str] = {
2383
2482
  "openTypeHeadLowestRecPPEM",
2384
2483
  "openTypeHheaAscender",
2385
2484
  "openTypeHheaDescender",
@@ -2407,7 +2506,9 @@ _ufo2To3FloatToInt = {
2407
2506
  }
2408
2507
 
2409
2508
 
2410
- def convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value):
2509
+ def convertFontInfoValueForAttributeFromVersion2ToVersion3(
2510
+ attr: str, value: Any
2511
+ ) -> tuple[str, Any]:
2411
2512
  """
2412
2513
  Convert value from version 2 to version 3 format.
2413
2514
  Returns the new attribute name and the converted value.
@@ -2435,7 +2536,9 @@ def convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value):
2435
2536
  return attr, value
2436
2537
 
2437
2538
 
2438
- def convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value):
2539
+ def convertFontInfoValueForAttributeFromVersion3ToVersion2(
2540
+ attr: str, value: Any
2541
+ ) -> tuple[str, Any]:
2439
2542
  """
2440
2543
  Convert value from version 3 to version 2 format.
2441
2544
  Returns the new attribute name and the converted value.
@@ -2444,7 +2547,7 @@ def convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value):
2444
2547
  return attr, value
2445
2548
 
2446
2549
 
2447
- def _convertFontInfoDataVersion3ToVersion2(data):
2550
+ def _convertFontInfoDataVersion3ToVersion2(data: dict[str, Any]) -> dict[str, Any]:
2448
2551
  converted = {}
2449
2552
  for attr, value in list(data.items()):
2450
2553
  newAttr, newValue = convertFontInfoValueForAttributeFromVersion3ToVersion2(
@@ -2456,7 +2559,7 @@ def _convertFontInfoDataVersion3ToVersion2(data):
2456
2559
  return converted
2457
2560
 
2458
2561
 
2459
- def _convertFontInfoDataVersion2ToVersion3(data):
2562
+ def _convertFontInfoDataVersion2ToVersion3(data: dict[str, Any]) -> dict[str, Any]:
2460
2563
  converted = {}
2461
2564
  for attr, value in list(data.items()):
2462
2565
  attr, value = convertFontInfoValueForAttributeFromVersion2ToVersion3(