Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/explanations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ changes, but complicates adding libs to the objects directly. Since ufoLib2
intentionally lacks defcon's parent-child object hierarchy, a guideline can't
follow a link to the containing parent and access its lib. Magically loading
and storing the ``public.objectLibs`` lib key on loading and saving a UFO adds
an uncomfortable amount of magic and edge-casiness. What if a client itself
an uncomfortable amount of magic and edge-cases. What if a client itself
adds a ``public.objectLibs`` entry for an object that differs from what is in
that object's lib? Overwrite, ignore, error out? With the ``.objectLib``
method, this edge case does not exist.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ multi_line_output = 3
profile = "black"
float_to_top = true
known_first_party = "ufoLib2"
split_on_trailing_comma = false

[tool.mypy]
python_version = "3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/ufoLib2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- https://unifiedfontobject.org/versions/ufo3/glyphs/glif/#publicobjectlibs
"""

DATA_LIB_KEY = "com.github.fonttools.ufoLib2.lib.plist.data"
DATA_LIB_KEY: str = "com.github.fonttools.ufoLib2.lib.plist.data"
"""
Lib key used for serializing binary data as JSON-encodable string.
"""
45 changes: 16 additions & 29 deletions src/ufoLib2/converters.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,42 @@
from __future__ import annotations

import sys
from collections.abc import Callable
from functools import partial
from typing import Any, Callable, Tuple, Type, cast
from typing import Any, cast, get_origin

from attrs import fields, has, resolve_types
from cattrs import Converter
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override
from cattrs.gen._consts import AttributeOverride
from fontTools.misc.transform import Transform

is_py37 = sys.version_info[:2] == (3, 7)

if is_py37:

def get_origin(cl: Type[Any]) -> Any:
return getattr(cl, "__origin__", None)

else:
from typing import get_origin # type: ignore


__all__ = [
"register_hooks",
"structure",
"unstructure",
]


def is_ufoLib2_class(cls: Type[Any]) -> bool:
def is_ufoLib2_class(cls: type[Any]) -> bool:
mod: str = getattr(cls, "__module__", "")
return mod.split(".")[0] == "ufoLib2"


def is_ufoLib2_attrs_class(cls: Type[Any]) -> bool:
return is_ufoLib2_class(cls) and (has(cls) or has(get_origin(cls)))
def is_ufoLib2_attrs_class(cls: type[Any]) -> bool:
return is_ufoLib2_class(cls) and (has(cls) or has(get_origin(cls))) # type: ignore


def is_ufoLib2_class_with_custom_unstructure(cls: Type[Any]) -> bool:
def is_ufoLib2_class_with_custom_unstructure(cls: type[Any]) -> bool:
return is_ufoLib2_class(cls) and hasattr(cls, "_unstructure")


def is_ufoLib2_class_with_custom_structure(cls: Type[Any]) -> bool:
def is_ufoLib2_class_with_custom_structure(cls: type[Any]) -> bool:
return is_ufoLib2_class(cls) and hasattr(cls, "_structure")


def register_hooks(conv: Converter, allow_bytes: bool = True) -> None:
def attrs_hook_factory(
cls: Type[Any], gen_fn: Callable[..., Callable[..., Any]], structuring: bool
cls: type[Any], gen_fn: Callable[..., Callable[..., Any]], structuring: bool
) -> Callable[..., Any]:
base = get_origin(cls)
if base is None:
Expand All @@ -74,7 +63,7 @@ def attrs_hook_factory(
else:
# by default, we omit all Optional attributes (i.e. with None default),
# overriding a Converter's global 'omit_if_default' option. Specific
# attibutes can still define their own 'omit_if_default' behavior in
# attributes can still define their own 'omit_if_default' behavior in
# the Attribute.metadata dict.
attrib_override = override(
omit_if_default=a.metadata.get(
Expand All @@ -89,16 +78,16 @@ def attrs_hook_factory(

return gen_fn(cls, conv, **kwargs)

def custom_unstructure_hook_factory(cls: Type[Any]) -> Callable[[Any], Any]:
def custom_unstructure_hook_factory(cls: type[Any]) -> Callable[[Any], Any]:
return partial(cls._unstructure, converter=conv)

def custom_structure_hook_factory(cls: Type[Any]) -> Callable[[Any, Any], Any]:
def custom_structure_hook_factory(cls: type[Any]) -> Callable[[Any, Any], Any]:
return partial(cls._structure, converter=conv)

def unstructure_transform(t: Transform) -> Tuple[float]:
return cast(Tuple[float], tuple(t))
def unstructure_transform(t: Transform) -> tuple[float]:
return cast(tuple[float], tuple(t))

def structure_transform(t: Tuple[float], _: Any) -> Transform:
def structure_transform(t: tuple[float], _: Any) -> Transform:
return Transform(*t)

conv.register_unstructure_hook_factory(
Expand All @@ -109,11 +98,9 @@ def structure_transform(t: Tuple[float], _: Any) -> Transform:
is_ufoLib2_class_with_custom_unstructure,
custom_unstructure_hook_factory,
)
conv.register_unstructure_hook(
cast(Type[Transform], Transform), unstructure_transform
)
conv.register_unstructure_hook(Transform, unstructure_transform)

conv.register_structure_hook(cast(Type[Transform], Transform), structure_transform)
conv.register_structure_hook(Transform, structure_transform)
conv.register_structure_hook_factory(
is_ufoLib2_attrs_class,
partial(attrs_hook_factory, gen_fn=make_dict_structure_fn, structuring=True),
Expand Down
8 changes: 3 additions & 5 deletions src/ufoLib2/objects/anchor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from __future__ import annotations

from typing import Optional

from attrs import define

from ufoLib2.objects.misc import AttrDictMixin
Expand All @@ -22,13 +20,13 @@ class Anchor(AttrDictMixin):
y: float
"""The y coordinate of the anchor."""

name: Optional[str] = None
name: str | None = None
"""The name of the anchor."""

color: Optional[str] = None
color: str | None = None
"""The color of the anchor."""

identifier: Optional[str] = None
identifier: str | None = None
"""The globally unique identifier of the anchor."""

def move(self, delta: tuple[float, float]) -> None:
Expand Down
3 changes: 1 addition & 2 deletions src/ufoLib2/objects/component.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import warnings
from typing import Optional

from attrs import define, field
from fontTools.misc.transform import Identity, Transform
Expand Down Expand Up @@ -33,7 +32,7 @@ class Component:
transformation: Transform = field(default=Identity, converter=_convert_transform)
"""The affine transformation to apply to the :attr:`.Component.baseGlyph`."""

identifier: Optional[str] = None
identifier: str | None = None
"""The globally unique identifier of the component."""

def move(self, delta: tuple[float, float]) -> None:
Expand Down
7 changes: 4 additions & 3 deletions src/ufoLib2/objects/contour.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import warnings
from typing import Iterable, Iterator, List, MutableSequence, Optional, overload
from collections.abc import Iterable, Iterator, MutableSequence
from typing import overload

from attrs import define, field
from fontTools.pens.basePen import AbstractPen
Expand Down Expand Up @@ -42,10 +43,10 @@ class Contour(MutableSequence[Point]):
contour[0] = anotherPoint
"""

