fonttools 4.55.4__cp313-cp313-musllinux_1_2_aarch64.whl → 4.61.1__cp313-cp313-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.
Files changed (140) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/annotations.py +30 -0
  3. fontTools/cffLib/CFF2ToCFF.py +65 -10
  4. fontTools/cffLib/__init__.py +61 -26
  5. fontTools/cffLib/specializer.py +4 -1
  6. fontTools/cffLib/transforms.py +11 -6
  7. fontTools/config/__init__.py +15 -0
  8. fontTools/cu2qu/cu2qu.c +6567 -5579
  9. fontTools/cu2qu/cu2qu.cpython-313-aarch64-linux-musl.so +0 -0
  10. fontTools/cu2qu/cu2qu.py +36 -4
  11. fontTools/cu2qu/ufo.py +14 -0
  12. fontTools/designspaceLib/__init__.py +8 -3
  13. fontTools/designspaceLib/statNames.py +14 -7
  14. fontTools/feaLib/ast.py +24 -15
  15. fontTools/feaLib/builder.py +139 -66
  16. fontTools/feaLib/error.py +1 -1
  17. fontTools/feaLib/lexer.c +7038 -7995
  18. fontTools/feaLib/lexer.cpython-313-aarch64-linux-musl.so +0 -0
  19. fontTools/feaLib/parser.py +75 -40
  20. fontTools/feaLib/variableScalar.py +6 -1
  21. fontTools/fontBuilder.py +50 -44
  22. fontTools/merge/__init__.py +1 -1
  23. fontTools/merge/cmap.py +33 -1
  24. fontTools/merge/tables.py +12 -1
  25. fontTools/misc/bezierTools.c +14913 -17013
  26. fontTools/misc/bezierTools.cpython-313-aarch64-linux-musl.so +0 -0
  27. fontTools/misc/bezierTools.py +4 -1
  28. fontTools/misc/configTools.py +3 -1
  29. fontTools/misc/enumTools.py +23 -0
  30. fontTools/misc/etree.py +4 -27
  31. fontTools/misc/filesystem/__init__.py +68 -0
  32. fontTools/misc/filesystem/_base.py +134 -0
  33. fontTools/misc/filesystem/_copy.py +45 -0
  34. fontTools/misc/filesystem/_errors.py +54 -0
  35. fontTools/misc/filesystem/_info.py +75 -0
  36. fontTools/misc/filesystem/_osfs.py +164 -0
  37. fontTools/misc/filesystem/_path.py +67 -0
  38. fontTools/misc/filesystem/_subfs.py +92 -0
  39. fontTools/misc/filesystem/_tempfs.py +34 -0
  40. fontTools/misc/filesystem/_tools.py +34 -0
  41. fontTools/misc/filesystem/_walk.py +55 -0
  42. fontTools/misc/filesystem/_zipfs.py +204 -0
  43. fontTools/misc/fixedTools.py +1 -1
  44. fontTools/misc/loggingTools.py +1 -1
  45. fontTools/misc/psCharStrings.py +17 -2
  46. fontTools/misc/sstruct.py +2 -6
  47. fontTools/misc/symfont.py +6 -8
  48. fontTools/misc/testTools.py +5 -1
  49. fontTools/misc/textTools.py +4 -2
  50. fontTools/misc/visitor.py +32 -16
  51. fontTools/misc/xmlWriter.py +44 -8
  52. fontTools/mtiLib/__init__.py +1 -3
  53. fontTools/otlLib/builder.py +402 -155
  54. fontTools/otlLib/optimize/gpos.py +49 -63
  55. fontTools/pens/filterPen.py +218 -26
  56. fontTools/pens/momentsPen.c +5514 -5584
  57. fontTools/pens/momentsPen.cpython-313-aarch64-linux-musl.so +0 -0
  58. fontTools/pens/pointPen.py +61 -18
  59. fontTools/pens/roundingPen.py +2 -2
  60. fontTools/pens/t2CharStringPen.py +31 -11
  61. fontTools/qu2cu/qu2cu.c +6581 -6168
  62. fontTools/qu2cu/qu2cu.cpython-313-aarch64-linux-musl.so +0 -0
  63. fontTools/subset/__init__.py +283 -25
  64. fontTools/subset/svg.py +2 -3
  65. fontTools/ttLib/__init__.py +4 -0
  66. fontTools/ttLib/__main__.py +47 -8
  67. fontTools/ttLib/removeOverlaps.py +7 -5
  68. fontTools/ttLib/reorderGlyphs.py +8 -7
  69. fontTools/ttLib/sfnt.py +11 -9
  70. fontTools/ttLib/tables/D__e_b_g.py +20 -2
  71. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  72. fontTools/ttLib/tables/S__i_l_f.py +2 -2
  73. fontTools/ttLib/tables/T_S_I__0.py +14 -3
  74. fontTools/ttLib/tables/T_S_I__1.py +2 -5
  75. fontTools/ttLib/tables/T_S_I__5.py +18 -7
  76. fontTools/ttLib/tables/__init__.py +1 -0
  77. fontTools/ttLib/tables/_a_v_a_r.py +12 -3
  78. fontTools/ttLib/tables/_c_m_a_p.py +20 -7
  79. fontTools/ttLib/tables/_c_v_t.py +3 -2
  80. fontTools/ttLib/tables/_f_p_g_m.py +3 -1
  81. fontTools/ttLib/tables/_g_l_y_f.py +45 -21
  82. fontTools/ttLib/tables/_g_v_a_r.py +67 -19
  83. fontTools/ttLib/tables/_h_d_m_x.py +4 -4
  84. fontTools/ttLib/tables/_h_m_t_x.py +7 -3
  85. fontTools/ttLib/tables/_l_o_c_a.py +2 -2
  86. fontTools/ttLib/tables/_n_a_m_e.py +11 -6
  87. fontTools/ttLib/tables/_p_o_s_t.py +9 -7
  88. fontTools/ttLib/tables/otBase.py +5 -12
  89. fontTools/ttLib/tables/otConverters.py +5 -2
  90. fontTools/ttLib/tables/otData.py +1 -1
  91. fontTools/ttLib/tables/otTables.py +33 -30
  92. fontTools/ttLib/tables/otTraverse.py +2 -1
  93. fontTools/ttLib/tables/sbixStrike.py +3 -3
  94. fontTools/ttLib/ttFont.py +666 -120
  95. fontTools/ttLib/ttGlyphSet.py +0 -10
  96. fontTools/ttLib/woff2.py +10 -13
  97. fontTools/ttx.py +13 -1
  98. fontTools/ufoLib/__init__.py +300 -202
  99. fontTools/ufoLib/converters.py +103 -30
  100. fontTools/ufoLib/errors.py +8 -0
  101. fontTools/ufoLib/etree.py +1 -1
  102. fontTools/ufoLib/filenames.py +171 -106
  103. fontTools/ufoLib/glifLib.py +303 -205
  104. fontTools/ufoLib/kerning.py +98 -48
  105. fontTools/ufoLib/utils.py +46 -15
  106. fontTools/ufoLib/validators.py +121 -99
  107. fontTools/unicodedata/Blocks.py +35 -20
  108. fontTools/unicodedata/Mirrored.py +446 -0
  109. fontTools/unicodedata/ScriptExtensions.py +63 -37
  110. fontTools/unicodedata/Scripts.py +173 -152
  111. fontTools/unicodedata/__init__.py +10 -2
  112. fontTools/varLib/__init__.py +198 -109
  113. fontTools/varLib/avar/__init__.py +0 -0
  114. fontTools/varLib/avar/__main__.py +72 -0
  115. fontTools/varLib/avar/build.py +79 -0
  116. fontTools/varLib/avar/map.py +108 -0
  117. fontTools/varLib/avar/plan.py +1004 -0
  118. fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
  119. fontTools/varLib/avarPlanner.py +3 -999
  120. fontTools/varLib/featureVars.py +21 -7
  121. fontTools/varLib/hvar.py +113 -0
  122. fontTools/varLib/instancer/__init__.py +180 -65
  123. fontTools/varLib/interpolatableHelpers.py +3 -0
  124. fontTools/varLib/iup.c +7564 -6903
  125. fontTools/varLib/iup.cpython-313-aarch64-linux-musl.so +0 -0
  126. fontTools/varLib/models.py +17 -2
  127. fontTools/varLib/mutator.py +11 -0
  128. fontTools/varLib/varStore.py +10 -38
  129. fontTools/voltLib/__main__.py +206 -0
  130. fontTools/voltLib/ast.py +4 -0
  131. fontTools/voltLib/parser.py +16 -8
  132. fontTools/voltLib/voltToFea.py +347 -166
  133. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/METADATA +269 -1410
  134. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/RECORD +318 -294
  135. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/WHEEL +1 -1
  136. fonttools-4.61.1.dist-info/licenses/LICENSE.external +388 -0
  137. {fonttools-4.55.4.data → fonttools-4.61.1.data}/data/share/man/man1/ttx.1 +0 -0
  138. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/entry_points.txt +0 -0
  139. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info/licenses}/LICENSE +0 -0
  140. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/top_level.txt +0 -0