points: List[Point] = field(factory=list)
points: list[Point] = field(factory=list)
"""The list of points in the contour."""

identifier: Optional[str] = field(default=None, repr=False)
identifier: str | None = field(default=None, repr=False)
"""The globally unique identifier of the contour."""

# collections.abc.MutableSequence interface
Expand Down
2 changes: 1 addition & 1 deletion src/ufoLib2/objects/dataSet.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class DataSet(DataStore):
"""Represents a mapping of POSIX filename strings to arbitrary data bytes.

Always use forward slahes (/) as directory separators, even on Windows.
Always use forward slashes (/) as directory separators, even on Windows.

Behavior:
DataSet behaves like a dictionary of type ``Dict[str, bytes]``.
Expand Down
4 changes: 2 additions & 2 deletions src/ufoLib2/objects/features.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import re
from typing import TYPE_CHECKING, Type
from typing import TYPE_CHECKING

from attrs import define

Expand Down Expand Up @@ -41,6 +41,6 @@ def _unstructure(self, converter: Converter) -> str:
return self.text

@staticmethod
def _structure(data: str, cls: Type[Features], converter: Converter) -> Features:
def _structure(data: str, cls: type[Features], converter: Converter) -> Features:
del converter # unused
return cls(data)
26 changes: 8 additions & 18 deletions src/ufoLib2/objects/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@

import os
import shutil
import sys
import tempfile
from typing import (
Any,
Dict,
from collections.abc import (
Iterable,
Iterator,
KeysView,
List,
Mapping,
MutableMapping,
Optional,
Sequence,
Comment thread
madig marked this conversation as resolved.
cast,
)
from typing import Any, cast

from attrs import define, field
from fontTools.ufoLib import UFOFileStructure, UFOReader, UFOWriter
Expand Down Expand Up @@ -159,7 +154,7 @@ class Font:
features: Features = field(factory=Features, converter=_convert_Features)
"""Features: The font Features object."""

groups: Dict[str, List[str]] = field(factory=dict)
groups: dict[str, list[str]] = field(factory=dict[str, list[str]])
"""Dict[str, List[str]]: A mapping of group names to a list of glyph names."""

_kerning: Kerning = field(factory=Kerning, converter=_convert_Kerning)
Expand All @@ -179,12 +174,10 @@ class Font:
"""Dict[str, PlistEncodable]: A temporary map of arbitrary plist values."""

# init=False args, set by alternate open/read classmethod constructors
_path: Optional[PathLike] = field(default=None, eq=False, init=False)
_lazy: Optional[bool] = field(default=None, init=False, eq=False)
_reader: Optional[UFOReader] = field(default=None, init=False, eq=False)
_fileStructure: Optional[UFOFileStructure] = field(
default=None, init=False, eq=False
)
_path: PathLike | None = field(default=None, eq=False, init=False)
_lazy: bool | None = field(default=None, init=False, eq=False)
_reader: UFOReader | None = field(default=None, init=False, eq=False)
_fileStructure: UFOFileStructure | None = field(default=None, init=False, eq=False)

@classmethod
def open(cls, path: PathLike, lazy: bool = True, validate: bool = True) -> Font:
Expand Down Expand Up @@ -576,10 +569,7 @@ def save(
if isinstance(path, str) and os.path.exists(path):
if overwrite:
overwritePath = path
if sys.version_info < (3, 10):
tmp = tempfile.TemporaryDirectory()
else:
tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
path = os.path.join(tmp.name, os.path.basename(path))
else:
import errno
Expand Down
17 changes: 9 additions & 8 deletions src/ufoLib2/objects/glyph.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

from collections.abc import Iterator, Mapping
from copy import deepcopy
from typing import Any, Iterator, List, Mapping, Optional, cast
from typing import Any, Optional, cast

from attrs import define, field
from fontTools.misc.transform import Transform
Expand Down Expand Up @@ -62,15 +63,15 @@ class Glyph:
:attr:`.Glyph.components` and :attr:`.Glyph.anchors` attributes.
"""