@@ -32,30 +32,57 @@ Value conversion functions are available for converting
32
32
  - :func:`.convertFontInfoValueForAttributeFromVersion3ToVersion2`
33
33
  """
34
34
 
35
- import os
36
- from copy import deepcopy
37
- from os import fsdecode
35
+ from __future__ import annotations
36
+
37
+ import enum
38
38
  import logging
39
+ import os
39
40
  import zipfile
40
- import enum
41
41
  from collections import OrderedDict
42
- import fs
43
- import fs.base
44
- import fs.subfs
45
- import fs.errors
46
- import fs.copy
47
- import fs.osfs
48
- import fs.zipfs
49
- import fs.tempfs
50
- import fs.tools
42
+ from copy import deepcopy
43
+ from os import fsdecode
44
+ from typing import IO, TYPE_CHECKING, Any, Optional, Union, cast
45
+
46
+ from fontTools.misc import filesystem as fs
51
47
  from fontTools.misc import plistlib
52
- from fontTools.ufoLib.validators import *
53
- from fontTools.ufoLib.filenames import userNameToFileName
54
48
  from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
55
49
  from fontTools.ufoLib.errors import UFOLibError
56
- from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
50
+ from fontTools.ufoLib.filenames import userNameToFileName
51
+ from fontTools.ufoLib.utils import (
52
+ BaseFormatVersion,
53
+ normalizeFormatVersion,
54
+ numberTypes,
55
+ )
56
+ from fontTools.ufoLib.validators import *
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
+
81
+ # client code can check this to see if the upstream `fs` package is being used
82
+ haveFS = fs._haveFS
57
83
 
58
- __all__ = [
84
+ __all__: list[str] = [
85
+ "haveFS",
59
86
  "makeUFOPath",
60
87
  "UFOLibError",
61
88
  "UFOReader",
@@ -72,43 +99,37 @@ __all__ = [
72
99
  "convertFontInfoValueForAttributeFromVersion2ToVersion1",
73
100
  ]
74
101
 
75
- __version__ = "3.0.0"
102
+ __version__: str = "3.0.0"
76
103
 
77
104
 
78
- logger = logging.getLogger(__name__)
105
+ logger: Logger = logging.getLogger(__name__)
79
106
 
80
107
 
81
108
  # ---------
82
109
  # Constants
83
110
  # ---------
84
111
 
85
- DEFAULT_GLYPHS_DIRNAME = "glyphs"
86
- DATA_DIRNAME = "data"
87
- IMAGES_DIRNAME = "images"
88
- METAINFO_FILENAME = "metainfo.plist"
89
- FONTINFO_FILENAME = "fontinfo.plist"
90
- LIB_FILENAME = "lib.plist"
91
- GROUPS_FILENAME = "groups.plist"
92
- KERNING_FILENAME = "kerning.plist"
93
- FEATURES_FILENAME = "features.fea"
94
- LAYERCONTENTS_FILENAME = "layercontents.plist"
95
- 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"
96
123
 
97
- DEFAULT_LAYER_NAME = "public.default"
124
+ DEFAULT_LAYER_NAME: str = "public.default"
98
125
 
99
126
 
100
- class UFOFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
127
+ class UFOFormatVersion(BaseFormatVersion):
101
128
  FORMAT_1_0 = (1, 0)
102
129
  FORMAT_2_0 = (2, 0)
103
130
  FORMAT_3_0 = (3, 0)
104
131
 
105
132
 
106
- # python 3.11 doesn't like when a mixin overrides a dunder method like __str__
107
- # for some reasons it keep using Enum.__str__, see
108
- # https://github.com/fonttools/fonttools/pull/2655
109
- UFOFormatVersion.__str__ = _VersionTupleEnumMixin.__str__
110
-
111
-
112
133
  class UFOFileStructure(enum.Enum):
113
134
  ZIP = "zip"
114
135
  PACKAGE = "package"
@@ -120,7 +141,11 @@ class UFOFileStructure(enum.Enum):
120
141
 
121
142
 
122
143
  class _UFOBaseIO:
123
- def getFileModificationTime(self, path):
144
+ if TYPE_CHECKING:
145
+ fs: FS
146
+ _havePreviousFile: bool
147
+
148
+ def getFileModificationTime(self, path: PathStr) -> Optional[float]:
124
149
  """
125
150
  Returns the modification time for the file at the given path, as a
126
151
  floating point number giving the number of seconds since the epoch.
@@ -132,9 +157,11 @@ class _UFOBaseIO:
132
157
  except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound):
133
158
  return None
134
159
  else:
135
- return dt.timestamp()
160
+ if dt is not None:
161
+ return dt.timestamp()
162
+ return None
136
163
 
137
- def _getPlist(self, fileName, default=None):
164
+ def _getPlist(self, fileName: str, default: Optional[Any] = None) -> Any:
138
165
  """
139
166
  Read a property list relative to the UFO filesystem's root.
140
167
  Raises UFOLibError if the file is missing and default is None,
@@ -158,7 +185,7 @@ class _UFOBaseIO:
158
185
  # TODO(anthrotype): try to narrow this down a little
159
186
  raise UFOLibError(f"'{fileName}' could not be read on {self.fs}: {e}")
160
187
 
161
- def _writePlist(self, fileName, obj):
188
+ def _writePlist(self, fileName: str, obj: Any) -> None:
162
189
  """
163
190
  Write a property list to a file relative to the UFO filesystem's root.
164
191
 
@@ -184,7 +211,7 @@ class _UFOBaseIO:
184
211
  return
185
212
  self.fs.writebytes(fileName, data)
186
213
  else:
187
- with self.fs.openbin(fileName, mode="w") as fp:
214
+ with self.fs.open(fileName, mode="wb") as fp:
188
215
  try:
189
216
  plistlib.dump(obj, fp)
190
217
  except Exception as e:
@@ -204,7 +231,7 @@ class UFOReader(_UFOBaseIO):
204
231
  """Read the various components of a .ufo.
205
232
 
206
233
  Attributes:
207
- path: An `os.PathLike` object pointing to the .ufo.
234
+ path: An :class:`os.PathLike` object pointing to the .ufo.
208
235
  validate: A boolean indicating if the data read should be
209
236
  validated. Defaults to `True`.
210
237
 
@@ -212,15 +239,17 @@ class UFOReader(_UFOBaseIO):
212
239
  ``False`` to not validate the data.
213
240
  """
214
241
 
215
- def __init__(self, path, validate=True):
216
- 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__"):
217
245
  path = path.__fspath__()
218
246
 
219
247
  if isinstance(path, str):
220
248
  structure = _sniffFileStructure(path)
249
+ parentFS: FS
221
250
  try:
222
251
  if structure is UFOFileStructure.ZIP:
223
- parentFS = fs.zipfs.ZipFS(path, write=False, encoding="utf-8")
252
+ parentFS = fs.zipfs.ZipFS(path, write=False, encoding="utf-8") # type: ignore[abstract]
224
253
  else:
225
254
  parentFS = fs.osfs.OSFS(path)
226
255
  except fs.errors.CreateFailed as e:
@@ -238,7 +267,7 @@ class UFOReader(_UFOBaseIO):
238
267
  if len(rootDirs) == 1:
239
268
  # 'ClosingSubFS' ensures that the parent zip file is closed when
240
269
  # its root subdirectory is closed
241
- self.fs = parentFS.opendir(
270
+ self.fs: FS = parentFS.opendir(
242
271
  rootDirs[0], factory=fs.subfs.ClosingSubFS
243
272
  )
244
273
  else:
@@ -250,10 +279,10 @@ class UFOReader(_UFOBaseIO):
250
279
  self.fs = parentFS
251
280
  # when passed a path string, we make sure we close the newly opened fs
252
281
  # upon calling UFOReader.close method or context manager's __exit__
253
- self._shouldClose = True
282
+ self._shouldClose: bool = True
254
283
  self._fileStructure = structure
255
284
  elif isinstance(path, fs.base.FS):
256
- filesystem = path
285
+ filesystem: FS = path
257
286
  try:
258
287
  filesystem.check()
259
288
  except fs.errors.FilesystemClosed:
@@ -275,9 +304,9 @@ class UFOReader(_UFOBaseIO):
275
304
  "Expected a path string or fs.base.FS object, found '%s'"
276
305
  % type(path).__name__
277
306
  )
278
- self._path = fsdecode(path)
279
- self._validate = validate
280
- self._upConvertedKerningData = None
307
+ self._path: str = fsdecode(path)
308
+ self._validate: bool = validate
309
+ self._upConvertedKerningData: Optional[dict[str, Any]] = None
281
310
 
282
311
  try:
283
312
  self.readMetaInfo(validate=validate)
@@ -287,7 +316,7 @@ class UFOReader(_UFOBaseIO):
287
316
 
288
317
  # properties
289
318
 
290
- def _get_path(self):
319
+ def _get_path(self) -> str:
291
320
  import warnings
292
321
 
293
322
  warnings.warn(
@@ -297,9 +326,9 @@ class UFOReader(_UFOBaseIO):
297
326
  )
298
327
  return self._path
299
328
 
300
- path = property(_get_path, doc="The path of the UFO (DEPRECATED).")
329
+ path: property = property(_get_path, doc="The path of the UFO (DEPRECATED).")
301
330
 
302
- def _get_formatVersion(self):
331
+ def _get_formatVersion(self) -> int:
303
332
  import warnings
304
333
 
305
334
  warnings.warn(
@@ -315,16 +344,16 @@ class UFOReader(_UFOBaseIO):
315
344
  )
316
345
 
317
346
  @property
318
- def formatVersionTuple(self):
347
+ def formatVersionTuple(self) -> tuple[int, int]:
319
348
  """The (major, minor) format version of the UFO.
320
349
  This is determined by reading metainfo.plist during __init__.
321
350
  """
322
351
  return self._formatVersion
323
352
 
324
- def _get_fileStructure(self):
353
+ def _get_fileStructure(self) -> Any:
325
354
  return self._fileStructure
326
355
 
327
- fileStructure = property(
356
+ fileStructure: property = property(
328
357
  _get_fileStructure,
329
358
  doc=(
330
359
  "The file structure of the UFO: "
@@ -334,7 +363,7 @@ class UFOReader(_UFOBaseIO):
334
363
 
335
364
  # up conversion
336
365
 
337
- def _upConvertKerning(self, validate):
366
+ def _upConvertKerning(self, validate: bool) -> None:
338
367
  """
339
368
  Up convert kerning and groups in UFO 1 and 2.
340
369
  The data will be held internally until each bit of data
@@ -388,7 +417,7 @@ class UFOReader(_UFOBaseIO):
388
417
 
389
418
  # support methods
390
419
 
391
- def readBytesFromPath(self, path):
420
+ def readBytesFromPath(self, path: PathStr) -> Optional[bytes]:
392
421
  """
393
422
  Returns the bytes in the file at the given path.
394
423
  The path must be relative to the UFO's filesystem root.
@@ -399,7 +428,9 @@ class UFOReader(_UFOBaseIO):
399
428
  except fs.errors.ResourceNotFound:
400
429
  return None
401
430
 
402
- def getReadFileForPath(self, path, encoding=None):
431
+ def getReadFileForPath(
432
+ self, path: PathStr, encoding: Optional[str] = None
433
+ ) -> Optional[Union[IO[bytes], IO[str]]]:
403
434
  """
404
435
  Returns a file (or file-like) object for the file at the given path.
405
436
  The path must be relative to the UFO path.
@@ -412,7 +443,7 @@ class UFOReader(_UFOBaseIO):
412
443
  path = fsdecode(path)
413
444
  try:
414
445
  if encoding is None:
415
- return self.fs.openbin(path)
446
+ return self.fs.open(path, mode="rb")
416
447
  else:
417
448
  return self.fs.open(path, mode="r", encoding=encoding)
418
449
  except fs.errors.ResourceNotFound:
@@ -420,7 +451,7 @@ class UFOReader(_UFOBaseIO):
420
451
 
421
452
  # metainfo.plist
422
453
 
423
- def _readMetaInfo(self, validate=None):
454
+ def _readMetaInfo(self, validate: Optional[bool] = None) -> dict[str, Any]:
424
455
  """
425
456
  Read metainfo.plist and return raw data. Only used for internal operations.
426
457
 
@@ -462,7 +493,7 @@ class UFOReader(_UFOBaseIO):
462
493
  data["formatVersionTuple"] = formatVersion
463
494
  return data
464
495
 
465
- def readMetaInfo(self, validate=None):
496
+ def readMetaInfo(self, validate: Optional[bool] = None) -> None:
466
497
  """
467
498
  Read metainfo.plist and set formatVersion. Only used for internal operations.
468
499
 
@@ -474,7 +505,7 @@ class UFOReader(_UFOBaseIO):
474
505
 
475
506
  # groups.plist
476
507
 
477
- def _readGroups(self):
508
+ def _readGroups(self) -> dict[str, list[str]]:
478
509
  groups = self._getPlist(GROUPS_FILENAME, {})
479
510
  # remove any duplicate glyphs in a kerning group
480
511
  for groupName, glyphList in groups.items():
@@ -482,7 +513,7 @@ class UFOReader(_UFOBaseIO):
482
513
  groups[groupName] = list(OrderedDict.fromkeys(glyphList))
483
514
  return groups
484
515
 
485
- def readGroups(self, validate=None):
516
+ def readGroups(self, validate: Optional[bool] = None) -> dict[str, list[str]]:
486
517
  """
487
518
  Read groups.plist. Returns a dict.
488
519
  ``validate`` will validate the read data, by default it is set to the
@@ -493,7 +524,7 @@ class UFOReader(_UFOBaseIO):
493
524
  # handle up conversion
494
525
  if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
495
526
  self._upConvertKerning(validate)
496
- groups = self._upConvertedKerningData["groups"]
527
+ groups = cast(dict, self._upConvertedKerningData)["groups"]
497
528
  # normal
498
529
  else:
499
530
  groups = self._readGroups()
@@ -503,7 +534,9 @@ class UFOReader(_UFOBaseIO):
503
534
  raise UFOLibError(message)
504
535
  return groups
505
536
 
506
- def getKerningGroupConversionRenameMaps(self, validate=None):
537
+ def getKerningGroupConversionRenameMaps(
538
+ self, validate: Optional[bool] = None
539
+ ) -> KerningGroupRenameMaps:
507
540
  """
508
541
  Get maps defining the renaming that was done during any
509
542
  needed kerning group conversion. This method returns a
@@ -527,17 +560,17 @@ class UFOReader(_UFOBaseIO):
527
560
  # use the public group reader to force the load and
528
561
  # conversion of the data if it hasn't happened yet.
529
562
  self.readGroups(validate=validate)
530
- return self._upConvertedKerningData["groupRenameMaps"]
563
+ return cast(dict, self._upConvertedKerningData)["groupRenameMaps"]
531
564
 
532
565
  # fontinfo.plist
533
566
 
534
- def _readInfo(self, validate):
567
+ def _readInfo(self, validate: bool) -> dict[str, Any]:
535
568
  data = self._getPlist(FONTINFO_FILENAME, {})
536
569
  if validate and not isinstance(data, dict):
537
570
  raise UFOLibError("fontinfo.plist is not properly formatted.")
538
571
  return data
539
572
 
540
- def readInfo(self, info, validate=None):
573
+ def readInfo(self, info: Any, validate: Optional[bool] = None) -> None:
541
574
  """
542
575
  Read fontinfo.plist. It requires an object that allows
543
576
  setting attributes with names that follow the fontinfo.plist
@@ -596,11 +629,11 @@ class UFOReader(_UFOBaseIO):
596
629
 
597
630
  # kerning.plist
598
631
 
599
- def _readKerning(self):
632
+ def _readKerning(self) -> KerningNested:
600
633
  data = self._getPlist(KERNING_FILENAME, {})
601
634
  return data
602
635
 
603
- def readKerning(self, validate=None):
636
+ def readKerning(self, validate: Optional[bool] = None) -> KerningDict:
604
637
  """
605
638
  Read kerning.plist. Returns a dict.
606
639
 
@@ -612,7 +645,7 @@ class UFOReader(_UFOBaseIO):
612
645
  # handle up conversion
613
646
  if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
614
647
  self._upConvertKerning(validate)
615
- kerningNested = self._upConvertedKerningData["kerning"]
648
+ kerningNested = cast(dict, self._upConvertedKerningData)["kerning"]
616
649
  # normal
617
650
  else:
618
651
  kerningNested = self._readKerning()
@@ -630,7 +663,7 @@ class UFOReader(_UFOBaseIO):
630
663
 
631
664
  # lib.plist
632
665
 
633
- def readLib(self, validate=None):
666
+ def readLib(self, validate: Optional[bool] = None) -> dict[str, Any]:
634
667
  """
635
668
  Read lib.plist. Returns a dict.
636
669
 
@@ -648,20 +681,20 @@ class UFOReader(_UFOBaseIO):
648
681
 
649
682
  # features.fea
650
683
 
651
- def readFeatures(self):
684
+ def readFeatures(self) -> str:
652
685
  """
653
686
  Read features.fea. Return a string.
654
687
  The returned string is empty if the file is missing.
655
688
  """
656
689
  try:
657
- with self.fs.open(FEATURES_FILENAME, "r", encoding="utf-8") as f:
690
+ with self.fs.open(FEATURES_FILENAME, "r", encoding="utf-8-sig") as f:
658
691
  return f.read()
659
692
  except fs.errors.ResourceNotFound:
660
693
  return ""
661
694
 
662
695
  # glyph sets & layers
663
696
 
664
- def _readLayerContents(self, validate):
697
+ def _readLayerContents(self, validate: bool) -> list[tuple[str, str]]:
665
698
  """
666
699
  Rebuild the layer contents list by checking what glyphsets
667
700
  are available on disk.
@@ -677,7 +710,7 @@ class UFOReader(_UFOBaseIO):
677
710
  raise UFOLibError(error)
678
711
  return contents
679
712
 
680
- def getLayerNames(self, validate=None):
713
+ def getLayerNames(self, validate: Optional[bool] = None) -> list[str]:
681
714
  """
682
715
  Get the ordered layer names from layercontents.plist.
683
716
 
@@ -690,7 +723,7 @@ class UFOReader(_UFOBaseIO):
690
723
  layerNames = [layerName for layerName, directoryName in layerContents]
691
724
  return layerNames
692
725
 
693
- def getDefaultLayerName(self, validate=None):
726
+ def getDefaultLayerName(self, validate: Optional[bool] = None) -> str:
694
727
  """
695
728
  Get the default layer name from layercontents.plist.
696
729
 
@@ -706,7 +739,12 @@ class UFOReader(_UFOBaseIO):
706
739
  # this will already have been raised during __init__
707
740
  raise UFOLibError("The default layer is not defined in layercontents.plist.")
708
741
 
709
- 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:
710
748
  """
711
749
  Return the GlyphSet associated with the
712
750
  glyphs directory mapped to layerName
@@ -747,7 +785,9 @@ class UFOReader(_UFOBaseIO):
747
785
  expectContentsFile=True,
748
786
  )
749
787
 
750
- 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]]:
751
791
  """
752
792
  Return a dictionary that maps unicode values (ints) to
753
793
  lists of glyph names.
@@ -758,7 +798,7 @@ class UFOReader(_UFOBaseIO):
758
798
  layerName, validateRead=validate, validateWrite=True
759
799
  )
760
800
  allUnicodes = glyphSet.getUnicodes()
761
- cmap = {}
801
+ cmap: dict[int, list[str]] = {}
762
802
  for glyphName, unicodes in allUnicodes.items():
763
803
  for code in unicodes:
764
804
  if code in cmap:
@@ -769,7 +809,7 @@ class UFOReader(_UFOBaseIO):
769
809
 
770
810
  # /data
771
811
 
772
- def getDataDirectoryListing(self):
812
+ def getDataDirectoryListing(self) -> list[str]:
773
813
  """