_name: Optional[str] = None
_name: str | None = None

width: float = 0
"""The width of the glyph."""

height: float = 0
"""The height of the glyph."""

unicodes: List[int] = field(factory=list)
unicodes: list[int] = field(factory=list[int])
"""The Unicode code points assigned to the glyph. Note that a glyph can have
multiple."""

Expand All @@ -79,17 +80,17 @@ class Glyph:
_lib: Lib = field(factory=Lib, converter=_convert_Lib)
"""The glyph's mapping of string keys to arbitrary data."""

note: Optional[str] = None
note: str | None = None
"""A free form text note about the glyph."""

_anchors: List[Anchor] = field(factory=list)
components: List[Component] = field(factory=list)
_anchors: list[Anchor] = field(factory=list[Anchor])
components: list[Component] = field(factory=list[Component])
"""The list of components the glyph contains."""

contours: List[Contour] = field(factory=list)
contours: list[Contour] = field(factory=list[Contour])
"""The list of contours the glyph contains."""

_guidelines: List[Guideline] = field(factory=list)
_guidelines: list[Guideline] = field(factory=list[Guideline])

_tempLib: Lib = field(factory=Lib, converter=_convert_Lib)
"""A temporary map of arbitrary plist values."""
Expand Down
14 changes: 6 additions & 8 deletions src/ufoLib2/objects/guideline.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from __future__ import annotations

from typing import Optional

from attrs import define

from ufoLib2.objects.misc import AttrDictMixin
Expand All @@ -17,22 +15,22 @@ class Guideline(AttrDictMixin):
data composition restrictions.
"""

x: Optional[float] = None
x: float | None = None
"""The origin x coordinate of the guideline."""

y: Optional[float] = None
y: float | None = None
"""The origin y coordinate of the guideline."""

angle: Optional[float] = None
angle: float | None = None
"""The angle of the guideline."""

name: Optional[str] = None
name: str | None = None
"""The name of the guideline, no uniqueness required."""

color: Optional[str] = None
color: str | None = None
"""The color of the guideline."""

identifier: Optional[str] = None
identifier: str | None = None
"""The globally unique identifier of the guideline."""

def __attrs_post_init__(self) -> None:
Expand Down
Loading
Loading