774
814
  Returns a list of all files in the data directory.
775
815
  The returned paths will be relative to the UFO.
@@ -790,7 +830,7 @@ class UFOReader(_UFOBaseIO):
790
830
  except fs.errors.ResourceError:
791
831
  return []
792
832
 
793
- def getImageDirectoryListing(self, validate=None):
833
+ def getImageDirectoryListing(self, validate: Optional[bool] = None) -> list[str]:
794
834
  """
795
835
  Returns a list of all image file names in
796
836
  the images directory. Each of the images will
@@ -818,7 +858,7 @@ class UFOReader(_UFOBaseIO):
818
858
  # systems often have hidden directories
819
859
  continue
820
860
  if validate:
821
- with imagesFS.openbin(path.name) as fp:
861
+ with imagesFS.open(path.name, "rb") as fp:
822
862
  valid, error = pngValidator(fileObj=fp)
823
863
  if valid:
824
864
  result.append(path.name)
@@ -826,7 +866,7 @@ class UFOReader(_UFOBaseIO):
826
866
  result.append(path.name)
827
867
  return result
828
868
 
829
- def readData(self, fileName):
869
+ def readData(self, fileName: PathStr) -> bytes:
830
870
  """
831
871
  Return bytes for the file named 'fileName' inside the 'data/' directory.
832
872
  """
@@ -842,7 +882,7 @@ class UFOReader(_UFOBaseIO):
842
882
  raise UFOLibError(f"No data file named '{fileName}' on {self.fs}")
843
883
  return data
844
884
 
845
- def readImage(self, fileName, validate=None):
885
+ def readImage(self, fileName: PathStr, validate: Optional[bool] = None) -> bytes:
846
886
  """
847
887
  Return image data for the file named fileName.
848
888
 
@@ -871,14 +911,14 @@ class UFOReader(_UFOBaseIO):
871
911
  raise UFOLibError(error)
872
912
  return data
873
913
 
874
- def close(self):
914
+ def close(self) -> None:
875
915
  if self._shouldClose:
876
916
  self.fs.close()
877
917
 
878
- def __enter__(self):
918
+ def __enter__(self) -> UFOReader:
879
919
  return self
880
920
 
881
- def __exit__(self, exc_type, exc_value, exc_tb):
921
+ def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None:
882
922
  self.close()
883
923
 
884
924
 
@@ -891,7 +931,7 @@ class UFOWriter(UFOReader):
891
931
  """Write the various components of a .ufo.
892
932
 
893
933
  Attributes:
894
- path: An `os.PathLike` object pointing to the .ufo.
934
+ path: An :class:`os.PathLike` object pointing to the .ufo.
895
935
  formatVersion: the UFO format version as a tuple of integers (major, minor),
896
936
  or as a single integer for the major digit only (minor is implied to be 0).
897
937
  By default, the latest formatVersion will be used; currently it is 3.0,
@@ -913,14 +953,14 @@ class UFOWriter(UFOReader):
913
953
 
914
954
  def __init__(
915
955
  self,
916
- path,
917
- formatVersion=None,
918
- fileCreator="com.github.fonttools.ufoLib",
919
- structure=None,
920
- validate=True,
921
- ):
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:
922
962
  try:
923
- formatVersion = UFOFormatVersion(formatVersion)
963
+ formatVersion = normalizeFormatVersion(formatVersion, UFOFormatVersion)
924
964
  except ValueError as e:
925
965
  from fontTools.ufoLib.errors import UnsupportedUFOFormat
926
966
 
@@ -966,8 +1006,8 @@ class UFOWriter(UFOReader):
966
1006
  # we can't write a zip in-place, so we have to copy its
967
1007
  # contents to a temporary location and work from there, then
968
1008
  # upon closing UFOWriter we create the final zip file
969
- parentFS = fs.tempfs.TempFS()
970
- 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]
971
1011
  fs.copy.copy_fs(origFS, parentFS)
972
1012
  # if output path is an existing zip, we require that it contains
973
1013
  # one, and only one, root directory (with arbitrary name), in turn
@@ -984,25 +1024,23 @@ class UFOWriter(UFOReader):
984
1024
  % len(rootDirs)
985
1025
  )
986
1026
  else:
987
- # 'ClosingSubFS' ensures that the parent filesystem is closed
988
- # when its root subdirectory is closed
989
- self.fs = parentFS.opendir(
990
- rootDirs[0], factory=fs.subfs.ClosingSubFS
991
- )
1027
+ rootDir = rootDirs[0]
992
1028
  else:
993
1029
  # if the output zip file didn't exist, we create the root folder;
994
1030
  # we name it the same as input 'path', but with '.ufo' extension
995
1031
  rootDir = os.path.splitext(os.path.basename(path))[0] + ".ufo"
996
- parentFS = fs.zipfs.ZipFS(path, write=True, encoding="utf-8")
1032
+ parentFS = fs.zipfs.ZipFS(path, write=True, encoding="utf-8") # type: ignore[abstract]
997
1033
  parentFS.makedir(rootDir)
998
- self.fs = parentFS.opendir(rootDir, factory=fs.subfs.ClosingSubFS)
1034
+ # 'ClosingSubFS' ensures that the parent filesystem is closed
1035
+ # when its root subdirectory is closed
1036
+ self.fs = parentFS.opendir(rootDir, factory=fs.subfs.ClosingSubFS)
999
1037
  else:
1000
1038
  self.fs = fs.osfs.OSFS(path, create=True)
1001
1039
  self._fileStructure = structure
1002
1040
  self._havePreviousFile = havePreviousFile
1003
1041
  self._shouldClose = True
1004
1042
  elif isinstance(path, fs.base.FS):
1005
- filesystem = path
1043
+ filesystem: FS = path
1006
1044
  try:
1007
1045
  filesystem.check()
1008
1046
  except fs.errors.FilesystemClosed:
@@ -1037,7 +1075,7 @@ class UFOWriter(UFOReader):
1037
1075
  self._path = fsdecode(path)
1038
1076
  self._formatVersion = formatVersion
1039
1077
  self._fileCreator = fileCreator
1040
- self._downConversionKerningData = None
1078
+ self._downConversionKerningData: Optional[KerningGroupRenameMaps] = None
1041
1079
  self._validate = validate
1042
1080
  # if the file already exists, get the format version.
1043
1081
  # this will be needed for up and down conversion.
@@ -1055,7 +1093,7 @@ class UFOWriter(UFOReader):
1055
1093
  "that is trying to be written. This is not supported."
1056
1094
  )
1057
1095
  # handle the layer contents
1058
- self.layerContents = {}
1096
+ self.layerContents: Union[dict[str, str], OrderedDict[str, str]] = {}
1059
1097
  if previousFormatVersion is not None and previousFormatVersion.major >= 3:
1060
1098
  # already exists
1061
1099
  self.layerContents = OrderedDict(self._readLayerContents(validate))
@@ -1069,17 +1107,19 @@ class UFOWriter(UFOReader):
1069
1107
 
1070
1108
  # properties
1071
1109
 
1072
- def _get_fileCreator(self):
1110
+ def _get_fileCreator(self) -> str:
1073
1111
  return self._fileCreator
1074
1112
 
1075
- fileCreator = property(
1113
+ fileCreator: property = property(
1076
1114
  _get_fileCreator,
1077
1115
  doc="The file creator of the UFO. This is set into metainfo.plist during __init__.",
1078
1116
  )
1079
1117
 
1080
1118
  # support methods for file system interaction
1081
1119
 
1082
- def copyFromReader(self, reader, sourcePath, destPath):
1120
+ def copyFromReader(
1121
+ self, reader: UFOReader, sourcePath: PathStr, destPath: PathStr
1122
+ ) -> None:
1083
1123
  """
1084
1124
  Copy the sourcePath in the provided UFOReader to destPath
1085
1125
  in this writer. The paths must be relative. This works with
@@ -1102,7 +1142,7 @@ class UFOWriter(UFOReader):
1102
1142
  else:
1103
1143
  fs.copy.copy_file(reader.fs, sourcePath, self.fs, destPath)
1104
1144
 
1105
- def writeBytesToPath(self, path, data):
1145
+ def writeBytesToPath(self, path: PathStr, data: bytes) -> None:
1106
1146
  """
1107
1147
  Write bytes to a path relative to the UFO filesystem's root.
1108
1148
  If writing to an existing UFO, check to see if data matches the data
@@ -1122,7 +1162,12 @@ class UFOWriter(UFOReader):
1122
1162
  self.fs.makedirs(fs.path.dirname(path), recreate=True)
1123
1163
  self.fs.writebytes(path, data)
1124
1164
 
1125
- 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]]:
1126
1171
  """
1127
1172
  Returns a file (or file-like) object for the
1128
1173
  file at the given path. The path must be relative
@@ -1145,9 +1190,12 @@ class UFOWriter(UFOReader):
1145
1190
  self.fs.makedirs(fs.path.dirname(path), recreate=True)
1146
1191
  return self.fs.open(path, mode=mode, encoding=encoding)
1147
1192
  except fs.errors.ResourceError as e:
1148
- 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
1149
1195
 
1150
- def removePath(self, path, force=False, removeEmptyParents=True):
1196
+ def removePath(
1197
+ self, path: PathStr, force: bool = False, removeEmptyParents: bool = True
1198
+ ) -> None:
1151
1199
  """
1152
1200
  Remove the file (or directory) at path. The path
1153
1201
  must be relative to the UFO.
@@ -1174,7 +1222,7 @@ class UFOWriter(UFOReader):
1174
1222
 
1175
1223
  # UFO mod time
1176
1224
 
1177
- def setModificationTime(self):
1225
+ def setModificationTime(self) -> None:
1178
1226
  """
1179
1227
  Set the UFO modification time to the current time.
1180
1228
  This is never called automatically. It is up to the
@@ -1190,7 +1238,7 @@ class UFOWriter(UFOReader):
1190
1238
 
1191
1239
  # metainfo.plist
1192
1240
 
1193
- def _writeMetaInfo(self):
1241
+ def _writeMetaInfo(self) -> None:
1194
1242
  metaInfo = dict(
1195
1243
  creator=self._fileCreator,
1196
1244
  formatVersion=self._formatVersion.major,
@@ -1201,7 +1249,7 @@ class UFOWriter(UFOReader):
1201
1249
 
1202
1250
  # groups.plist
1203
1251
 
1204
- def setKerningGroupConversionRenameMaps(self, maps):
1252
+ def setKerningGroupConversionRenameMaps(self, maps: KerningGroupRenameMaps) -> None:
1205
1253
  """
1206
1254
  Set maps defining the renaming that should be done
1207
1255
  when writing groups and kerning in UFO 1 and UFO 2.
@@ -1226,7 +1274,9 @@ class UFOWriter(UFOReader):
1226
1274
  remap[dataName] = writeName
1227
1275
  self._downConversionKerningData = dict(groupRenameMap=remap)
1228
1276
 
1229
- def writeGroups(self, groups, validate=None):
1277
+ def writeGroups(
1278
+ self, groups: KerningGroups, validate: Optional[bool] = None
1279
+ ) -> None:
1230
1280
  """
1231
1281
  Write groups.plist. This method requires a
1232
1282
  dict of glyph groups as an argument.
@@ -1281,7 +1331,7 @@ class UFOWriter(UFOReader):
1281
1331
 
1282
1332
  # fontinfo.plist
1283
1333
 
1284
- def writeInfo(self, info, validate=None):
1334
+ def writeInfo(self, info: Any, validate: Optional[bool] = None) -> None:
1285
1335
  """
1286
1336
  Write info.plist. This method requires an object
1287
1337
  that supports getting attributes that follow the
@@ -1327,7 +1377,9 @@ class UFOWriter(UFOReader):
1327
1377
 
1328
1378
  # kerning.plist
1329
1379
 
1330
- def writeKerning(self, kerning, validate=None):
1380
+ def writeKerning(
1381
+ self, kerning: KerningDict, validate: Optional[bool] = None
1382
+ ) -> None:
1331
1383
  """
1332
1384
  Write kerning.plist. This method requires a
1333
1385
  dict of kerning pairs as an argument.
@@ -1371,7 +1423,7 @@ class UFOWriter(UFOReader):
1371
1423
  remappedKerning[side1, side2] = value
1372
1424
  kerning = remappedKerning
1373
1425
  # pack and write
1374
- kerningDict = {}
1426
+ kerningDict: KerningNested = {}
1375
1427
  for left, right in kerning.keys():
1376
1428
  value = kerning[left, right]
1377
1429
  if left not in kerningDict:
@@ -1384,7 +1436,7 @@ class UFOWriter(UFOReader):
1384
1436
 
1385
1437
  # lib.plist
1386
1438
 
1387
- def writeLib(self, libDict, validate=None):
1439
+ def writeLib(self, libDict: LibDict, validate: Optional[bool] = None) -> None:
1388
1440
  """
1389
1441
  Write lib.plist. This method requires a
1390
1442
  lib dict as an argument.
@@ -1405,7 +1457,7 @@ class UFOWriter(UFOReader):
1405
1457
 
1406
1458
  # features.fea
1407
1459
 
1408
- def writeFeatures(self, features, validate=None):
1460
+ def writeFeatures(self, features: str, validate: Optional[bool] = None) -> None:
1409
1461
  """
1410
1462
  Write features.fea. This method requires a
1411
1463
  features string as an argument.
@@ -1424,7 +1476,9 @@ class UFOWriter(UFOReader):
1424
1476
 
1425
1477
  # glyph sets & layers
1426
1478
 
1427
- def writeLayerContents(self, layerOrder=None, validate=None):
1479
+ def writeLayerContents(
1480
+ self, layerOrder: LayerOrderList = None, validate: Optional[bool] = None
1481
+ ) -> None:
1428
1482
  """
1429
1483
  Write the layercontents.plist file. This method *must* be called
1430
1484
  after all glyph sets have been written.
@@ -1434,7 +1488,7 @@ class UFOWriter(UFOReader):
1434
1488
  if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
1435
1489
  return
1436
1490
  if layerOrder is not None:
1437
- newOrder = []
1491
+ newOrder: list[Optional[str]] = []
1438
1492
  for layerName in layerOrder:
1439
1493
  if layerName is None:
1440
1494
  layerName = DEFAULT_LAYER_NAME
@@ -1447,11 +1501,13 @@ class UFOWriter(UFOReader):
1447
1501
  "The layer order content does not match the glyph sets that have been created."
1448
1502
  )
1449
1503
  layerContents = [
1450
- (layerName, self.layerContents[layerName]) for layerName in layerOrder
1504
+ (layerName, self.layerContents[layerName])
1505
+ for layerName in layerOrder
1506
+ if layerName is not None
1451
1507
  ]
1452
1508
  self._writePlist(LAYERCONTENTS_FILENAME, layerContents)
1453
1509
 
1454
- def _findDirectoryForLayerName(self, layerName):
1510
+ def _findDirectoryForLayerName(self, layerName: Optional[str]) -> str:
1455
1511
  foundDirectory = None
1456
1512
  for existingLayerName, directoryName in list(self.layerContents.items()):
1457
1513
  if layerName is None and directoryName == DEFAULT_GLYPHS_DIRNAME:
@@ -1467,15 +1523,15 @@ class UFOWriter(UFOReader):
1467
1523
  )
1468
1524
  return foundDirectory
1469
1525
 
1470
- def getGlyphSet(
1526
+ def getGlyphSet( # type: ignore[override]
1471
1527
  self,
1472
- layerName=None,
1473
- defaultLayer=True,
1474
- glyphNameToFileNameFunc=None,
1475
- validateRead=None,
1476
- validateWrite=None,
1477
- expectContentsFile=False,
1478
- ):
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:
1479
1535
  """
1480
1536
  Return the GlyphSet object associated with the
1481
1537
  appropriate glyph directory in the .ufo.
@@ -1535,11 +1591,11 @@ class UFOWriter(UFOReader):
1535
1591
 
1536
1592
  def _getDefaultGlyphSet(
1537
1593
  self,
1538
- validateRead,
1539
- validateWrite,
1540
- glyphNameToFileNameFunc=None,
1541
- expectContentsFile=False,
1542
- ):
1594
+ validateRead: bool,
1595
+ validateWrite: bool,
1596
+ glyphNameToFileNameFunc: GlyphNameToFileNameFunc = None,
1597
+ expectContentsFile: bool = False,
1598
+ ) -> GlyphSet:
1543
1599
  from fontTools.ufoLib.glifLib import GlyphSet
1544
1600
 
1545
1601
  glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True)
@@ -1554,13 +1610,13 @@ class UFOWriter(UFOReader):
1554
1610
 
1555
1611
  def _getGlyphSetFormatVersion3(
1556
1612
  self,
1557
- validateRead,
1558
- validateWrite,
1559
- layerName=None,
1560
- defaultLayer=True,
1561
- glyphNameToFileNameFunc=None,
1562
- expectContentsFile=False,
1563
- ):
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:
1564
1620
  from fontTools.ufoLib.glifLib import GlyphSet
1565
1621
 
1566
1622
  # if the default flag is on, make sure that the default in the file
@@ -1578,6 +1634,11 @@ class UFOWriter(UFOReader):
1578
1634
  raise UFOLibError(
1579
1635
  "The layer name is already mapped to a non-default layer."
1580
1636
  )
1637
+
1638
+ # handle layerName is None to avoid MyPy errors
1639
+ if layerName is None:
1640
+ raise TypeError("'leyerName' cannot be None.")
1641
+
1581
1642
  # get an existing directory name
1582
1643
  if layerName in self.layerContents:
1583
1644
  directory = self.layerContents[layerName]
@@ -1606,7 +1667,12 @@ class UFOWriter(UFOReader):
1606
1667
  expectContentsFile=expectContentsFile,
1607
1668
  )
1608
1669
 
1609
- 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:
1610
1676
  """
1611
1677
  Rename a glyph set.
1612
1678
 
@@ -1620,7 +1686,7 @@ class UFOWriter(UFOReader):
1620
1686
  return
1621
1687
  # the new and old names can be the same
1622
1688
  # as long as the default is being switched
1623
- if layerName == newLayerName:
1689
+ if layerName is not None and layerName == newLayerName:
1624
1690
  # if the default is off and the layer is already not the default, skip
1625
1691
  if (
1626
1692
  self.layerContents[layerName] != DEFAULT_GLYPHS_DIRNAME
@@ -1649,12 +1715,13 @@ class UFOWriter(UFOReader):
1649
1715
  newLayerName, existing=existing, prefix="glyphs."
1650
1716
  )
1651
1717
  # update the internal mapping
1652
- del self.layerContents[layerName]
1718
+ if layerName is not None:
1719
+ del self.layerContents[layerName]
1653
1720
  self.layerContents[newLayerName] = newDirectory
1654
1721
  # do the file system copy
1655
1722
  self.fs.movedir(oldDirectory, newDirectory, create=True)
1656
1723
 
1657
- def deleteGlyphSet(self, layerName):
1724
+ def deleteGlyphSet(self, layerName: Optional[str]) -> None:
1658
1725
  """
1659
1726
  Remove the glyph set matching layerName.
1660
1727
  """
@@ -1664,16 +1731,17 @@ class UFOWriter(UFOReader):
1664
1731
  return
1665
1732
  foundDirectory = self._findDirectoryForLayerName(layerName)
1666
1733
  self.removePath(foundDirectory, removeEmptyParents=False)
1667
- del self.layerContents[layerName]
1734
+ if layerName is not None:
1735
+ del self.layerContents[layerName]
1668
1736
 
1669
- def writeData(self, fileName, data):
1737
+ def writeData(self, fileName: PathStr, data: bytes) -> None:
1670
1738
  """
1671
1739
  Write data to fileName in the 'data' directory.
1672
1740
  The data must be a bytes string.
1673
1741
  """
1674
1742
  self.writeBytesToPath(f"{DATA_DIRNAME}/{fsdecode(fileName)}", data)
1675
1743
 
1676
- def removeData(self, fileName):
1744
+ def removeData(self, fileName: PathStr) -> None:
1677
1745
  """
1678
1746
  Remove the file named fileName from the data directory.
1679
1747
  """
@@ -1681,7 +1749,12 @@ class UFOWriter(UFOReader):
1681
1749
 
1682
1750
  # /images
1683
1751
 
1684
- 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:
1685
1758
  """
1686
1759
  Write data to fileName in the images directory.
1687
1760
  The data must be a valid PNG.
@@ -1699,7 +1772,11 @@ class UFOWriter(UFOReader):
1699
1772
  raise UFOLibError(error)
1700
1773
  self.writeBytesToPath(f"{IMAGES_DIRNAME}/{fileName}", data)
1701
1774
 
1702
- 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'?
1703
1780
  """
1704
1781
  Remove the file named fileName from the
1705
1782
  images directory.
@@ -1710,7 +1787,13 @@ class UFOWriter(UFOReader):
1710
1787
  )
1711
1788
  self.removePath(f"{IMAGES_DIRNAME}/{fsdecode(fileName)}")
1712
1789
 
1713
- 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:
1714
1797
  """
1715
1798
  Copy the sourceFileName in the provided UFOReader to destFileName
1716
1799
  in this writer. This uses the most memory efficient method possible
@@ -1726,12 +1809,12 @@ class UFOWriter(UFOReader):
1726
1809
  destPath = f"{IMAGES_DIRNAME}/{fsdecode(destFileName)}"
1727
1810
  self.copyFromReader(reader, sourcePath, destPath)
1728
1811
 
1729
- def close(self):
1812
+ def close(self) -> None:
1730
1813
  if self._havePreviousFile and self._fileStructure is UFOFileStructure.ZIP:
1731
1814
  # if we are updating an existing zip file, we can now compress the
1732
1815
  # contents of the temporary filesystem in the destination path
1733
1816
  rootDir = os.path.splitext(os.path.basename(self._path))[0] + ".ufo"
1734
- 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]
1735
1818
  fs.copy.copy_fs(self.fs, destFS.makedir(rootDir))
1736
1819
  super().close()
1737
1820
 
@@ -1745,7 +1828,7 @@ UFOReaderWriter = UFOWriter
1745
1828
  # ----------------
1746
1829
 
1747
1830
 
1748
- def _sniffFileStructure(ufo_path):
1831
+ def _sniffFileStructure(ufo_path: PathStr) -> UFOFileStructure:
1749
1832
  """Return UFOFileStructure.ZIP if the UFO at path 'ufo_path' (str)
1750
1833
  is a zip file, else return UFOFileStructure.PACKAGE if 'ufo_path' is a
1751
1834
  directory.
@@ -1764,7 +1847,7 @@ def _sniffFileStructure(ufo_path):
1764
1847
  raise UFOLibError("No such file or directory: '%s'" % ufo_path)
1765
1848
 
1766
1849
 
1767
- def makeUFOPath(path):
1850
+ def makeUFOPath(path: PathStr) -> str:
1768
1851
  """
1769
1852
  Return a .ufo pathname.
1770
1853
 
@@ -1791,7 +1874,7 @@ def makeUFOPath(path):
1791
1874
  # cases of invalid values.
1792
1875
 
1793
1876
 
1794
- def validateFontInfoVersion2ValueForAttribute(attr, value):
1877
+ def validateFontInfoVersion2ValueForAttribute(attr: str, value: Any) -> bool:
1795
1878
  """
1796
1879
  This performs very basic validation of the value for attribute
1797
1880
  following the UFO 2 fontinfo.plist specification. The results
@@ -1803,7 +1886,7 @@ def validateFontInfoVersion2ValueForAttribute(attr, value):
1803
1886
  """
1804
1887
  dataValidationDict = fontInfoAttributesVersion2ValueData[attr]
1805
1888
  valueType = dataValidationDict.get("type")
1806
- validator = dataValidationDict.get("valueValidator")
1889
+ validator = dataValidationDict.get("valueValidator", genericTypeValidator)
1807
1890
  valueOptions = dataValidationDict.get("valueOptions")
1808
1891
  # have specific options for the validator
1809
1892
  if valueOptions is not None:
@@ -1817,7 +1900,7 @@ def validateFontInfoVersion2ValueForAttribute(attr, value):
1817
1900
  return isValidValue
1818
1901
 
1819
1902
 
1820
- def validateInfoVersion2Data(infoData):
1903
+ def validateInfoVersion2Data(infoData: dict[str, Any]) -> dict[str, Any]:
1821
1904
  """
1822
1905
  This performs very basic validation of the value for infoData
1823
1906
  following the UFO 2 fontinfo.plist specification. The results
@@ -1837,7 +1920,7 @@ def validateInfoVersion2Data(infoData):
1837
1920
  return validInfoData
1838
1921
 
1839
1922
 
1840
- def validateFontInfoVersion3ValueForAttribute(attr, value):
1923
+ def validateFontInfoVersion3ValueForAttribute(attr: str, value: Any) -> bool:
1841
1924
  """
1842
1925
  This performs very basic validation of the value for attribute
1843
1926
  following the UFO 3 fontinfo.plist specification. The results
@@ -1849,7 +1932,7 @@ def validateFontInfoVersion3ValueForAttribute(attr, value):
1849
1932
  """
1850
1933
  dataValidationDict = fontInfoAttributesVersion3ValueData[attr]
1851
1934
  valueType = dataValidationDict.get("type")
1852
- validator = dataValidationDict.get("valueValidator")
1935
+ validator = dataValidationDict.get("valueValidator", genericTypeValidator)
1853
1936
  valueOptions = dataValidationDict.get("valueOptions")
1854
1937
  # have specific options for the validator
1855
1938
  if valueOptions is not None:
@@ -1863,7 +1946,7 @@ def validateFontInfoVersion3ValueForAttribute(attr, value):
1863
1946
  return isValidValue
1864
1947
 
1865
1948
 
1866
- def validateInfoVersion3Data(infoData):
1949
+ def validateInfoVersion3Data(infoData: dict[str, Any]) -> dict[str, Any]:
1867
1950
  """
1868
1951
  This performs very basic validation of the value for infoData
1869
1952
  following the UFO 3 fontinfo.plist specification. The results
@@ -1885,18 +1968,18 @@ def validateInfoVersion3Data(infoData):
1885
1968
 
1886
1969
  # Value Options
1887
1970
 
1888
- fontInfoOpenTypeHeadFlagsOptions = list(range(0, 15))
1889
- fontInfoOpenTypeOS2SelectionOptions = [1, 2, 3, 4, 7, 8, 9]
1890
- fontInfoOpenTypeOS2UnicodeRangesOptions = list(range(0, 128))
1891
- fontInfoOpenTypeOS2CodePageRangesOptions = list(range(0, 64))
1892
- 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]
1893
1976
 
1894
1977
  # Version Attribute Definitions
1895
1978
  # This defines the attributes, types and, in some
1896
1979
  # cases the possible values, that can exist is
1897
1980
  # fontinfo.plist.
1898
1981
 
1899
- fontInfoAttributesVersion1 = {
1982
+ fontInfoAttributesVersion1: set[str] = {
1900
1983
  "familyName",
1901
1984
  "styleName",
1902
1985
  "fullName",
@@ -1939,7 +2022,7 @@ fontInfoAttributesVersion1 = {
1939
2022
  "ttVersion",
1940
2023
  }
1941
2024
 
1942
- fontInfoAttributesVersion2ValueData = {
2025
+ fontInfoAttributesVersion2ValueData: FontInfoAttributes = {
1943
2026
  "familyName": dict(type=str),
1944
2027
  "styleName": dict(type=str),
1945
2028
  "styleMapFamilyName": dict(type=str),
@@ -2081,9 +2164,11 @@ fontInfoAttributesVersion2ValueData = {
2081
2164
  "macintoshFONDFamilyID": dict(type=int),
2082
2165
  "macintoshFONDName": dict(type=str),
2083
2166
  }
2084
- fontInfoAttributesVersion2 = set(fontInfoAttributesVersion2ValueData.keys())
2167
+ fontInfoAttributesVersion2: set[str] = set(fontInfoAttributesVersion2ValueData.keys())
2085
2168
 
2086
- fontInfoAttributesVersion3ValueData = deepcopy(fontInfoAttributesVersion2ValueData)
2169
+ fontInfoAttributesVersion3ValueData: FontInfoAttributes = deepcopy(
2170
+ fontInfoAttributesVersion2ValueData
2171
+ )
2087
2172
  fontInfoAttributesVersion3ValueData.update(
2088
2173
  {
2089
2174
  "versionMinor": dict(type=int, valueValidator=genericNonNegativeIntValidator),
@@ -2166,7 +2251,7 @@ fontInfoAttributesVersion3ValueData.update(
2166
2251
  "guidelines": dict(type=list, valueValidator=guidelinesValidator),
2167
2252
  }
2168
2253
  )
2169
- fontInfoAttributesVersion3 = set(fontInfoAttributesVersion3ValueData.keys())
2254
+ fontInfoAttributesVersion3: set[str] = set(fontInfoAttributesVersion3ValueData.keys())
2170
2255
 
2171
2256
  # insert the type validator for all attrs that
2172
2257
  # have no defined validator.
@@ -2183,14 +2268,14 @@ for attr, dataDict in list(fontInfoAttributesVersion3ValueData.items()):
2183
2268
  # to version 2 or vice-versa.
2184
2269
 
2185
2270
 
2186
- def _flipDict(d):
2271
+ def _flipDict(d: dict[K, V]) -> dict[V, K]:
2187
2272
  flipped = {}
2188
2273
  for key, value in list(d.items()):
2189
2274
  flipped[value] = key
2190
2275
  return flipped
2191
2276
 
2192
2277
 
2193
- fontInfoAttributesVersion1To2 = {
2278
+ fontInfoAttributesVersion1To2: dict[str, str] = {
2194
2279
  "menuName": "styleMapFamilyName",
2195
2280
  "designer": "openTypeNameDesigner",
2196
2281
  "designerURL": "openTypeNameDesignerURL",
@@ -2222,12 +2307,17 @@ fontInfoAttributesVersion1To2 = {
2222
2307
  fontInfoAttributesVersion2To1 = _flipDict(fontInfoAttributesVersion1To2)
2223
2308
  deprecatedFontInfoAttributesVersion2 = set(fontInfoAttributesVersion1To2.keys())
2224
2309
 
2225
- _fontStyle1To2 = {64: "regular", 1: "italic", 32: "bold", 33: "bold italic"}
2226
- _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)
2227
2317
  # Some UFO 1 files have 0
2228
2318
  _fontStyle1To2[0] = "regular"
2229
2319
 
2230
- _widthName1To2 = {
2320
+ _widthName1To2: dict[str, int] = {
2231
2321
  "Ultra-condensed": 1,
2232
2322
  "Extra-condensed": 2,
2233
2323
  "Condensed": 3,
@@ -2238,7 +2328,7 @@ _widthName1To2 = {
2238
2328
  "Extra-expanded": 8,
2239
2329
  "Ultra-expanded": 9,
2240
2330
  }
2241
- _widthName2To1 = _flipDict(_widthName1To2)
2331
+ _widthName2To1: dict[int, str] = _flipDict(_widthName1To2)
2242
2332
  # FontLab's default width value is "Normal".
2243
2333
  # Many format version 1 UFOs will have this.
2244
2334
  _widthName1To2["Normal"] = 5
@@ -2250,7 +2340,7 @@ _widthName1To2["medium"] = 5
2250
2340
  # "Medium" appears in a lot of UFO 1 files.
2251
2341
  _widthName1To2["Medium"] = 5
2252
2342
 
2253
- _msCharSet1To2 = {
2343
+ _msCharSet1To2: dict[int, int] = {
2254
2344
  0: 1,
2255
2345
  1: 2,
2256
2346
  2: 3,
@@ -2272,12 +2362,14 @@ _msCharSet1To2 = {
2272
2362
  238: 19,
2273
2363
  255: 20,
2274
2364
  }
2275
- _msCharSet2To1 = _flipDict(_msCharSet1To2)
2365
+ _msCharSet2To1: dict[int, int] = _flipDict(_msCharSet1To2)
2276
2366
 
2277
2367
  # 1 <-> 2
2278
2368
 
2279
2369
 
2280
- def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value):
2370
+ def convertFontInfoValueForAttributeFromVersion1ToVersion2(
2371
+ attr: str, value: Any
2372
+ ) -> tuple[str, Any]:
2281
2373
  """
2282
2374
  Convert value from version 1 to version 2 format.
2283
2375
  Returns the new attribute name and the converted value.
@@ -2289,7 +2381,7 @@ def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value):
2289
2381
  value = int(value)
2290
2382
  if value is not None:
2291
2383
  if attr == "fontStyle":
2292
- v = _fontStyle1To2.get(value)
2384
+ v: Optional[Union[str, int]] = _fontStyle1To2.get(value)
2293
2385
  if v is None:
2294
2386
  raise UFOLibError(
2295
2387
  f"Cannot convert value ({value!r}) for attribute {attr}."
@@ -2313,7 +2405,9 @@ def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value):
2313
2405
  return attr, value
2314
2406
 
2315
2407
 
2316
- def convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value):
2408
+ def convertFontInfoValueForAttributeFromVersion2ToVersion1(
2409
+ attr: str, value: Any
2410
+ ) -> tuple[str, Any]:
2317
2411
  """
2318
2412
  Convert value from version 2 to version 1 format.
2319
2413
  Returns the new attribute name and the converted value.
@@ -2330,7 +2424,7 @@ def convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value):
2330
2424
  return attr, value
2331
2425
 
2332
2426
 
2333
- def _convertFontInfoDataVersion1ToVersion2(data):
2427
+ def _convertFontInfoDataVersion1ToVersion2(data: dict[str, Any]) -> dict[str, Any]:
2334
2428
  converted = {}
2335
2429
  for attr, value in list(data.items()):
2336
2430
  # FontLab gives -1 for the weightValue
@@ -2354,7 +2448,7 @@ def _convertFontInfoDataVersion1ToVersion2(data):
2354
2448
  return converted
2355
2449
 
2356
2450
 
2357
- def _convertFontInfoDataVersion2ToVersion1(data):
2451
+ def _convertFontInfoDataVersion2ToVersion1(data: dict[str, Any]) -> dict[str, Any]:
2358
2452
  converted = {}
2359
2453
  for attr, value in list(data.items()):
2360
2454
  newAttr, newValue = convertFontInfoValueForAttributeFromVersion2ToVersion1(
@@ -2375,16 +2469,16 @@ def _convertFontInfoDataVersion2ToVersion1(data):
2375
2469
 
2376
2470
  # 2 <-> 3
2377
2471
 
2378
- _ufo2To3NonNegativeInt = {
2472
+ _ufo2To3NonNegativeInt: set[str] = {
2379
2473
  "versionMinor",
2380
2474
  "openTypeHeadLowestRecPPEM",
2381
2475
  "openTypeOS2WinAscent",
2382
2476
  "openTypeOS2WinDescent",
2383
2477
  }
2384
- _ufo2To3NonNegativeIntOrFloat = {
2478
+ _ufo2To3NonNegativeIntOrFloat: set[str] = {
2385
2479
  "unitsPerEm",
2386
2480
  }
2387
- _ufo2To3FloatToInt = {
2481
+ _ufo2To3FloatToInt: set[str] = {
2388
2482
  "openTypeHeadLowestRecPPEM",
2389
2483
  "openTypeHheaAscender",
2390
2484
  "openTypeHheaDescender",
@@ -2412,7 +2506,9 @@ _ufo2To3FloatToInt = {
2412
2506
  }
2413
2507
 
2414
2508
 
2415
- def convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value):
2509
+ def convertFontInfoValueForAttributeFromVersion2ToVersion3(
2510
+ attr: str, value: Any
2511
+ ) -> tuple[str, Any]:
2416
2512
  """
2417
2513
  Convert value from version 2 to version 3 format.
2418
2514
  Returns the new attribute name and the converted value.
@@ -2440,7 +2536,9 @@ def convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value):
2440
2536
  return attr, value
2441
2537
 
2442
2538
 
2443
- def convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value):
2539
+ def convertFontInfoValueForAttributeFromVersion3ToVersion2(
2540
+ attr: str, value: Any
2541
+ ) -> tuple[str, Any]:
2444
2542
  """
2445
2543
  Convert value from version 3 to version 2 format.
2446
2544
  Returns the new attribute name and the converted value.
@@ -2449,7 +2547,7 @@ def convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value):
2449
2547
  return attr, value
2450
2548
 
2451
2549
 
2452
- def _convertFontInfoDataVersion3ToVersion2(data):
2550
+ def _convertFontInfoDataVersion3ToVersion2(data: dict[str, Any]) -> dict[str, Any]:
2453
2551
  converted = {}
2454
2552
  for attr, value in list(data.items()):
2455
2553
  newAttr, newValue = convertFontInfoValueForAttributeFromVersion3ToVersion2(
@@ -2461,7 +2559,7 @@ def _convertFontInfoDataVersion3ToVersion2(data):
2461
2559
  return converted
2462
2560
 
2463
2561
 
2464
- def _convertFontInfoDataVersion2ToVersion3(data):
2562
+ def _convertFontInfoDataVersion2ToVersion3(data: dict[str, Any]) -> dict[str, Any]:
2465
2563
  converted = {}
2466
2564
  for attr, value in list(data.items()):
2467
2565
  attr, value = convertFontInfoValueForAttributeFromVersion2ToVersion3(